• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Aldaviva / HidClient / 18671371811

21 Oct 2025 02:56AM UTC coverage: 96.552% (-2.3%) from 98.851%
18671371811

push

github

Aldaviva
Updated dependencies

34 of 40 branches covered (85.0%)

84 of 87 relevant lines covered (96.55%)

14887.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.55
/HidClient/AbstractHidClient.cs
1
using System.ComponentModel;
2
using System.Runtime.CompilerServices;
3
using HidSharp;
4

5
namespace HidClient;
6

7
/// <inheritdoc cref="IHidClient"/>
8
public abstract class AbstractHidClient: IHidClient {
9

10
    private readonly object _deviceStreamLock = new();
7✔
11

12
    private DeviceList?              _deviceList;
13
    private CancellationTokenSource? _cancellationTokenSource;
14
    private bool                     _isConnected;
15
    private int                      _maxInputReportLength;
16

17
    /// <summary>
18
    /// <para><c>HidSharp</c> stream that can be used to read or write bytes from the device, or set features.</para>
19
    /// <para>This will be <see langword="null"/> when the device is disconnected.</para>
20
    /// </summary>
21
    protected HidStream? DeviceStream;
22

23
    /// <summary>
24
    /// <para>The USB Vendor ID or <c>VID</c> of the device you want to connect to.</para>
25
    /// <para>In Windows, this can be found in Device Manager as the hexadecimal <c>VID</c> value under Hardware Ids.</para>
26
    /// <para>In Linux, this can be found in the output of <c>lsusb</c> in the hexadecimal <c>ID</c> colon-delimited value.</para>
27
    /// </summary>
28
    protected abstract int VendorId { get; }
29

30
    /// <summary>
31
    /// <para>The USB Product ID or <c>PID</c> of the device you want to connect to.</para>
32
    /// <para>In Windows, this can be found in Device Manager as the hexadecimal <c>PID</c> value under Hardware Ids.</para>
33
    /// <para>In Linux, this can be found in the output of <c>lsusb</c> in the hexadecimal <c>ID</c> colon-delimited value.</para>
34
    /// </summary>
35
    protected abstract int ProductId { get; }
36

37
    /// <inheritdoc />
38
    public event EventHandler<bool>? IsConnectedChanged;
39

40
    /// <inheritdoc />
41
    public event PropertyChangedEventHandler? PropertyChanged;
42

43
    /// <inheritdoc />
44
    public SynchronizationContext EventSynchronizationContext { get; set; } = SynchronizationContext.Current ?? new SynchronizationContext();
18!
45

46
    /// <summary>
47
    /// <para>Construct a new instance using the local system device list. This is the default invocation which is recommended for general use.</para>
48
    /// <para>During construction, this new instance will attempt to connect to a device with the given <see cref="VendorId"/> and <see cref="ProductId"/>.</para>
49
    /// </summary>
50
    protected AbstractHidClient(): this(DeviceList.Local) { }
2✔
51

52
    /// <summary>
53
    /// <para>Construct a new instance using a custom device list. This is an advanced invocation that is useful for unit testing.</para>
54
    /// <para>During construction, this new instance will attempt to connect to a device with the given <see cref="VendorId"/> and <see cref="ProductId"/>.</para>
55
    /// </summary>
56
    protected AbstractHidClient(DeviceList deviceList) {
7✔
57
        _deviceList         =  deviceList;
7✔
58
        _deviceList.Changed += OnDeviceListChanged;
7✔
59
        AttachToDevice();
7✔
60
    }
7✔
61

62
    /// <inheritdoc />
63
    public bool IsConnected {
64
        get => _isConnected;
2✔
65
        private set {
66
            if (value != _isConnected) {
10✔
67
                _isConnected = value;
10✔
68
                EventSynchronizationContext.Post(_ => {
10✔
69
                    IsConnectedChanged?.Invoke(this, value);
9✔
70
                    OnPropertyChanged();
9✔
71
                }, null);
19✔
72
            }
73
        }
10✔
74
    }
75

76
    private void OnDeviceListChanged(object? sender, DeviceListChangedEventArgs e) {
77
        AttachToDevice();
2✔
78
    }
2✔
79

80
    private void AttachToDevice() {
81
        bool isNewStream = false;
11✔
82
        lock (_deviceStreamLock) {
11✔
83
            if (DeviceStream == null) {
11✔
84
                HidDevice? newDevice = _deviceList?.GetHidDeviceOrNull(VendorId, ProductId);
10!
85
                if (newDevice != null) {
10✔
86
                    DeviceStream          = newDevice.Open();
8✔
87
                    _maxInputReportLength = newDevice.GetMaxInputReportLength();
8✔
88
                    isNewStream           = true;
8✔
89
                }
90
            }
91
        }
11✔
92

93
        if (DeviceStream != null && isNewStream) {
11✔
94
            DeviceStream.Closed      += ReattachToDevice;
8✔
95
            DeviceStream.ReadTimeout =  Timeout.Infinite;
8✔
96
            _cancellationTokenSource =  new CancellationTokenSource();
8✔
97
            IsConnected              =  true;
8✔
98
            OnConnect();
8✔
99

100
            try {
101
                Task.Factory.StartNew(HidReadLoop, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
8✔
102
            } catch (TaskCanceledException) { }
8✔
103
        }
104
    }
11✔
105

106
    /// <summary>
107
    /// Called when connected to a device after it was previously disconnected. When invoked, <see cref="IsConnected"/> will be <see langword="true"/>.
108
    /// </summary>
109
    protected virtual void OnConnect() { }
8✔
110

111
    private async Task HidReadLoop() {
112
        CancellationToken cancellationToken = _cancellationTokenSource!.Token;
7✔
113

114
        try {
115
            byte[] readBuffer = new byte[_maxInputReportLength > 0 ? _maxInputReportLength : 128];
6!
116
            while (!cancellationToken.IsCancellationRequested) {
215,792✔
117
                int readBytes = await DeviceStream!.ReadAsync(readBuffer, 0, readBuffer.Length, cancellationToken).ConfigureAwait(false);
215,791✔
118
                if (readBytes != 0) {
215,786✔
119
                    byte[] filledReadBuffer = readBuffer;
215,786✔
120
                    if (readBuffer.Length != readBytes) {
215,786!
121
                        filledReadBuffer = new byte[readBytes];
×
122
                        Array.Copy(readBuffer, 0, filledReadBuffer, 0, readBytes);
×
123
                    }
124

125
                    OnHidRead(filledReadBuffer);
215,786✔
126
                }
127
            }
128
        } catch (IOException) {
3✔
129
            ReattachToDevice();
2✔
130
        }
2✔
131
    }
3✔
132

133
    /// <summary>
134
    /// Callback method that is invoked when HID bytes are read from the device.
135
    /// </summary>
136
    /// <param name="readBuffer">Bytes that were read from the device, matching the <c>HID Data</c> field in USBPcap.</param>
137
    protected abstract void OnHidRead(byte[] readBuffer);
138

139
    private void ReattachToDevice(object? sender = null, EventArgs? e = null) {
140
        bool disconnected = false;
2✔
141
        lock (_deviceStreamLock) {
2✔
142
            if (DeviceStream != null) {
2✔
143
                DeviceStream.Closed -= ReattachToDevice;
2✔
144
                DeviceStream.Close();
2✔
145
                DeviceStream.Dispose();
2✔
146
                DeviceStream = null;
2✔
147
                disconnected = true;
2✔
148
            }
149
        }
2✔
150

151
        if (disconnected) {
2✔
152
            IsConnected = false;
2✔
153
        }
154

155
        try {
156
            _cancellationTokenSource?.Cancel();
2!
157
        } catch (AggregateException) { }
2✔
158

159
        AttachToDevice();
2✔
160
    }
2✔
161

162
    /// <summary>
163
    /// Raise the <see cref="PropertyChanged"/> event
164
    /// </summary>
165
    /// <param name="propertyName">the name of the property that changed, defaults to the caller member name</param>
166
    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
167
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
9!
168
    }
×
169

170
    /// <summary>
171
    /// <para>Clean up managed and, optionally, unmanaged resources.</para>
172
    /// <para>When inheriting from <see cref="AbstractHidClient"/>, you should override this method, dispose of your managed resources if <paramref name="disposing"/> is <see langword="true" />, then
173
    /// free your unmanaged resources regardless of the value of <paramref name="disposing"/>, and finally call this base <see cref="Dispose(bool)"/> implementation.</para>
174
    /// <para>For more information, see <see url="https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose#implement-the-dispose-pattern-for-a-derived-class">Implement
175
    /// the dispose pattern for a derived class</see>.</para>
176
    /// </summary>
177
    /// <param name="disposing">Should be <see langword="false" /> when called from a finalizer, and <see langword="true" /> when called from the <see cref="Dispose()"/> method. In other words, it is
178
    /// <see langword="true" /> when deterministically called and <see langword="false" /> when non-deterministically called.</param>
179
    protected virtual void Dispose(bool disposing) {
180
        if (disposing) {
5✔
181
            try {
182
                _cancellationTokenSource?.Cancel();
5✔
183
                _cancellationTokenSource?.Dispose();
5✔
184
                _cancellationTokenSource = null;
5✔
185
            } catch (AggregateException) { }
5✔
186

187
            lock (_deviceStreamLock) {
5✔
188
                if (DeviceStream != null) {
5✔
189
                    DeviceStream.Closed -= ReattachToDevice;
3✔
190
                    DeviceStream.Close();
3✔
191
                    DeviceStream.Dispose();
3✔
192
                    DeviceStream = null;
3✔
193
                }
194
            }
5✔
195

196
            if (_deviceList != null) {
5✔
197
                _deviceList.Changed -= OnDeviceListChanged;
4✔
198
                _deviceList         =  null;
4✔
199
            }
200
        }
201
    }
5✔
202

203
    /// <summary>
204
    /// <para>Disconnect from any connected device and clean up managed resources.</para>
205
    /// <para><see cref="IsConnectedChanged"/> and <see cref="INotifyPropertyChanged.PropertyChanged"/> events will not be emitted if a device is disconnected during disposal.</para>
206
    /// <para>Subclasses of <see cref="AbstractHidClient"/> should override <see cref="Dispose(bool)"/>.</para>
207
    /// <para>For more information, see <see url="https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/unmanaged">Cleaning Up Unmanaged Resources</see> and
208
    /// <see url="https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose">Implementing a Dispose Method</see>.</para>
209
    /// </summary>
210
    public void Dispose() {
211
        Dispose(true);
5✔
212
        GC.SuppressFinalize(this);
5✔
213
    }
5✔
214

215
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc