// 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.Collections.Generic; using System.Device.Gpio; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; namespace Iot.Device.Board { /// /// A GPIO Driver for testing on Windows /// This driver uses the keyboard for simulating GPIO pins. /// Pins 0-2 are output only and represent the keyboard LEDs (Caps lock, Scroll Lock and Num Lock). /// Setting a value to any of these pins toggles the LEDs on the keyboard (if they're physically present). /// Pins above 8 are input only and represent the keyboard keys. To get the pin number, /// cast the corresponding to int, e.g. int pinNumber = (int)ConsoleKey.A /// public class KeyboardGpioDriver : GpioDriver { private enum LedKey { NumLock, CapsLock, ScrollLock, } private const int SupportedPinCount = 256; private readonly Dictionary _pinValues = new Dictionary(); private KeyState[] _state; private Thread? _pollThread; private bool _terminateThread; /// /// Creates an instance of the KeyboardGpioDriver /// public KeyboardGpioDriver() { _state = new KeyState[SupportedPinCount]; for (int i = 0; i < SupportedPinCount; i++) { _state[i] = new KeyState((ConsoleKey)i, i); } _pollThread = null; _terminateThread = true; foreach (var key in Enum.GetValues(typeof(LedKey))) { _pinValues.Add((int)key!, GetLedState((LedKey)key).KeyValue); } } /// protected override int PinCount { get { // The ConsoleKey enum is used to index into our pins, if needed. This one does not use values below 8, so // we'll use 3 for the LEDs. return SupportedPinCount; } } /// protected override int ConvertPinNumberToLogicalNumberingScheme(int pinNumber) { return pinNumber; } /// protected override void OpenPin(int pinNumber) { } /// protected override void ClosePin(int pinNumber) { } /// protected override void SetPinMode(int pinNumber, PinMode mode) { if (IsPinModeSupported(pinNumber, mode)) { _state[pinNumber].Mode = mode; } else { throw new NotSupportedException($"Pin {pinNumber} does not support mode {mode}"); } } /// protected override PinMode GetPinMode(int pinNumber) { return _state[pinNumber].Mode; } /// protected override bool IsPinModeSupported(int pinNumber, PinMode mode) { if (pinNumber < 3) { // Output-only pins (the three LEDs on the keyboard) if (mode == PinMode.Output) { return true; } return false; } if (pinNumber >= 8) { if (mode == PinMode.Input || mode == PinMode.InputPullDown || mode == PinMode.InputPullUp) { return true; } } return false; } private bool IsKeyPressed(ConsoleKey key) { short state = Interop.GetKeyState((int)key); return (state & 0xFFFE) != 0; // any bits except the lowest } private void SetLedState(LedKey key, PinValue state) { (int virtualKey, int currentKeyState) = GetLedState(key); _pinValues[(int)key] = state; if ((state == PinValue.High && currentKeyState == 0) || (state == PinValue.Low && currentKeyState != 0)) { // Simulate a key press Interop.keybd_event((byte)virtualKey, 0x45, Interop.KEYEVENTF_EXTENDEDKEY, IntPtr.Zero); // Simulate a key release Interop.keybd_event((byte)virtualKey, 0x45, Interop.KEYEVENTF_EXTENDEDKEY | Interop.KEYEVENTF_KEYUP, IntPtr.Zero); } } private (int VirtualKey, int KeyValue) GetLedState(LedKey key) { int virtualKey = 0; if (key == LedKey.NumLock) { virtualKey = Interop.VK_NUMLOCK; } else if (key == LedKey.CapsLock) { virtualKey = Interop.VK_CAPITAL; } else if (key == LedKey.ScrollLock) { virtualKey = Interop.VK_SCROLL; } else { throw new NotSupportedException("No such key"); } // Bit 1 indicates whether the LED is currently on or off (or, whether Scroll lock, num lock, caps lock is on) return (virtualKey, Interop.GetKeyState(virtualKey) & 1); } /// protected override PinValue Read(int pinNumber) { short currentKeyState = Interop.GetAsyncKeyState(pinNumber); if ((currentKeyState & 0xFFFE) != 0) { return PinValue.High; } else { return PinValue.Low; } } /// protected override void Toggle(int pinNumber) => Write(pinNumber, !_pinValues[pinNumber]); /// protected override void Write(int pinNumber, PinValue value) { if (pinNumber == 0) { SetLedState(LedKey.NumLock, value); } if (pinNumber == 1) { SetLedState(LedKey.ScrollLock, value); } if (pinNumber == 2) { SetLedState(LedKey.CapsLock, value); } } /// protected override WaitForEventResult WaitForEvent(int pinNumber, PinEventTypes eventTypes, CancellationToken cancellationToken) { PinValue oldState = Read(pinNumber); while (!cancellationToken.IsCancellationRequested) { PinValue newState = Read(pinNumber); if (oldState != newState) { if (eventTypes == PinEventTypes.Rising && newState == PinValue.High) { return new WaitForEventResult() { EventTypes = PinEventTypes.Rising, TimedOut = false }; } else if (eventTypes == PinEventTypes.Falling && newState == PinValue.Low) { return new WaitForEventResult() { EventTypes = PinEventTypes.Falling, TimedOut = false }; } else { return new WaitForEventResult() { EventTypes = newState == PinValue.High ? PinEventTypes.Rising : PinEventTypes.Falling, TimedOut = false }; } } } return new WaitForEventResult() { TimedOut = true }; } /// protected override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventTypes, PinChangeEventHandler callback) { lock (_state) { if (_pollThread == null) { _terminateThread = false; _pollThread = new Thread(PollingKeyThread); _pollThread.IsBackground = true; _pollThread.Start(); } _state[pinNumber].State = Read(pinNumber); _state[pinNumber].EventModes = _state[pinNumber].EventModes | eventTypes; _state[pinNumber].Callback += callback; } } /// protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) { bool terminate; lock (_state) { _state[pinNumber].Callback -= callback; if (_state[pinNumber].CallbacksExist == false) { _state[pinNumber].EventModes = PinEventTypes.None; } terminate = _state.All(x => x.CallbacksExist == false); } // Can't do this within the lock - would risk a deadlock if (terminate && _pollThread != null) { _terminateThread = true; _pollThread.Join(); _pollThread = null; } } /// /// Poor man's interrupt handling. This class is not for real production use, so doesn't really matter /// private void PollingKeyThread() { while (!_terminateThread) { lock (_state) { foreach (var s in _state) { if (s.EventModes != PinEventTypes.None) { var newState = Read(s.PinNumber); if (s.State != newState) { s.State = newState; // Fire either way - the client has to handle that anyway (because other clients may request the other edge) s.FireCallback(this, new PinValueChangedEventArgs(newState == PinValue.High ? PinEventTypes.Rising : PinEventTypes.Falling, s.PinNumber)); } } } } Thread.Sleep(10); } } private sealed class KeyState { public KeyState(ConsoleKey key, int pinNumber) { Key = key; PinNumber = pinNumber; State = PinValue.Low; } public event PinChangeEventHandler? Callback; public ConsoleKey Key { get; } public int PinNumber { get; } public PinMode Mode { get; set; } public PinValue State { get; set; } public PinEventTypes EventModes { get; set; } public bool CallbacksExist { get { return Callback != null; } } public void FireCallback(object sender, PinValueChangedEventArgs pinValueChangedEventArgs) { Callback?.Invoke(sender, pinValueChangedEventArgs); } } } }