MayaFlux 0.2.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
HIDBackend.cpp
Go to the documentation of this file.
1#include "HIDBackend.hpp"
2
4
5#include <hidapi.h>
6
7namespace MayaFlux::Core {
8
13
15 : m_config(std::move(config))
16{
17}
18
20{
21 if (m_initialized.load()) {
22 shutdown();
23 }
24}
25
27{
28 if (m_initialized.load()) {
30 "HIDBackend already initialized");
31 return true;
32 }
33
34 if (hid_init() != 0) {
36 "Failed to initialize HIDAPI");
37 return false;
38 }
39
41 "HIDBackend initialized (HIDAPI version: {})", get_version());
42
43 m_initialized.store(true);
45
46 return true;
47}
48
50{
51 if (!m_initialized.load()) {
53 "Cannot start HIDBackend: not initialized");
54 return;
55 }
56
57 if (m_running.load()) {
59 "HIDBackend already running");
60 return;
61 }
62
63 m_stop_requested.store(false);
64 m_running.store(true);
65
66 m_poll_thread = std::thread(&HIDBackend::poll_thread_func, this);
67
69 "HIDBackend started polling {} open device(s)", get_open_devices().size());
70}
71
73{
74 if (!m_running.load()) {
75 return;
76 }
77
78 m_stop_requested.store(true);
79
80 if (m_poll_thread.joinable()) {
81 m_poll_thread.join();
82 }
83
84 m_running.store(false);
85
87 "HIDBackend stopped");
88}
89
91{
92 if (!m_initialized.load()) {
93 return;
94 }
95
96 stop();
97
98 {
99 std::lock_guard lock(m_devices_mutex);
100 for (auto& [id, state] : m_open_devices) {
101 if (state->handle) {
102 hid_close(state->handle);
103 state->handle = nullptr;
104 }
105 }
106 m_open_devices.clear();
107 m_enumerated_devices.clear();
108 }
109
110 hid_exit();
111 m_initialized.store(false);
112
114 "HIDBackend shutdown complete");
115}
116
117std::vector<InputDeviceInfo> HIDBackend::get_devices() const
118{
119 std::lock_guard lock(m_devices_mutex);
120
121 std::vector<InputDeviceInfo> result;
122 result.reserve(m_enumerated_devices.size());
123
124 for (const auto& [id, ext_info] : m_enumerated_devices) {
125 result.push_back(ext_info);
126 }
127
128 return result;
129}
130
132{
133 if (!m_initialized.load()) {
134 return 0;
135 }
136
137 std::lock_guard lock(m_devices_mutex);
138
139 std::unordered_set<std::string> previous_paths;
140 for (const auto& [id, info] : m_enumerated_devices) {
141 previous_paths.insert(info.path);
142 }
143
144 std::unordered_set<std::string> current_paths;
145
146 hid_device_info* devs = hid_enumerate(0x0, 0x0);
147 hid_device_info* cur = devs;
148
149 while (cur) {
150 if (!m_config.filters.empty()) {
151 if (!matches_any_filter(cur->vendor_id, cur->product_id,
152 cur->usage_page, cur->usage)) {
153 cur = cur->next;
154 continue;
155 }
156 }
157
158 std::string path(cur->path);
159 current_paths.insert(path);
160
161 bool is_new = (previous_paths.find(path) == previous_paths.end());
162
163 uint32_t dev_id = find_or_assign_device_id(path);
164
165 HIDDeviceInfoExt info;
166 info.id = dev_id;
168 info.vendor_id = cur->vendor_id;
169 info.product_id = cur->product_id;
170 info.usage_page = cur->usage_page;
171 info.usage = cur->usage;
172 info.release_number = cur->release_number;
173 info.interface_number = cur->interface_number;
174 info.path = path;
175 info.is_connected = true;
176
177 if (cur->manufacturer_string) {
178 std::wstring ws(cur->manufacturer_string);
179 info.manufacturer.resize(ws.length());
180 std::ranges::transform(ws, info.manufacturer.begin(), [](wchar_t c) { return static_cast<char>(c); });
181 }
182 if (cur->product_string) {
183 std::wstring ws(cur->product_string);
184 info.name.resize(ws.length());
185 std::ranges::transform(ws, info.name.begin(), [](wchar_t c) { return static_cast<char>(c); });
186 } else {
187 info.name = "HID Device " + std::to_string(cur->vendor_id) + ":" + std::to_string(cur->product_id);
188 }
189 if (cur->serial_number) {
190 std::wstring ws(cur->serial_number);
191 info.serial_number.resize(ws.length());
192 std::ranges::transform(ws, info.serial_number.begin(), [](wchar_t c) { return static_cast<char>(c); });
193 }
194
195 m_enumerated_devices[dev_id] = info;
196
197 if (is_new) {
199 "HID device found: {} (VID:{:04X} PID:{:04X})",
200 info.name, info.vendor_id, info.product_id);
201 notify_device_change(info, true);
202 }
203
204 cur = cur->next;
205 }
206
207 hid_free_enumeration(devs);
208
209 for (auto it = m_enumerated_devices.begin(); it != m_enumerated_devices.end();) {
210 if (current_paths.find(it->second.path) == current_paths.end()) {
212 "HID device disconnected: {}", it->second.name);
213
214 auto open_it = m_open_devices.find(it->first);
215 if (open_it != m_open_devices.end()) {
216 if (open_it->second->handle) {
217 hid_close(open_it->second->handle);
218 }
219 m_open_devices.erase(open_it);
220 }
221
222 notify_device_change(it->second, false);
223 it = m_enumerated_devices.erase(it);
224 } else {
225 ++it;
226 }
227 }
228
229 return m_enumerated_devices.size();
230}
231
232bool HIDBackend::open_device(uint32_t device_id)
233{
234 std::lock_guard lock(m_devices_mutex);
235
236 if (m_open_devices.find(device_id) != m_open_devices.end()) {
238 "HID device {} already open", device_id);
239 return true;
240 }
241
242 auto it = m_enumerated_devices.find(device_id);
243 if (it == m_enumerated_devices.end()) {
245 "HID device {} not found", device_id);
246 return false;
247 }
248
249 hid_device* handle = hid_open_path(it->second.path.c_str());
250 if (!handle) {
252 "Failed to open HID device {}: {}", device_id, it->second.name);
253 return false;
254 }
255
256 hid_set_nonblocking(handle, 1);
257
258 HIDDeviceState state;
259 state.handle = handle;
260 state.info = it->second;
262 state.active.store(true);
263
264 auto state_ptr = std::make_shared<HIDDeviceState>();
265 state_ptr->handle = handle;
266 state_ptr->info = it->second;
267 state_ptr->read_buffer.resize(m_config.read_buffer_size);
268 state_ptr->active.store(true);
269
270 m_open_devices.insert_or_assign(device_id, state_ptr);
271
273 "Opened HID device {}: {}", device_id, it->second.name);
274
275 return true;
276}
277
278void HIDBackend::close_device(uint32_t device_id)
279{
280 std::lock_guard lock(m_devices_mutex);
281
282 auto it = m_open_devices.find(device_id);
283 if (it == m_open_devices.end()) {
284 return;
285 }
286
287 it->second->active.store(false);
288 if (it->second->handle) {
289 hid_close(it->second->handle);
290 }
291
293 "Closed HID device {}: {}", device_id, it->second->info.name);
294
295 m_open_devices.erase(it);
296}
297
298bool HIDBackend::is_device_open(uint32_t device_id) const
299{
300 std::lock_guard lock(m_devices_mutex);
301 return m_open_devices.find(device_id) != m_open_devices.end();
302}
303
304std::vector<uint32_t> HIDBackend::get_open_devices() const
305{
306 std::lock_guard lock(m_devices_mutex);
307
308 std::vector<uint32_t> result;
309 result.reserve(m_open_devices.size());
310
311 for (const auto& [id, state] : m_open_devices) {
312 result.push_back(id);
313 }
314
315 return result;
316}
317
319{
320 std::lock_guard lock(m_callback_mutex);
321 m_input_callback = std::move(callback);
322}
323
325{
326 std::lock_guard lock(m_callback_mutex);
327 m_device_callback = std::move(callback);
328}
329
330std::string HIDBackend::get_version() const
331{
332 const struct hid_api_version* ver = hid_version();
333 if (ver) {
334 return std::to_string(ver->major) + "." + std::to_string(ver->minor) + "." + std::to_string(ver->patch);
335 }
336 return "unknown";
337}
338
340{
341 m_config.filters.push_back(filter);
342}
343
348
349std::optional<HIDDeviceInfoExt> HIDBackend::get_device_info_ext(uint32_t device_id) const
350{
351 std::lock_guard lock(m_devices_mutex);
352 auto it = m_enumerated_devices.find(device_id);
353 if (it != m_enumerated_devices.end()) {
354 return it->second;
355 }
356 return std::nullopt;
357}
358
359int HIDBackend::send_feature_report(uint32_t device_id, std::span<const uint8_t> data)
360{
361 std::lock_guard lock(m_devices_mutex);
362 auto it = m_open_devices.find(device_id);
363 if (it == m_open_devices.end() || !it->second->handle) {
364 return -1;
365 }
366 return hid_send_feature_report(it->second->handle, data.data(), data.size());
367}
368
369int HIDBackend::get_feature_report(uint32_t device_id, uint8_t report_id, std::span<uint8_t> buffer)
370{
371 std::lock_guard lock(m_devices_mutex);
372 auto it = m_open_devices.find(device_id);
373 if (it == m_open_devices.end() || !it->second->handle) {
374 return -1;
375 }
376 buffer[0] = report_id;
377 return hid_get_feature_report(it->second->handle, buffer.data(), buffer.size());
378}
379
380int HIDBackend::write(uint32_t device_id, std::span<const uint8_t> data)
381{
382 std::lock_guard lock(m_devices_mutex);
383 auto it = m_open_devices.find(device_id);
384 if (it == m_open_devices.end() || !it->second->handle) {
385 return -1;
386 }
387 return hid_write(it->second->handle, data.data(), data.size());
388}
389
390// ─────────────────────────────────────────────────────────────────────────────
391// Private Implementation
392// ─────────────────────────────────────────────────────────────────────────────
393
395{
396 while (!m_stop_requested.load()) {
397 {
398 std::lock_guard lock(m_devices_mutex);
399 for (auto& [id, state] : m_open_devices) {
400 if (state->active.load() && state->handle) {
401 poll_device(id, *state);
402 }
403 }
404 }
405
406 std::this_thread::sleep_for(std::chrono::milliseconds(1));
407 }
408}
409
410void HIDBackend::poll_device(uint32_t device_id, HIDDeviceState& state)
411{
412 int bytes_read = hid_read_timeout(
413 state.handle, state.read_buffer.data(),
414 state.read_buffer.size(), m_config.poll_timeout_ms);
415
416 if (bytes_read > 0) {
417 std::span<const uint8_t> report(state.read_buffer.data(), bytes_read);
418 InputValue value = parse_hid_report(device_id, report);
419 notify_input(value);
420 } else if (bytes_read < 0) {
422 "HID read error on device {}", device_id);
423 state.active.store(false);
424 }
425 // bytes_read == 0 means timeout (no data), which is normal
426}
427
428bool HIDBackend::matches_any_filter(uint16_t vid, uint16_t pid,
429 uint16_t usage_page, uint16_t usage) const
430{
431 return std::ranges::any_of(m_config.filters,
432 [=](const HIDDeviceFilter& f) {
433 return f.matches(vid, pid, usage_page, usage);
434 });
435}
436
437uint32_t HIDBackend::find_or_assign_device_id(const std::string& path)
438{
439 for (const auto& [id, info] : m_enumerated_devices) {
440 if (info.path == path) {
441 return id;
442 }
443 }
444 return m_next_device_id++;
445}
446
448{
449 std::lock_guard lock(m_callback_mutex);
450 if (m_input_callback) {
451 m_input_callback(value);
452 }
453}
454
455void HIDBackend::notify_device_change(const InputDeviceInfo& info, bool connected)
456{
457 std::lock_guard lock(m_callback_mutex);
458 if (m_device_callback) {
459 m_device_callback(info, connected);
460 }
461}
462
463InputValue HIDBackend::parse_hid_report(uint32_t device_id, std::span<const uint8_t> report)
464{
465 InputValue value;
467 value.data = std::vector<uint8_t>(report.begin(), report.end());
468 value.timestamp_ns = static_cast<uint64_t>(
469 std::chrono::steady_clock::now().time_since_epoch().count());
470 value.device_id = device_id;
472 return value;
473}
474} // namespace MayaFlux::Core
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
hid_device_ hid_device
Definition HIDBackend.hpp:5
void close_device(uint32_t device_id) override
Close a previously opened device.
std::vector< InputDeviceInfo > get_devices() const override
Get list of available devices.
bool open_device(uint32_t device_id) override
Open a device for input.
void notify_input(const InputValue &value)
void clear_device_filters()
Clear all device filters.
InputValue parse_hid_report(uint32_t device_id, std::span< const uint8_t > report)
int send_feature_report(uint32_t device_id, std::span< const uint8_t > data)
Send a feature report to a device.
void start() override
Start listening for input events.
std::atomic< bool > m_running
bool matches_any_filter(uint16_t vid, uint16_t pid, uint16_t usage_page, uint16_t usage) const
int write(uint32_t device_id, std::span< const uint8_t > data)
Send an output report to a device.
bool is_device_open(uint32_t device_id) const override
Check if a device is currently open.
void set_input_callback(InputCallback callback) override
Register callback for input values.
std::atomic< bool > m_initialized
void set_device_callback(DeviceCallback callback) override
Register callback for device connect/disconnect events.
std::atomic< bool > m_stop_requested
void poll_device(uint32_t device_id, HIDDeviceState &state)
void add_device_filter(const HIDDeviceFilter &filter)
Add a device filter for enumeration.
std::optional< HIDDeviceInfoExt > get_device_info_ext(uint32_t device_id) const
Get extended HID device info.
void shutdown() override
Shutdown and release all resources.
DeviceCallback m_device_callback
uint32_t find_or_assign_device_id(const std::string &path)
std::unordered_map< uint32_t, HIDDeviceInfoExt > m_enumerated_devices
void stop() override
Stop listening for input events.
void notify_device_change(const InputDeviceInfo &info, bool connected)
std::string get_version() const override
Get backend version string.
int get_feature_report(uint32_t device_id, uint8_t report_id, std::span< uint8_t > buffer)
Get a feature report from a device.
bool initialize() override
Initialize the input backend.
std::vector< uint32_t > get_open_devices() const override
Get list of currently open device IDs.
size_t refresh_devices() override
Refresh the device list.
std::unordered_map< uint32_t, std::shared_ptr< HIDDeviceState > > m_open_devices
HIDAPI-based HID input backend.
@ HID
Generic HID devices (game controllers, custom hardware)
std::function< void(const InputValue &)> InputCallback
Callback signature for input events.
std::function< void(const InputDeviceInfo &, bool connected)> DeviceCallback
Callback signature for device connection/disconnection events.
@ InputBackend
Input device backend (HID, MIDI, OSC)
@ Core
Core engine, backend, subsystems.
int poll_timeout_ms
Timeout for hid_read_timeout.
size_t read_buffer_size
Per-device read buffer size.
std::vector< HIDDeviceFilter > filters
Device filters (empty = all devices)
Configuration for HID backend.
Filter for HID device enumeration.
uint16_t release_number
Device release number.
uint16_t usage_page
HID usage page.
std::string path
Platform-specific device path.
int interface_number
USB interface number (-1 if unknown)
Extended HID device information.
std::vector< uint8_t > read_buffer
std::atomic< bool > active
Internal state for an open HID device.
bool is_connected
Current connection state.
std::string name
Human-readable device name.
std::string manufacturer
Device manufacturer (if available)
uint16_t product_id
USB Product ID.
uint32_t id
Unique device identifier within backend.
std::string serial_number
Device serial (if available)
uint16_t vendor_id
USB Vendor ID.
InputType backend_type
Which backend manages this device.
Information about a connected input device.
uint32_t device_id
Source device identifier.
uint64_t timestamp_ns
Nanoseconds since epoch (or backend start)
@ BYTES
Raw byte data (HID reports, sysex)
std::variant< double, std::vector< double >, std::vector< uint8_t >, MIDIMessage, OSCMessage > data
InputType source_type
Backend that generated this value.
Generic input value container.