未验证 提交 e99971cb 编写于 作者: A Andrew Gould 提交者: GitHub

Add RotaryEncoder Binding (#705)

* Add RotaryEncoder Binding

* Created a Separate Scaled binding and added properties to represent rotation.

* Adjusting rotary with limitations, simplifying without generics, nullable enabled

* Adjusting based on PR feedback

* Correcting sample
Co-authored-by: NLaurent Ellerbach <laurelle@microsoft.com>
上级 41093404
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Device.Gpio;
using System.Diagnostics;
namespace Iot.Device.RotaryEncoder
{
/// <summary>
/// Binding that exposes a quadrature rotary encoder
/// </summary>
public class QuadratureRotaryEncoder : IDisposable
{
private GpioController _controller;
private int _pinA;
private int _pinB;
private bool _disposeController = true;
private Stopwatch _debouncer = new Stopwatch();
private uint _debounceMillisec;
/// <summary>
/// The number of pulses expected per rotation of the encoder
/// </summary>
public int PulsesPerRotation { get; private set; }
/// <summary>
/// The number of pulses before or after the start position of the encoder
/// </summary>
public long PulseCount { get; set; }
/// <summary>
/// The number of rotations backwards or forwards from the initial position of the encoder
/// </summary>
public float Rotations { get => (float)PulseCount / PulsesPerRotation; }
/// <summary>The Debounce property represents the minimum amount of delay
/// allowed between falling edges of the A (clk) pin. The recommended value are few milliseconds typically around 5.
/// This depends from your usage.</summary>
public TimeSpan Debounce
{
get => TimeSpan.FromMilliseconds(_debounceMillisec);
set
{
_debounceMillisec = (uint)value.TotalMilliseconds;
}
}
/// <summary>
/// EventHandler to allow the notification of value changes.
/// </summary>
public event EventHandler<RotaryEncoderEventArgs>? PulseCountChanged;
/// <summary>
/// QuadratureRotaryEncoder constructor
/// </summary>
/// <param name="pinA">Pin A that is connected to the rotary encoder. Sometimes called clk</param>
/// <param name="pinB">Pin B that is connected to the rotary encoder. Sometimes called data</param>
/// <param name="edges">The pin event types to 'listen' for.</param>
/// <param name="pulsesPerRotation">The number of pulses to be received for every full rotation of the encoder.</param>
/// <param name="controller">GpioController that hosts Pins A and B.</param>
/// <param name="shouldDispose">True to dispose the controller</param>
public QuadratureRotaryEncoder(int pinA, int pinB, PinEventTypes edges, int pulsesPerRotation, GpioController? controller = null, bool shouldDispose = true)
{
_disposeController = controller == null | shouldDispose;
_controller = controller ?? new GpioController();
PulsesPerRotation = pulsesPerRotation;
_debounceMillisec = 5;
Initialize(pinA, pinB, edges);
}
/// <summary>
/// QuadratureRotaryEncoder constructor
/// </summary>
/// <param name="pinA">Pin A that is connected to the rotary encoder. Sometimes called clk</param>
/// <param name="pinB">Pin B that is connected to the rotary encoder. Sometimes called data</param>
/// <param name="pulsesPerRotation">The number of pulses to be received for every full rotation of the encoder.</param>
public QuadratureRotaryEncoder(int pinA, int pinB, int pulsesPerRotation)
: this(pinA, pinB, PinEventTypes.Falling, pulsesPerRotation, new GpioController(), false)
{
}
/// <summary>
/// Modify the current value on receipt of a pulse from the rotary encoder.
/// </summary>
/// <param name="blnUp">When true then the value should be incremented otherwise it should be decremented.</param>
/// <param name="milliSecondsSinceLastPulse">The number of miliseconds since the last pulse.</param>
protected virtual void OnPulse(bool blnUp, int milliSecondsSinceLastPulse)
{
PulseCount += blnUp ? 1 : -1;
// fire an event if an event handler has been attached
PulseCountChanged?.Invoke(this, new RotaryEncoderEventArgs(PulseCount));
}
/// <summary>
/// Initialize an QuadratureRotaryEncoder
/// </summary>
/// <param name="pinA">Pin A that is connected to the rotary encoder. Sometimes called clk</param>
/// <param name="pinB">Pin B that is connected to the rotary encoder. Sometimes called data</param>
/// <param name="edges">The pin event types to 'listen' for.</param>
private void Initialize(int pinA, int pinB, PinEventTypes edges)
{
_pinA = pinA;
_pinB = pinB;
_controller.OpenPin(_pinA, PinMode.Input);
_controller.OpenPin(_pinB, PinMode.Input);
_debouncer.Start();
_controller.RegisterCallbackForPinValueChangedEvent(_pinA, edges, (o, e) =>
{
if (_debounceMillisec == 0 | _debouncer.ElapsedMilliseconds > _debounceMillisec)
{
OnPulse(_controller.Read(_pinA) == _controller.Read(_pinB), (int)_debouncer.ElapsedMilliseconds);
}
_debouncer.Restart();
});
}
/// <inheritdoc/>
public void Dispose()
{
_controller?.ClosePin(_pinA);
_controller?.ClosePin(_pinB);
if (_disposeController)
{
_controller?.Dispose();
}
}
}
}
# Quadrature Rotary Encoder
## Summary
A Rotary encoder is a device that detects angular position. One use of this is similar to a volume control on an FM radio where the user turns a shaft and the loudness of the broadcast is changed. Incremental rotary encoders do not provide information on their exact position but supply information about how much they have moved and in which direction.
![image of rotary encoder](pec11r.png)
Typically a quadrature rotary encoder will have two outputs A and B, perhaps called clock and data. For each part of a rotation then the A pin will provide a clock signal and the B pin will provide a data signal that is out of phase with the clock. The sign of the phase difference between the pins indicates the direction of rotation.
![encoder](encoder.png)
From above if we look at Pin B (data) at the time of a falling edge on Pin A (clk) then the if the value of pin P is 1 then the direction is clockwise and if it is 0 then the rotation is counter clockwise.
## Binding Notes
This binding implements scaled quadrature rotary encoder as `ScaledQuadratureEncoder`. The value is a double. You can for example set it up as a tuning dial for an FM radio with a range of 88.0 to 108.0 with a step of 0.1.
The code below shows an example of using the encoder as an FM tuner control.
```csharp
using (GpioController controller = new GpioController())
{
// create a RotaryEncoder that represents an FM Radio tuning dial with a range of 88 -> 108 MHz
ScaledQuadratureEncoder encoder = new ScaledQuadratureEncoder(new GpioController(), pinA: 5, pinB: 6, PinEventTypes.Falling, pulsesPerRotation: 20 , pulseIncrement: 0.1, rangeMin: 88.0, rangeMax: 108.0) { Value = 88 };
encoder.ValueChanged += (o, e) =>
{
Console.WriteLine($"Value {e.Value}");
};
// Do Other Stuff
}
```
This binding also features
- Debounce functionality on the clock signal.
- Acceleration so that rotating the encoder moves it further the faster the rotation.
- Events when the value changes.
Also available is a `QuadratureRotaryEncoder` binding which has properties that represent the rotation of the encoder and the raw pulses.
## Limitations
This binding is suitable for manual and small rotations where it is not a big deal if one or few rotations may be lost.
This binding **is not** suitable for motor control with a very high rate and very precise number of counts.
The precision really depends of the hardware you are using and it is not possible to give specific range of usage. You may have to try to understand if this is working for you or not.
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<!--Disabling default items so samples source won't get build by the main library-->
<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>
<ItemGroup>
<Compile Include="*.cs" />
<None Include="README.md" />
<ProjectReference Include="$(MainLibraryPath)System.Device.Gpio.csproj">
<AdditionalProperties>$(AdditionalProperties);RuntimeIdentifier=linux</AdditionalProperties>
</ProjectReference>
</ItemGroup>
</Project>

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30626.31
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RotaryEncoder", "RotaryEncoder.csproj", "{9E6A8E8F-19B6-46CD-B1B5-674FD3627E57}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RotaryEncoder.Samples", "samples\RotaryEncoder.Samples.csproj", "{C421C15C-BC7E-4D64-81CD-A7F0ED901B2E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9E6A8E8F-19B6-46CD-B1B5-674FD3627E57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E6A8E8F-19B6-46CD-B1B5-674FD3627E57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E6A8E8F-19B6-46CD-B1B5-674FD3627E57}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E6A8E8F-19B6-46CD-B1B5-674FD3627E57}.Release|Any CPU.Build.0 = Release|Any CPU
{C421C15C-BC7E-4D64-81CD-A7F0ED901B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C421C15C-BC7E-4D64-81CD-A7F0ED901B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C421C15C-BC7E-4D64-81CD-A7F0ED901B2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C421C15C-BC7E-4D64-81CD-A7F0ED901B2E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D15FEC8F-EFCE-4415-8F0E-7244FB6A5812}
EndGlobalSection
EndGlobal
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Iot.Device.RotaryEncoder
{
/// <summary>
/// EventArgs used with the RotaryEncode binding to pass event information when the Value changes.
/// </summary>
public class RotaryEncoderEventArgs : EventArgs
{
/// <summary>The Value property represents current value associated with the RotaryEncoder.</summary>
public double Value { get; private set; }
/// <summary>
/// Construct a new RotaryEncoderEventArgs
/// </summary>
/// <param name="value">Current value associated with the rotary encoder</param>
public RotaryEncoderEventArgs(double value)
{
Value = value;
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Device.Gpio;
namespace Iot.Device.RotaryEncoder
{
/// <summary>
/// Scaled Quadrature Rotary Controller binding
/// </summary>
public class ScaledQuadratureEncoder : QuadratureRotaryEncoder
{
private double _rangeMax;
private double _rangeMin;
private double _pulseIncrement;
/// <summary>The Value property represents current value associated with the RotaryEncoder.</summary>
public double Value { get; set; }
/// <summary>The AccelerationSlope property along with the AccelerationOffset property represents how the
/// increase or decrease in value should grow as the incremental encoder is turned faster.</summary>
public float AccelerationSlope { get; set; } = -0.05F;
/// <summary>The AccelerationOffset property along with the AccelerationSlope property represents how the
/// increase or decrease in value should grow as the incremental encoder is turned faster.</summary>
public float AccelerationOffset { get; set; } = 6.0F;
/// <summary>
/// EventHandler to allow the notification of value changes.
/// </summary>
public event EventHandler<RotaryEncoderEventArgs>? ValueChanged;
/// <summary>
/// ScaledQuadratureEncoder constructor
/// </summary>
/// <param name="pinA">Pin A that is connected to the rotary encoder. Sometimes called clk</param>
/// <param name="pinB">Pin B that is connected to the rotary encoder. Sometimes called data</param>
/// <param name="edges">The pin event types to 'listen' for.</param>
/// <param name="pulsesPerRotation">The number of pulses to be received for every full rotation of the encoder.</param>
/// <param name="pulseIncrement">The amount that the value increases or decreases on each pulse from the rotary encoder</param>
/// <param name="rangeMin">Minimum value permitted. The value is clamped to this.</param>
/// <param name="rangeMax">Maximum value permitted. The value is clamped to this.</param>
/// <param name="controller">GpioController that hosts Pins A and B.</param>
/// <param name="shouldDispose">Dispose the controller if true</param>
public ScaledQuadratureEncoder(int pinA, int pinB, PinEventTypes edges, int pulsesPerRotation, double pulseIncrement, double rangeMin, double rangeMax, GpioController? controller = null, bool shouldDispose = true)
: base(pinA, pinB, edges, pulsesPerRotation, controller, shouldDispose)
{
_pulseIncrement = pulseIncrement;
_rangeMin = rangeMin;
_rangeMax = rangeMax;
Value = _rangeMin;
}
/// <summary>
/// ScaledQuadratureEncoder constructor
/// </summary>
/// <param name="pinA">Pin A that is connected to the rotary encoder. Sometimes called clk</param>
/// <param name="pinB">Pin B that is connected to the rotary encoder. Sometimes called data</param>
/// <param name="edges">The pin event types to 'listen' for.</param>
/// <param name="pulsesPerRotation">The number of pulses to be received for every full rotation of the encoder.</param>
/// <param name="pulseIncrement">The amount that the value increases or decreases on each pulse from the rotary encoder</param>
/// <param name="rangeMin">Minimum value permitted. The value is clamped to this.</param>
/// <param name="rangeMax">Maximum value permitted. The value is clamped to this.</param>
public ScaledQuadratureEncoder(int pinA, int pinB, PinEventTypes edges, int pulsesPerRotation, double pulseIncrement, double rangeMin, double rangeMax)
: this(pinA, pinB, edges, pulsesPerRotation, pulseIncrement, rangeMin, rangeMax, new GpioController(), true)
{
}
/// <summary>
/// ScaledQuadratureEncoder constructor for a 0..100 range with 100 steps
/// </summary>
/// <param name="pinA">Pin A that is connected to the rotary encoder. Sometimes called clk</param>
/// <param name="pinB">Pin B that is connected to the rotary encoder. Sometimes called data</param>
/// <param name="edges">The pin event types to 'listen' for.</param>
/// <param name="pulsesPerRotation">The number of pulses to be received for every full rotation of the encoder.</param>
public ScaledQuadratureEncoder(int pinA, int pinB, PinEventTypes edges, int pulsesPerRotation)
: this(pinA, pinB, edges, pulsesPerRotation, new GpioController(), true)
{
}
/// <summary>
/// ScaledQuadratureEncoder constructor for a 0..100 range with 100 steps
/// </summary>
/// <param name="pinA">Pin A that is connected to the rotary encoder. Sometimes called clk</param>
/// <param name="pinB">Pin B that is connected to the rotary encoder. Sometimes called data</param>
/// <param name="edges">The pin event types to 'listen' for.</param>
/// <param name="pulsesPerRotation">The number of pulses to be received for every full rotation of the encoder.</param>
/// <param name="controller">GpioController that hosts Pins A and B.</param>
/// <param name="shouldDispose">Dispose the controller if true</param>
public ScaledQuadratureEncoder(int pinA, int pinB, PinEventTypes edges, int pulsesPerRotation, GpioController? controller = null, bool shouldDispose = true)
: base(pinA, pinB, edges, pulsesPerRotation, controller, shouldDispose)
{
_pulseIncrement = (dynamic)1;
_rangeMin = (dynamic)0;
_rangeMax = (dynamic)100;
Value = _rangeMin;
}
/// <summary>
/// Read the current Value
/// </summary>
/// <returns>The value associated with the rotary encoder.</returns>
public double ReadValue() => Value;
/// <summary>
/// Calculate the amount of acceleration to be applied to the increment of the encoder.
/// </summary>
/// <remarks>
/// This uses a straight line function output = input * AccelerationSlope + Acceleration offset but can be overridden
/// to perform different algorithms.
/// </remarks>
/// <param name="milliSecondsSinceLastPulse">The amount of time elapsed since the last data pulse from the encoder in milliseconds.</param>
/// <returns>A value that can be used to apply acceleration to the rotary encoder.</returns>
protected virtual int Acceleration(int milliSecondsSinceLastPulse)
{
// apply a straight line line function to the pulseWidth to determine the acceleration but clamp the lower value to 1
return Math.Max(1, (int)(milliSecondsSinceLastPulse * AccelerationSlope + AccelerationOffset));
}
/// <summary>
/// Modify the current value on receipt of a pulse from the encoder.
/// </summary>
/// <param name="blnUp">When true then the value should be incremented otherwise it should be decremented.</param>
/// <param name="milliSecondsSinceLastPulse">The amount of time elapsed since the last data pulse from the encoder in milliseconds.</param>
protected override void OnPulse(bool blnUp, int milliSecondsSinceLastPulse)
{
// call the OnPulse method on the base class to ensure the pulsecount is kept up to date
base.OnPulse(blnUp, milliSecondsSinceLastPulse);
// calculate how much to change the value by
dynamic valueChange = (blnUp ? (dynamic)_pulseIncrement : -_pulseIncrement) * Acceleration(milliSecondsSinceLastPulse);
// set the value to the new value clamped by the maximum and minumum of the range.
Value = Math.Max(Math.Min(Value + valueChange, _rangeMax), _rangeMin);
// fire an event if an event handler has been attached
ValueChanged?.Invoke(this, new RotaryEncoderEventArgs(Value));
}
}
}
# FM Radio Tuner Control
This sample shows how to implement an FM radio tuner control using the `ScaledQuadradureEncoder` binding. It simply prints the tuner value to the console as the knob is turned.
![rotary example](RotaryEncoder.Sample_bb.png)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Device.Gpio;
using Iot.Device.RotaryEncoder;
Console.WriteLine("Tune your radio station with RotaryEncoder and press a key to exit");
using (GpioController controller = new GpioController())
{
// create a RotaryEncoder that represents an FM Radio tuning dial with a range of 88 -> 108 MHz
ScaledQuadratureEncoder encoder = new ScaledQuadratureEncoder(pinA: 5, pinB: 6, PinEventTypes.Falling, pulsesPerRotation: 20, pulseIncrement: 0.1, rangeMin: 88.0, rangeMax: 108.0) { Value = 88 };
// 2 milliseconds debonce time
encoder.Debounce = TimeSpan.FromMilliseconds(2);
// Register to Value change events
encoder.ValueChanged += (o, e) =>
{
Console.WriteLine($"Tuned to {e.Value}MHz");
};
while (!Console.KeyAvailable)
{
System.Threading.Tasks.Task.Delay(100).Wait();
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../RotaryEncoder.csproj" />
<ProjectReference Include="$(MainLibraryPath)System.Device.Gpio.csproj">
<AdditionalProperties>$(AdditionalProperties);RuntimeIdentifier=linux</AdditionalProperties>
</ProjectReference>
</ItemGroup>
</Project>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册