未验证 提交 55ce2fdb 编写于 作者: H HumphreyJ 提交者: GitHub

Add key matrix (restart #522) (#803)

Add Key Matrix
ReadKey and Event based
上级 e94cb1a5
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Device.Gpio;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Threading;
using System.Threading.Tasks;
namespace Iot.Device.KeyMatrix
{
/// <summary>
/// GPIO key matrix Driver
/// </summary>
public class KeyMatrix : IDisposable
{
/// <summary>
/// Get output pins
/// </summary>
public IEnumerable<int> OutputPins => _outputPins;
/// <summary>
/// Get input pins
/// </summary>
public IEnumerable<int> InputPins => _inputPins;
/// <summary>
/// Get all buttons' values
/// </summary>
public ReadOnlySpan<PinValue> Values => _buttonValues.AsSpan();
/// <summary>
/// Get or set interval in milliseconds
/// </summary>
public TimeSpan ScanInterval { get; set; }
/// <summary>
/// Get buttons' values by output
/// </summary>
/// <param name="output">Output index</param>
public ReadOnlySpan<PinValue> this[int output] => _buttonValues.AsSpan(output * _outputPins.Length, _inputPins.Length);
private int[] _outputPins;
private int[] _inputPins;
private GpioController? _gpioController;
private PinValue[] _buttonValues;
private bool _pinsOpened;
private int _currentOutput = 0;
private bool _shouldDispose;
private bool _isRunning = false;
private KeyMatrixEvent? _lastKeyEvent;
/// <summary>
/// Fire an event when a key is pressed or released
/// </summary>
/// <param name="sender">The sender KeyMatrix</param>
/// <param name="keyMatrixEvent">The key event</param>
public delegate void KeyEventHandler(object sender, KeyMatrixEvent keyMatrixEvent);
/// <summary>
/// The raised event
/// </summary>
public event KeyEventHandler? KeyEvent;
/// <summary>
/// Initialize key matrix
/// </summary>
/// <param name="outputPins">Output pins</param>
/// <param name="inputPins">Input pins</param>
/// <param name="scanInterval">Scanning interval in milliseconds</param>
/// <param name="gpioController">GPIO controller</param>
/// <param name="shouldDispose">True to dispose the GpioController</param>
public KeyMatrix(IEnumerable<int> outputPins, IEnumerable<int> inputPins, TimeSpan scanInterval, GpioController? gpioController = null, bool shouldDispose = true)
{
_shouldDispose = shouldDispose || gpioController == null;
_gpioController = gpioController ?? new();
if (outputPins == null)
{
throw new ArgumentNullException(nameof(outputPins));
}
if (!outputPins.Any())
{
throw new ArgumentOutOfRangeException(nameof(outputPins), "The number of outputs must be at least 1");
}
if (inputPins == null)
{
throw new ArgumentNullException(nameof(inputPins));
}
if (!inputPins.Any())
{
throw new ArgumentOutOfRangeException(nameof(inputPins), "The number of inputs must be at least 1");
}
_outputPins = outputPins.ToArray();
_inputPins = inputPins.ToArray();
_buttonValues = new PinValue[_outputPins.Length * _inputPins.Length];
for (int i = 0; i < _buttonValues.Length; i++)
{
_buttonValues[i] = PinValue.Low;
}
_pinsOpened = false;
ScanInterval = scanInterval;
OpenPins();
}
/// <summary>
/// Start listening to key events
/// </summary>
public void StartListeningKeyEvent()
{
if (_isRunning)
{
return;
}
_isRunning = true;
new Thread(() =>
{
LoopReadKey();
}).Start();
}
/// <summary>
/// Stop listening to key events
/// </summary>
public void StopListeningKeyEvent()
{
_isRunning = false;
}
private void LoopReadKey()
{
do
{
Thread.Sleep(ScanInterval);
_currentOutput = (_currentOutput + 1) % _outputPins.Length;
_gpioController!.Write(_outputPins[_currentOutput], PinValue.High);
for (var i = 0; i < _inputPins.Length; i++)
{
int index = _currentOutput * _inputPins.Length + i;
PinValue oldValue = _buttonValues[index];
PinValue newValue = _gpioController.Read(_inputPins[i]);
_buttonValues[index] = newValue;
if (newValue != oldValue)
{
KeyEvent?.Invoke(this, new KeyMatrixEvent(newValue == PinValue.High ? PinEventTypes.Rising : PinEventTypes.Falling, _currentOutput, i));
}
}
_gpioController.Write(_outputPins[_currentOutput], PinValue.Low);
}
while (_pinsOpened && _isRunning);
}
/// <summary>
/// Blocks execution until a key event is received
/// </summary>
public KeyMatrixEvent? ReadKey()
{
_currentOutput--;
KeyEvent += KeyMatrixKeyEvent;
_isRunning = true;
LoopReadKey();
KeyEvent -= KeyMatrixKeyEvent;
return _lastKeyEvent;
}
private void KeyMatrixKeyEvent(object sender, KeyMatrixEvent keyMatrixEvent)
{
_isRunning = false;
_lastKeyEvent = keyMatrixEvent;
}
/// <inheritdoc/>
public void Dispose()
{
ClosePins();
if (_shouldDispose)
{
_gpioController?.Dispose();
_gpioController = null;
}
else
{
if (_gpioController is object && _pinsOpened)
{
ClosePins();
}
}
}
private void OpenPins()
{
for (int i = 0; i < _outputPins.Length; i++)
{
_gpioController!.OpenPin(_outputPins[i], PinMode.Output);
}
for (int i = 0; i < _inputPins.Length; i++)
{
_gpioController!.OpenPin(_inputPins[i], PinMode.Input);
}
_pinsOpened = true;
}
private void ClosePins()
{
_isRunning = false;
_pinsOpened = false;
Thread.Sleep(ScanInterval); // wait for current scan to complete
for (int i = 0; i < _outputPins.Length; i++)
{
_gpioController!.ClosePin(_outputPins[i]);
}
for (int i = 0; i < _inputPins.Length; i++)
{
_gpioController!.ClosePin(_inputPins[i]);
}
}
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;netcoreapp2.1</TargetFrameworks>
<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(MainLibraryPath)System.Device.Gpio.csproj" />
<Compile Include="KeyMatrix.cs" />
<Compile Include="KeyMatrixEvent.cs" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" />
</ItemGroup>
</Project>

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30804.86
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeyMatrix", "KeyMatrix.csproj", "{1C93B28A-C12A-497C-8D3C-4AAA2267B539}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeyMatrix.Sample", "samples\KeyMatrix.Sample.csproj", "{69C4AE8B-8D19-472B-81F9-65C0B80AC6A5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1C93B28A-C12A-497C-8D3C-4AAA2267B539}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1C93B28A-C12A-497C-8D3C-4AAA2267B539}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C93B28A-C12A-497C-8D3C-4AAA2267B539}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C93B28A-C12A-497C-8D3C-4AAA2267B539}.Release|Any CPU.Build.0 = Release|Any CPU
{69C4AE8B-8D19-472B-81F9-65C0B80AC6A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69C4AE8B-8D19-472B-81F9-65C0B80AC6A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69C4AE8B-8D19-472B-81F9-65C0B80AC6A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69C4AE8B-8D19-472B-81F9-65C0B80AC6A5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2F63D969-8AEF-4EFF-9FE3-AB23A67A7439}
EndGlobalSection
EndGlobal
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Device.Gpio;
namespace Iot.Device.KeyMatrix
{
/// <summary>
/// Keyboard event
/// </summary>
public class KeyMatrixEvent
{
/// <summary>
/// Event type of current button. PinEventTypes.Rising is pressed,PinEventTypes.Falling is released
/// </summary>
public PinEventTypes EventType { get; internal set; }
/// <summary>
/// Current button's output index
/// </summary>
public int Output { get; internal set; }
/// <summary>
/// Current button's input index
/// </summary>
public int Input { get; internal set; }
internal KeyMatrixEvent(PinEventTypes eventType, int output, int input)
{
EventType = eventType;
Output = output;
Input = input;
}
}
}
# Key Matrix
An M×N key matrix driver.
(M is number of output pins and N is number of input pins.)
## Summary
These key matrices look like this:
![4x4-Keypad](https://www.waveshare.com/img/devkit/accBoard/4x4-Keypad/4x4-Keypad-1.jpg)
This is a 4×4 matrix. And [here is the schematic](https://www.waveshare.com/w/upload/e/ea/4x4-Keypad-Schematic.pdf)
You can connect any M×N key matrix, theoretically, by using M+N GPIO pins.
You can also use any compatible GPIO controller like [Mcp23xxx](../Mcp23xxx) instead of native controller.
## Usage
You need to create 2 lists of int, one for the input and one for the output.
```csharp
IEnumerable<int> outputs = new int[] { 26, 19, 13, 6 };
IEnumerable<int> inputs = new int[] { 21, 20, 16, 12 };
KeyMatrix mk = new KeyMatrix(outputs, inputs, TimeSpan.FromMilliseconds(20));
```
You can use as well any GpioController like the MCP23017 in the following example
```csharp
var settings = new System.Device.I2c.I2cConnectionSettings(1, 0x20);
var i2cDevice = System.Device.I2c.I2cDevice.Create(settings);
var mcp23017 = new Iot.Device.Mcp23xxx.Mcp23017(i2cDevice);
GpioController gpio = new GpioController(PinNumberingScheme.Logical, mcp23017);
IEnumerable<int> outputs = new int[] { 26, 19, 13, 6 };
IEnumerable<int> inputs = new int[] { 21, 20, 16, 12 };
KeyMatrix mk = new KeyMatrix(outputs, inputs, TimeSpan.FromMilliseconds(20), gpio, true);
```
## Read on Key
To read a key, just the `ReadKey` function:
```csharp
KeyMatrixEvent? key = mk.ReadKey();
```
KeyMatrixEvent contains the event that happened. Please note that ReadKey is blocked up to the moment an event is detected.
## Event based approach
`KeyMatrix` supports events. Just subscribe to the event and have a function to handle the events:
```csharp
Console.WriteLine("This will now start listening to events and display them. Press a key to finish.");
mk.KeyEvent += KeyMatrixEventReceived;
mk.StartListeningKeyEvent();
while (!Console.KeyAvailable)
{
Thread.Sleep(1);
}
mk.StopListeningKeyEvent();
void KeyMatrixEventReceived(object sender, KeyMatrixEvent keyMatrixEvent)
{
// Do something here, you have an event!
}
```
## Tips and tricks
- Using diodes(eg. 1N4148) for each button prevents "ghosting" or "masking" problem.
- Input pins need pull-down resistors connect to ground if your MCU doesn't have it. So you need to have a pull-down on a Raspberry Pi for example.
- If your key matrix doesn't work well, try to swap output and input pins. Some includes diodes and if they are used the reverse way won't work properly.
## References
- http://pcbheaven.com/wikipages/How_Key_Matrices_Works/
- https://www.waveshare.com/wiki/4x4_Keypad
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\KeyMatrix.csproj" />
<ProjectReference Include="$(MainLibraryPath)System.Device.Gpio.csproj" />
</ItemGroup>
</Project>
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Device.Gpio;
using System.Linq;
using System.Threading;
using Iot.Device.KeyMatrix;
Console.WriteLine("Please enter your output pins separated by a coma. For example: 27,22,25,6");
var line = Console.ReadLine();
IEnumerable<int> outputs = string.IsNullOrEmpty(line) ? new int[] { 27, 22, 25, 6 } : line.Split(',').Select(m => int.Parse(m));
Console.WriteLine("Please enter your input pins separated by a coma. For example: 17,23,24,5");
line = Console.ReadLine();
IEnumerable<int> inputs = string.IsNullOrEmpty(line) ? new int[] { 17, 23, 24, 5 } : line.Split(',').Select(m => int.Parse(m));
Console.WriteLine("Please enter the scanning interval in milliseconds. For example: 15");
line = Console.ReadLine();
int interval = int.TryParse(line, out int i) ? i : 15;
Console.WriteLine("Please enter the number of keys you want to read individually events. For example: 20");
line = Console.ReadLine();
int count = int.TryParse(line, out int c) ? c : 20;
// initialize keyboard
KeyMatrix mk = new KeyMatrix(outputs, inputs, TimeSpan.FromMilliseconds(interval));
// define the cancellation token.
CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;
// read key events
for (int n = 0; n < count; n++)
{
Console.WriteLine($"Waiting for matrix keyboard event... {n}/{count}");
KeyMatrixEvent? key = mk.ReadKey();
if (key is not object)
{
Console.WriteLine("No key pressed");
continue;
}
ShowKeyMatrixEvent(mk, key);
}
Console.WriteLine("This will now start listening to events and display them. Press a key to finish.");
mk.KeyEvent += KeyMatrixEventReceived;
mk.StartListeningKeyEvent();
while (!Console.KeyAvailable)
{
Thread.Sleep(1);
}
mk.StopListeningKeyEvent();
// dispose
Console.WriteLine("Dispose after 2 seconds...");
Thread.Sleep(2000);
mk.Dispose();
void KeyMatrixEventReceived(object sender, KeyMatrixEvent keyMatrixEvent)
{
ShowKeyMatrixEvent((KeyMatrix)sender, keyMatrixEvent);
}
void ShowKeyMatrixEvent(KeyMatrix sender, KeyMatrixEvent pinValueChangedEventArgs)
{
Console.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss.fff} {pinValueChangedEventArgs.Output}, {pinValueChangedEventArgs.Input}, {pinValueChangedEventArgs.EventType}");
Console.WriteLine();
// print keyboard status
for (int r = 0; r < sender.OutputPins.Count(); r++)
{
ReadOnlySpan<PinValue> rv = sender[r];
for (int c = 0; c < sender.InputPins.Count(); c++)
{
Console.Write(rv[c] == PinValue.Low ? " ." : " #");
}
Console.WriteLine();
}
}
# Key Matrix Samples
This shows how to connect the matrix.
**Important**: Please make you don't forget to place a pull down on the input matrix.
## Connection using on-board GPIO
![Connection using Raspberry Pi](4x4kb.png)
## Connection using MCP23017
![Connection using a MCP23017](4x4kb_via_mcp23017.png)
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册