MayaFlux 1.0.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 const bool first_in_pass = current_paths.insert(path).second;
160
161 bool is_new = first_in_pass && (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 info.manufacturer = wide_to_utf8(cur->manufacturer_string);
178 if (cur->product_string) {
179 info.name = wide_to_utf8(cur->product_string);
180 } else {
181 info.name = "HID Device " + std::to_string(cur->vendor_id) + ":" + std::to_string(cur->product_id);
182 }
183 info.serial_number = wide_to_utf8(cur->serial_number);
184
185 m_enumerated_devices[dev_id] = info;
186
187 if (is_new) {
189 "HID device found: {} (VID:{:04X} PID:{:04X})",
190 info.name, info.vendor_id, info.product_id);
191 notify_device_change(info, true);
192 }
193
194 cur = cur->next;
195 }
196
197 hid_free_enumeration(devs);
198
199 for (auto it = m_enumerated_devices.begin(); it != m_enumerated_devices.end();) {
200 if (current_paths.find(it->second.path) == current_paths.end()) {
202 "HID device disconnected: {}", it->second.name);
203
204 auto open_it = m_open_devices.find(it->first);
205 if (open_it != m_open_devices.end()) {
206 if (open_it->second->handle) {
207 hid_close(open_it->second->handle);
208 }
209 m_open_devices.erase(open_it);
210 }
211
212 notify_device_change(it->second, false);
213 it = m_enumerated_devices.erase(it);
214 } else {
215 ++it;
216 }
217 }
218
219 return m_enumerated_devices.size();
220}
221
222bool HIDBackend::open_device(uint32_t device_id)
223{
224 std::lock_guard lock(m_devices_mutex);
225
226 if (m_open_devices.find(device_id) != m_open_devices.end()) {
228 "HID device {} already open", device_id);
229 return true;
230 }
231
232 auto it = m_enumerated_devices.find(device_id);
233 if (it == m_enumerated_devices.end()) {
235 "HID device {} not found", device_id);
236 return false;
237 }
238
239 hid_device* handle = hid_open_path(it->second.path.c_str());
240 if (!handle) {
242 "Failed to open HID device {}: {}", device_id, it->second.name);
243 return false;
244 }
245
246 hid_set_nonblocking(handle, 1);
247
248 HIDDeviceState state;
249 state.handle = handle;
250 state.info = it->second;
252 state.active.store(true);
253
254 auto state_ptr = std::make_shared<HIDDeviceState>();
255 state_ptr->handle = handle;
256 state_ptr->info = it->second;
257 state_ptr->read_buffer.resize(m_config.read_buffer_size);
258 state_ptr->active.store(true);
259
260 m_open_devices.insert_or_assign(device_id, state_ptr);
261
263 "Opened HID device {}: {}", device_id, it->second.name);
264
265 return true;
266}
267
268void HIDBackend::close_device(uint32_t device_id)
269{
270 std::lock_guard lock(m_devices_mutex);
271
272 auto it = m_open_devices.find(device_id);
273 if (it == m_open_devices.end()) {
274 return;
275 }
276
277 it->second->active.store(false);
278 if (it->second->handle) {
279 hid_close(it->second->handle);
280 }
281
283 "Closed HID device {}: {}", device_id, it->second->info.name);
284
285 m_open_devices.erase(it);
286}
287
288bool HIDBackend::is_device_open(uint32_t device_id) const
289{
290 std::lock_guard lock(m_devices_mutex);
291 return m_open_devices.find(device_id) != m_open_devices.end();
292}
293
294std::vector<uint32_t> HIDBackend::get_open_devices() const
295{
296 std::lock_guard lock(m_devices_mutex);
297
298 std::vector<uint32_t> result;
299 result.reserve(m_open_devices.size());
300
301 for (const auto& [id, state] : m_open_devices) {
302 result.push_back(id);
303 }
304
305 return result;
306}
307
309{
310 std::lock_guard lock(m_callback_mutex);
311 m_input_callback = std::move(callback);
312}
313
315{
316 std::lock_guard lock(m_callback_mutex);
317 m_device_callback = std::move(callback);
318}
319
320std::string HIDBackend::get_version() const
321{
322 const struct hid_api_version* ver = hid_version();
323 if (ver) {
324 return std::to_string(ver->major) + "." + std::to_string(ver->minor) + "." + std::to_string(ver->patch);
325 }
326 return "unknown";
327}
328
330{
331 m_config.filters.push_back(filter);
332}
333
338
339std::optional<HIDDeviceInfoExt> HIDBackend::get_device_info_ext(uint32_t device_id) const
340{
341 std::lock_guard lock(m_devices_mutex);
342 auto it = m_enumerated_devices.find(device_id);
343 if (it != m_enumerated_devices.end()) {
344 return it->second;
345 }
346 return std::nullopt;
347}
348
349int HIDBackend::send_feature_report(uint32_t device_id, std::span<const uint8_t> data)
350{
351 std::lock_guard lock(m_devices_mutex);
352 auto it = m_open_devices.find(device_id);
353 if (it == m_open_devices.end() || !it->second->handle) {
354 return -1;
355 }
356 return hid_send_feature_report(it->second->handle, data.data(), data.size());
357}
358
359int HIDBackend::get_feature_report(uint32_t device_id, uint8_t report_id, std::span<uint8_t> buffer)
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 buffer[0] = report_id;
367 return hid_get_feature_report(it->second->handle, buffer.data(), buffer.size());
368}
369
370int HIDBackend::write(uint32_t device_id, std::span<const uint8_t> data)
371{
372 std::lock_guard lock(m_devices_mutex);
373 auto it = m_open_devices.find(device_id);
374 if (it == m_open_devices.end() || !it->second->handle) {
375 return -1;
376 }
377 return hid_write(it->second->handle, data.data(), data.size());
378}
379
380// ─────────────────────────────────────────────────────────────────────────────
381// Private Implementation
382// ─────────────────────────────────────────────────────────────────────────────
383
385{
386 while (!m_stop_requested.load()) {
387 {
388 std::lock_guard lock(m_devices_mutex);
389 for (auto& [id, state] : m_open_devices) {
390 if (state->active.load() && state->handle) {
391 poll_device(id, *state);
392 }
393 }
394 }
395
396 std::this_thread::sleep_for(std::chrono::milliseconds(1));
397 }
398}
399
400void HIDBackend::poll_device(uint32_t device_id, HIDDeviceState& state)
401{
402 int bytes_read = hid_read_timeout(
403 state.handle, state.read_buffer.data(),
404 state.read_buffer.size(), m_config.poll_timeout_ms);
405
406 if (bytes_read > 0) {
407 std::span<const uint8_t> report(state.read_buffer.data(), bytes_read);
408 InputValue value = parse_hid_report(device_id, report);
410 } else if (bytes_read < 0) {
412 "HID read error on device {}", device_id);
413 state.active.store(false);
414 }
415 // bytes_read == 0 means timeout (no data), which is normal
416}
417
418bool HIDBackend::matches_any_filter(uint16_t vid, uint16_t pid,
419 uint16_t usage_page, uint16_t usage) const
420{
421 return std::ranges::any_of(m_config.filters,
422 [=](const HIDDeviceFilter& f) {
423 return f.matches(vid, pid, usage_page, usage);
424 });
425}
426
427uint32_t HIDBackend::find_or_assign_device_id(const std::string& path)
428{
429 for (const auto& [id, info] : m_enumerated_devices) {
430 if (info.path == path) {
431 return id;
432 }
433 }
434 return m_next_device_id++;
435}
436
438{
439 std::lock_guard lock(m_callback_mutex);
440 if (m_input_callback) {
442 }
443}
444
445void HIDBackend::notify_device_change(const InputDeviceInfo& info, bool connected)
446{
447 std::lock_guard lock(m_callback_mutex);
448 if (m_device_callback) {
449 m_device_callback(info, connected);
450 }
451}
452
453InputValue HIDBackend::parse_hid_report(uint32_t device_id, std::span<const uint8_t> report)
454{
457 value.data = std::vector<uint8_t>(report.begin(), report.end());
458 value.timestamp_ns = static_cast<uint64_t>(
459 std::chrono::steady_clock::now().time_since_epoch().count());
460 value.device_id = device_id;
461 value.source_type = InputType::HID;
462 return value;
463}
464} // namespace MayaFlux::Core
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
bool size
hid_device_ hid_device
Definition HIDBackend.hpp:5
std::shared_ptr< NetworkGeometryBuffer > buffer
PickCallback callback
Definition Picker.cpp:22
uint16_t usage
uint16_t usage_page
uint8_t report_id
glm::vec3 value
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.
MAYAFLUX_API std::string wide_to_utf8(const wchar_t *ws)
Encode a null-terminated wide string as UTF-8.
@ 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.
@ BYTES
Raw byte data (HID reports, sysex)
Generic input value container.