電車でGO!新幹線専用コントローラーをパソコンにつなぐ（ライブラリ編）

前回の記事「[電車でGO!新幹線専用コントローラーをパソコンにつなぐ（通信フォーマット編）](https://qiita.com/admiralhetare/items/ab9690551d40b86c0143)」がデバイス固有の大事なところのすべてですが、C#で使いやすいようにするクラスを書いてみたので、載せておきます。
# 簡単な使い方
時速100キロで走行中の表示をさせる例

```csharp:Program.cs
using TAITO_DENSYA_CON_T02_ctrl;
namespace TAITO_DENSYA_CON_console
{
    public class TAITO_DENSYA_console
    {
        public static void Main()
        {
            using (TAITO_DENSYA_CON_T02_USBctrl ctrl = new())
            {
                ctrl.ATCSpeed = 100; // ATC100 km/h
                ctrl.Speed = 100;    // 100 km/h
                ctrl.DoorLamp = true;// 戸閉灯点灯
                Task.Run(() => ctrl.Execute());// 上記の設定を反映
                Console.ReadKey();
            }
        }
    }
}
```

# というわけで以下ソース

```csharp:TAITO_DENSYA_CON_T02_ctrl.cs
// 電車でGO!新幹線専用コントローラー用ライブラリのようなもの
// 2021.11.21
// v0.1
// Copyright と∃ しまみじめ(@admiralhetare)
// 本コードは自己責任にてご自由にお使いください。無保証です。
//
// 更新履歴
// 2021.11.21 v0.1 とりあえず書けた

using System;
using LibUsbDotNet;
using LibUsbDotNet.Main;

namespace TAITO_DENSYA_CON_T02_ctrl
{
    public enum SpeedDispModes
    {
        On,
        Off,
        /// <summary>
        /// ---
        /// </summary>
        Hyphen,
        /// <summary>
        /// ‾‾‾
        /// </summary>
        Overbar,
    }
    /// <summary>
    /// ReaderWriterLockSlimをusingで使えるようにしたもの
    /// </summary>
    public struct WriteLock : IDisposable
    {
        ReaderWriterLockSlim obj;
        public WriteLock(ReaderWriterLockSlim lockObj)
        {
            obj = lockObj;
            obj.EnterWriteLock();
        }
        public void Dispose()
        {
            obj.ExitWriteLock();
        }
    }
    /// <summary>
    /// ReaderWriterLockSlimをusingで使えるようにしたもの
    /// </summary>
    public struct ReadLock : IDisposable
    {
        ReaderWriterLockSlim obj;
        public ReadLock(ReaderWriterLockSlim lockObj)
        {
            obj = lockObj;
            obj.EnterReadLock();
        }
        public void Dispose()
        {
            obj.ExitReadLock();
        }
    }
    /// <summary>
    /// ReaderWriterLockSlimをusingで使えるようにしたもの
    /// </summary>
    public struct UpgradeableReadLock : IDisposable
    {
        ReaderWriterLockSlim obj;
        public UpgradeableReadLock(ReaderWriterLockSlim lockObj)
        {
            obj = lockObj;
            obj.EnterUpgradeableReadLock();
        }
        public void Dispose()
        {
            obj.ExitUpgradeableReadLock();
        }
    }
    /// <summary>
    /// 電車でGO!新幹線専用コントローラーを制御するクラス
    /// </summary>
    public class TAITO_DENSYA_CON_T02_USBctrl : IDisposable
    {
        private ReaderWriterLockSlim LockSlim = new ReaderWriterLockSlim();
        private UsbDevice usbDevice;
        private const int vid = 0x0AE4;
        private const int pid = 0x0005;
        private static UsbDeviceFinder MyUsbFinder = new(vid, pid);
        /// <summary>
        /// コンストラクタ。デバイスが見つからない等の場合は例外を吐く
        /// </summary>
        /// <exception cref="Exception">Device Not Found.</exception>
        public TAITO_DENSYA_CON_T02_USBctrl()
        {
            // USBデバイス接続
            try
            {
                using (var wl = new WriteLock(LockSlim))
                    usbDevice = UsbDevice.OpenUsbDevice(MyUsbFinder);
                if (usbDevice == null) throw new Exception("Device Not Found.");
                IUsbDevice? wholeUsbDevice = usbDevice as IUsbDevice;
                if (!ReferenceEquals(wholeUsbDevice, null))
                {
                    wholeUsbDevice.SetConfiguration(1);
                    wholeUsbDevice.ClaimInterface(0);
                }
            }
            catch
            {
                throw;
            }

            // 初期値代入
            VibrationBrake = false;
            VibrationMascon = false;
            DoorLamp = false;
            ATCBar = ATCBarMin;
            SpeedBar = SpeedBarMin;
            Speed = SpeedMin;
            ATCSpeed = ATCSpeedMin;

            // イベントリスナー
            USBDataReceivedEvent += SetInputsFromUSBData;

            // 問答無用でデータ受信開始
            BeginReceiveData();
        }
        /// <summary>
        /// コントロール転送で送るデータ
        /// </summary>
        /// VibrationBrake,VibrationMascon,DoorLamp ATCBar,SpeedBar,Speed,,ATCSpeed,
        private byte[] _USBdata = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
        /// <summary>
        /// ブレーキハンドルを振動させるかどうか
        /// </summary>
        public bool VibrationBrake
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return (_USBdata[0] & 0x01) == 0x01;
            }
            set
            {
                using var wl = new WriteLock(LockSlim);
                _USBdata[0] = value ? (byte)0x01 : (byte)0x00;
            }
        }
        /// <summary>
        /// マスコンハンドルを振動させるかどうか
        /// </summary>
        public bool VibrationMascon
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return (_USBdata[1] & 0x01) == 0x01;
            }
            set
            {
                using var wl = new WriteLock(LockSlim);
                _USBdata[1] = value ? (byte)0x01 : (byte)0x00;
            }
        }
        /// <summary>
        /// 戸閉灯を点灯させるかどうか
        /// </summary>
        public bool DoorLamp
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return (_USBdata[2] & 0x80) == 0x80;
            }
            set
            {
                using var wl = new WriteLock(LockSlim);
                _USBdata[2] = value ? (byte)(_USBdata[2] | 0x80) : (byte)(_USBdata[2] & ~0x80);
            }
        }
        private int _SpeedBar = SpeedBarMin;
        /// <summary>
        /// スピードバーグラフ
        /// </summary>
        public int SpeedBar
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _SpeedBar;
            }
            set
            {
                using var wl = new WriteLock(LockSlim);
                _SpeedBar = Clamp(value, SpeedBarMin, SpeedBarMax);
                _USBdata[3] = (byte)_SpeedBar;
            }
        }
        public const int SpeedBarMin = 0;
        public const int SpeedBarMax = 0x16;
        public const int SpeedBarMinSpeed = 0;
        public const int SpeedBarMaxSpeed = 350;
        private bool _SpeedBarAuto = true;
        /// <summary>
        /// スピードとスピードバーグラフを連動させるかどうか
        /// </summary>
        public bool SpeedBarAuto
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _SpeedBarAuto;
            }
            set
            {
                using var wl = new WriteLock(LockSlim);
                _SpeedBarAuto = value;
            }
        }
        /// <summary>
        /// フラグが立っていればSpeedBarをアップデート
        /// </summary>
        private void UpdateSpeedBarAuto()
        {
            if (SpeedBarAuto)
            { // 最後の1マスはバーグラフ点灯しないので+1する。
                double doubleSpeedBar = (SpeedBarMax + 1.0) * Speed / SpeedBarMaxSpeed;
                SpeedBar = (int)Math.Round(doubleSpeedBar);
            }
        }
        /// <summary>
        /// 2byteのBCDに変換する。リトルエンディアン的な感じ
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private static (byte, byte) ConvertToBCD(int value)
        {
            byte Lower8bit = (byte)(((value / 10 % 10) << 4) + value / 1 % 10);
            byte Upper8bit = (byte)(((value / 1000 % 10) << 4) + value / 100 % 10);
            return (Lower8bit, Upper8bit);
        }
        private int _Speed = SpeedMin;
        /// <summary>
        /// スピード
        /// </summary>
        public int Speed
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _Speed;
            }
            set
            {
                using (var wl = new WriteLock(LockSlim))
                    _Speed = Clamp(value, SpeedMin, SpeedMax);
                SetSpeed();
                UpdateATCBarAuto();
                UpdateSpeedBarAuto();
            }
        }
        public const int SpeedMin = 0;
        public const int SpeedMax = 999;
        private void SetSpeed()
        {
            switch (SpeedDispMode)
            {
                case SpeedDispModes.On:
                    SetSpeed(ConvertToBCD(_Speed));
                    break;
                case SpeedDispModes.Off:
                    SetSpeed(_SpeedOff);
                    break;
                case SpeedDispModes.Hyphen:
                    SetSpeed(_SpeedHyphen);
                    break;
                case SpeedDispModes.Overbar:
                    SetSpeed(_SpeedOverbar);
                    break;
                default:
                    SetSpeed(ConvertToBCD(_Speed));
                    break;
            }
        }
        private void SetSpeed((byte, byte) Speed)
        {
            using var wl = new WriteLock(LockSlim);
            (_USBdata[4], _USBdata[5]) = Speed;
        }
        private static readonly (byte, byte) _SpeedOff = (0xaa, 0x0a);
        private static readonly (byte, byte) _SpeedHyphen = (0xbb, 0x0b);
        private static readonly (byte, byte) _SpeedOverbar = (0xdd, 0x0d);
        private SpeedDispModes _SpeedDispMode = SpeedDispModes.On;
        /// <summary>
        /// スピード表示の表示モード
        /// </summary>
        public SpeedDispModes SpeedDispMode
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _SpeedDispMode;
            }
            set
            {
                using (var wl = new WriteLock(LockSlim))
                    _SpeedDispMode = value;
                SetSpeed();
            }
        }
        private int _ATCSpeed = ATCSpeedMin;
        /// <summary>
        /// ATC指示速度
        /// </summary>
        public int ATCSpeed
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _ATCSpeed;
            }
            set
            {
                using (var wl = new WriteLock(LockSlim))
                    _ATCSpeed = Clamp(value, ATCSpeedMin, ATCSpeedMax);
                SetATCSpeed();
                UpdateATCBarAuto();
            }
        }
        private void SetATCSpeed((byte, byte) Speed)
        {
            using var wl = new WriteLock(LockSlim);
            (_USBdata[6], _USBdata[7]) = Speed;
        }
        private void SetATCSpeed()
        {
            switch (ATCSpeedDispMode)
            {
                case SpeedDispModes.On:
                    SetATCSpeed(ConvertToBCD(_ATCSpeed));
                    break;
                case SpeedDispModes.Off:
                    SetATCSpeed(_SpeedOff);
                    break;
                case SpeedDispModes.Hyphen:
                    SetATCSpeed(_SpeedHyphen);
                    break;
                case SpeedDispModes.Overbar:
                    SetATCSpeed(_SpeedOverbar);
                    break;
                default:
                    SetATCSpeed(ConvertToBCD(_ATCSpeed));
                    break;
            }
        }
        private SpeedDispModes _ATCSpeedDispMode = SpeedDispModes.On;
        /// <summary>
        /// ATC指示速度表示の表示モード
        /// </summary>
        public SpeedDispModes ATCSpeedDispMode
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _ATCSpeedDispMode;
            }
            set
            {
                using (var wl = new WriteLock(LockSlim))
                    _ATCSpeedDispMode = value;
                SetATCSpeed();
            }
        }

        /// <summary>
        /// フラグが立っていればATCBarをアップデート
        /// </summary>
        private void UpdateATCBarAuto()
        {
            if (ATCBarAuto)
                ATCBar = ATCBarMax - ATCSpeed + Speed;
        }
        public const int ATCSpeedMin = 0;
        public const int ATCSpeedMax = 999;

        private int _ATCBar = ATCBarMin;
        /// <summary>
        /// ATC指示速度と現在速度の差を表示するバーグラフ
        /// </summary>
        public int ATCBar
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _ATCBar;
            }
            set
            {
                using var wl = new WriteLock(LockSlim);
                _ATCBar = Clamp(value, ATCBarMin, ATCBarMax);
                _USBdata[2] = (byte)((_USBdata[2] & 0xF0) + (_ATCBar & 0x0F));
            }
        }
        public const int ATCBarMin = 0;
        public const int ATCBarMax = 10;
        private bool _ATCBarAuto = true;
        /// <summary>
        /// ATCバーグラフを速度とATC指示速度に連動させるかどうか
        /// </summary>
        public bool ATCBarAuto
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _ATCBarAuto;
            }
            set
            {
                using var wl = new WriteLock(LockSlim);
                _ATCBarAuto = value;
            }
        }
        /// <summary>
        /// 最大値と最小値でクランプ
        /// </summary>
        /// <param name="value"></param>
        /// <param name="min"></param>
        /// <param name="max"></param>
        /// <returns></returns>
        private static int Clamp(int value, int min, int max)
        {
            if (value < min)
                return min;
            if (value > max)
                return max;
            return value;
        }
        /// <summary>
        /// USBポートからデータ送信実行
        /// </summary>
        /// <exception cref="Exception">USB device is closed</exception>
        /// <exception cref="Exception">ControlTransfer Error</exception>
        public void Execute()
        {
            int transfered = 0;
            var usp = new UsbSetupPacket(0x40, 0x09, 0x0301, 0x0000, _USBdata.Length);
            bool res = false;
            using (var rl = new ReadLock(LockSlim))
            {
                if (!usbDevice.IsOpen)
                {
                    throw new Exception("USB device is closed");
                }
                res = usbDevice.ControlTransfer(ref usp, _USBdata, _USBdata.Length, out transfered);
            }
            if (!res)
                throw new Exception("ControlTransfer Error");
        }

        private delegate void USBDataReceiveCallback(byte[] ReceivedData);
        private event USBDataReceiveCallback USBDataReceivedEvent;
        /// <summary>
        /// データを受信し続けるメソッドキャンセルトークンは2秒以内で反応する？
        /// </summary>
        /// <param name="token"></param>
        /// <exception cref="Exception">なんかエラー</exception>
        private void ReceiveData(CancellationToken token)
        {
            while (usbDevice.IsOpen)
            {
                if (token.IsCancellationRequested) return;
                byte[] buf = new byte[6];
                int bytesRead = 0;

                using (var Reader = usbDevice.OpenEndpointReader(ReadEndpointID.Ep01))
                {
                    ErrorCode ec = ErrorCode.None;
                    try
                    {
                        ec = Reader.Read(buf, 2000, out bytesRead);
                    }
                    catch (Exception)
                    {
                        ec = ErrorCode.ReadFailed;
                    }

                    if (ec == ErrorCode.IoTimedOut) continue;
                    if (ec == ErrorCode.IoCancelled) return;
                    if (ec == ErrorCode.ReadFailed) return;
                    if (ec != ErrorCode.None) throw new Exception("usbcom ReadCtrl Error : ErrorCode==" + ec.ToString() + "\n" + UsbDevice.LastErrorString);
                }

                if (bytesRead > 0)
                    Task.Run(() => USBDataReceivedEvent?.Invoke(buf));
            }
        }
        private Task? _ReceiveDataTask;
        private CancellationTokenSource? _ReceiveDataTaskCancel;
        /// <summary>
        /// データ受信開始
        /// </summary>
        public void BeginReceiveData()
        {
            if (Receiving) return;
            using var wl = new WriteLock(LockSlim);
            _ReceiveDataTaskCancel = new CancellationTokenSource();
            _ReceiveDataTask = Task.Run(() => ReceiveData(_ReceiveDataTaskCancel.Token));
        }
        /// <summary>
        /// データ受信中かどうか
        /// </summary>
        public bool Receiving
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                if (_ReceiveDataTask == null) return false;
                return _ReceiveDataTask.Status == TaskStatus.Running;
            }
        }
        /// <summary>
        /// データ受信終了
        /// </summary>
        public void EndReceiveData()
        {
            if (!Receiving) return;
            using var wl = new WriteLock(LockSlim);
            _ReceiveDataTaskCancel?.Cancel();
        }
        public const int BrakeInputMax = 8;
        public const int BrakeInputMin = 0;
        private int _BrakeInput = 0;
        /// <summary>
        /// ブレーキハンドルのポジション0～8
        /// </summary>
        public int BrakeInput
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _BrakeInput;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _BrakeInput = value;
            }
        }
        public const int MasconInputMax = 13;
        public const int MasconInputMin = 0;
        private int _MasconInput = 0;
        /// <summary>
        /// マスコンハンドルのポジション0～13
        /// </summary>
        public int MasconInput
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _MasconInput;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _MasconInput = value;
            }
        }
        private bool _KeyUP = false;
        /// <summary>
        /// ↑キー
        /// </summary>
        public bool KeyUP
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _KeyUP;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _KeyUP = value;
            }
        }
        private bool _KeyRight = false;
        /// <summary>
        /// →キー
        /// </summary>
        public bool KeyRight
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _KeyRight;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _KeyRight = value;
            }
        }
        private bool _KeyDown = false;
        /// <summary>
        /// ↓キー
        /// </summary>
        public bool KeyDown
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _KeyDown;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _KeyDown = value;
            }
        }
        private bool _KeyLeft = false;
        /// <summary>
        /// ←キー
        /// </summary>
        public bool KeyLeft
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _KeyLeft;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _KeyLeft = value;
            }
        }
        private bool _KeySelect = false;
        /// <summary>
        /// セレクトキー
        /// </summary>
        public bool KeySelect
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _KeySelect;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _KeySelect = value;
            }
        }
        private bool _KeyStart = false;
        /// <summary>
        /// スタートキー
        /// </summary>
        public bool KeyStart
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _KeyStart;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _KeyStart = value;
            }
        }
        private bool _KeyA = false;
        /// <summary>
        /// Aキー
        /// </summary>
        public bool KeyA
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _KeyA;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _KeyA = value;
            }
        }
        /// <summary>
        /// Bキー
        /// </summary>
        private bool _KeyB = false;
        public bool KeyB
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _KeyB;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _KeyB = value;
            }
        }
        private bool _KeyC = false;
        /// <summary>
        /// Cキー
        /// </summary>
        public bool KeyC
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _KeyC;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _KeyC = value;
            }
        }
        private bool _KeyD = false;
        /// <summary>
        /// Dキー
        /// </summary>
        public bool KeyD
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _KeyD;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _KeyD = value;
            }
        }
        private bool _HornPedal = false;
        /// <summary>
        /// 警笛ペダルが押されているかどうか
        /// </summary>
        public bool HornPedal
        {
            get
            {
                using var rl = new ReadLock(LockSlim);
                return _HornPedal;
            }
            private set
            {
                using var wl = new WriteLock(LockSlim);
                _HornPedal = value;
            }
        }
        /// <summary>
        /// 受信したデータでプロパティ更新してイベント発火
        /// </summary>
        /// <param name="data"></param>
        private void SetInputsFromUSBData(byte[] data)
        {
            //Console.WriteLine(Convert.ToHexString(data));
            if (data[0] != 0xff) BrakeInput = ConvertToNotch(data[0], 28.0);
            if (data[1] != 0xff) MasconInput = ConvertToNotch(data[1], 18.0);
            if (data[2] == 0xff) HornPedal = false;
            else if (data[2] == 0x00) HornPedal = true;
            SetDirectionKeys((byte)(data[3] & 0x0f));
            KeyStart = Getbit(data[4], 5);
            KeySelect = Getbit(data[4], 4);
            KeyA = Getbit(data[4], 3);
            KeyB = Getbit(data[4], 2);
            KeyC = Getbit(data[4], 1);
            KeyD = Getbit(data[4], 0);
            UserInputMayChangedEvent?.Invoke(this);
        }
        /// <summary>
        /// ノッチ数に変換する
        /// </summary>
        /// <param name="usbdata"></param>
        /// <param name="divisor"></param>
        /// <returns></returns>
        private static int ConvertToNotch(byte usbdata, double divisor)
        {
            return (int)Math.Round(usbdata / divisor, MidpointRounding.AwayFromZero) - 1;
        }
        /// <summary>
        /// 特定のビットを取り出す
        /// </summary>
        /// <param name="bits"></param>
        /// <param name="bitpos"></param>
        /// <param name="ActiveHigh"></param>
        /// <returns></returns>
        private static bool Getbit(byte bits, int bitpos, bool ActiveHigh = true)
        {
            int Mask = 1 << (bitpos);
            bool Res = (bits & Mask) == Mask;
            return ActiveHigh ? Res : !Res;
        }
        /// <summary>
        /// 十字キーの各ボタンの押下状態を入れる
        /// </summary>
        /// <param name="KeysDirection"></param>
        private void SetDirectionKeys(byte KeysDirection)
        {
            KeyUP = false;
            KeyDown = false;
            KeyLeft = false;
            KeyRight = false;
            switch (KeysDirection)
            {
                case 0:
                    KeyUP = true;
                    break;
                case 1:
                    KeyUP = true;
                    KeyRight = true;
                    break;
                case 2:
                    KeyRight = true;
                    break;
                case 3:
                    KeyRight = true;
                    KeyDown = true;
                    break;
                case 4:
                    KeyDown = true;
                    break;
                case 5:
                    KeyDown = true;
                    KeyLeft = true;
                    break;
                case 6:
                    KeyLeft = true;
                    break;
                case 7:
                    KeyLeft = true;
                    KeyUP = false;
                    break;
            }
        }
        public delegate void UserInputMayChanged(TAITO_DENSYA_CON_T02_USBctrl sender);
        /// <summary>
        /// コントローラに何か入力変化があったかもしれない時に発火するイベント
        /// </summary>
        public event UserInputMayChanged? UserInputMayChangedEvent;

        public void Dispose()
        {
            EndReceiveData();
            USBDataReceivedEvent -= SetInputsFromUSBData;
            usbDevice.Close();
        }
    }
}
```