MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
TabletBackend.cpp
Go to the documentation of this file.
1#include "TabletBackend.hpp"
2
4
5#include <hidapi.h>
6
7#ifdef MAYAFLUX_PLATFORM_MACOS
8#include <hidapi_darwin.h>
9#endif
10
11namespace MayaFlux::Core {
12
13namespace {
14
15 constexpr uint16_t k_page_generic = 0x01;
16 constexpr uint16_t k_page_digitizer = 0x0D;
17
18 constexpr uint16_t k_usage_x = 0x30;
19 constexpr uint16_t k_usage_y = 0x31;
20 constexpr uint16_t k_usage_z = 0x32;
21 constexpr uint16_t k_usage_wheel = 0x38;
22
23 constexpr uint16_t k_usage_digitizer = 0x01;
24 constexpr uint16_t k_usage_pen = 0x02;
25 constexpr uint16_t k_usage_tip_pressure = 0x30;
26 constexpr uint16_t k_usage_barrel_pressure = 0x31;
27 constexpr uint16_t k_usage_in_range = 0x32;
28 constexpr uint16_t k_usage_invert = 0x3C;
29 constexpr uint16_t k_usage_tilt_x = 0x3D;
30 constexpr uint16_t k_usage_tilt_y = 0x3E;
31 constexpr uint16_t k_usage_twist = 0x41;
32 constexpr uint16_t k_usage_tip_switch = 0x42;
33 constexpr uint16_t k_usage_barrel_switch = 0x44;
34 constexpr uint16_t k_usage_eraser = 0x45;
35 constexpr uint16_t k_usage_barrel_switch_2 = 0x5A;
36 constexpr uint16_t k_usage_serial = 0x5B;
37
38 constexpr size_t k_slot_none = TabletFrame::SLOT_COUNT;
39 constexpr size_t k_pseudo_in_range = TabletFrame::SLOT_COUNT + 1;
40 constexpr size_t k_pseudo_tip = TabletFrame::SLOT_COUNT + 2;
41 constexpr size_t k_pseudo_invert = TabletFrame::SLOT_COUNT + 3;
42 constexpr size_t k_pseudo_barrel = TabletFrame::SLOT_COUNT + 4;
43 constexpr size_t k_pseudo_barrel_2 = TabletFrame::SLOT_COUNT + 5;
44 constexpr size_t k_pseudo_eraser = TabletFrame::SLOT_COUNT + 6;
45 constexpr size_t k_pseudo_serial = TabletFrame::SLOT_COUNT + 7;
46
47 /**
48 * @brief Map a usage to a frame slot or a pseudo slot.
49 *
50 * Pseudo slots are fields that contribute to BUTTONS or STATE rather
51 * than occupying a slot of their own.
52 */
53 size_t slot_for_usage(uint16_t page, uint16_t usage) noexcept
54 {
55 if (page == k_page_generic) {
56 switch (usage) {
57 case k_usage_x:
58 return TabletFrame::X;
59 case k_usage_y:
60 return TabletFrame::Y;
61 case k_usage_z:
63 case k_usage_wheel:
64 return TabletFrame::WHEEL;
65 default:
66 return k_slot_none;
67 }
68 }
69
70 if (page != k_page_digitizer)
71 return k_slot_none;
72
73 switch (usage) {
74 case k_usage_tip_pressure:
76 case k_usage_barrel_pressure:
78 case k_usage_tilt_x:
80 case k_usage_tilt_y:
82 case k_usage_twist:
84 case k_usage_in_range:
85 return k_pseudo_in_range;
86 case k_usage_tip_switch:
87 return k_pseudo_tip;
88 case k_usage_invert:
89 return k_pseudo_invert;
90 case k_usage_barrel_switch:
91 return k_pseudo_barrel;
92 case k_usage_barrel_switch_2:
93 return k_pseudo_barrel_2;
94 case k_usage_eraser:
95 return k_pseudo_eraser;
96 case k_usage_serial:
97 return k_pseudo_serial;
98 default:
99 return k_slot_none;
100 }
101 }
102
103 TabletAxes axis_for_slot(size_t slot) noexcept
104 {
105 switch (slot) {
112 return TabletAxes::TILT;
116 return TabletAxes::SLIDER;
118 return TabletAxes::WHEEL;
119 default:
120 return TabletAxes::NONE;
121 }
122 }
123
124 /**
125 * @brief Read a little-endian signed value of @p bytes width.
126 */
127 int32_t item_signed(const uint8_t* data, size_t bytes) noexcept
128 {
129 int32_t value = 0;
130 for (size_t i = 0; i < bytes; ++i)
131 value |= static_cast<int32_t>(data[i]) << (8U * i);
132
133 if (bytes > 0 && bytes < 4) {
134 const auto sign_bit = static_cast<int32_t>(1U << (8U * bytes - 1U));
135 if ((value & sign_bit) != 0)
136 value -= (sign_bit << 1);
137 }
138 return value;
139 }
140
141 uint32_t item_unsigned(const uint8_t* data, size_t bytes) noexcept
142 {
143 uint32_t value = 0;
144 for (size_t i = 0; i < bytes; ++i)
145 value |= static_cast<uint32_t>(data[i]) << (8U * i);
146 return value;
147 }
148
149 /**
150 * @brief Extract a field from a report, LSB first, sign extended.
151 */
152 int32_t extract_field(std::span<const uint8_t> report, const TabletField& field) noexcept
153 {
154 uint32_t raw = 0;
155 for (uint32_t i = 0; i < field.bit_size; ++i) {
156 const uint32_t bit = field.bit_offset + i;
157 const size_t byte = bit / 8U;
158 if (byte >= report.size())
159 break;
160 if (((report[byte] >> (bit % 8U)) & 1U) != 0U)
161 raw |= (1U << i);
162 }
163
164 if (field.logical_min < 0 && field.bit_size > 0 && field.bit_size < 32) {
165 const auto sign_bit = 1U << (field.bit_size - 1U);
166 if ((raw & sign_bit) != 0U)
167 return static_cast<int32_t>(raw) - static_cast<int32_t>(sign_bit << 1U);
168 }
169 return static_cast<int32_t>(raw);
170 }
171
172 double normalise(int32_t raw, int32_t lo, int32_t hi) noexcept
173 {
174 if (hi <= lo)
175 return 0.0;
176 const double span = static_cast<double>(hi) - static_cast<double>(lo);
177 const double v = (static_cast<double>(raw) - static_cast<double>(lo)) / span;
178 return std::clamp(v, 0.0, 1.0);
179 }
180
181 double to_degrees(int32_t raw, int32_t lo, int32_t hi, double limit) noexcept
182 {
183 if (hi <= lo)
184 return 0.0;
185 return (normalise(raw, lo, hi) * 2.0 - 1.0) * limit;
186 }
187
188 void set_state_bit(std::array<double, TabletFrame::SLOT_COUNT>& slots,
189 TabletState bit, bool on) noexcept
190 {
191 auto current = static_cast<uint32_t>(slots[TabletFrame::STATE]);
192 if (on) {
193 current |= static_cast<uint32_t>(bit);
194 } else {
195 current &= ~static_cast<uint32_t>(bit);
196 }
197 slots[TabletFrame::STATE] = static_cast<double>(current);
198 }
199
200 void set_button_bit(std::array<double, TabletFrame::SLOT_COUNT>& slots,
201 uint32_t bit, bool on) noexcept
202 {
203 auto mask = static_cast<uint32_t>(slots[TabletFrame::BUTTONS]);
204 if (on) {
205 mask |= (1U << bit);
206 } else {
207 mask &= ~(1U << bit);
208 }
209 slots[TabletFrame::BUTTONS] = static_cast<double>(mask);
210 }
211
212 /**
213 * @brief Last HIDAPI error for @p device as UTF-8, or the global error when null.
214 *
215 * The underlying buffer is only valid until the next call on the device,
216 * so the text is copied immediately.
217 */
218 std::string hid_failure_reason(hid_device* device)
219 {
220 auto text = wide_to_utf8(hid_error(device));
221 return text.empty() ? std::string("no detail from HIDAPI") : text;
222 }
223
224 /**
225 * @brief Global item state, saved and restored by Push and Pop.
226 */
227 struct GlobalState {
228 uint16_t usage_page {};
229 int32_t logical_min {};
230 int32_t logical_max {};
231 uint32_t report_size {};
232 uint32_t report_count {};
233 uint8_t report_id {};
234 };
235
236 /**
237 * @brief A local usage carrying its own page.
238 *
239 * Extended 32-bit usages embed a page that applies only to that usage,
240 * so the page cannot be folded into the global state.
241 */
242 struct LocalUsage {
243 uint16_t page {};
244 uint16_t usage {};
245 };
246
247} // namespace
248
249// =============================================================================
250// Descriptor parser
251// =============================================================================
252
253TabletLayout TabletBackend::parse_descriptor(std::span<const uint8_t> descriptor)
254{
255
256 TabletLayout layout;
257
258 GlobalState global;
259 std::vector<GlobalState> global_stack;
260
261 std::vector<LocalUsage> local_usages;
262 uint32_t usage_min = 0;
263 uint32_t usage_max = 0;
264 bool has_usage_range = false;
265
266 std::unordered_map<uint8_t, uint32_t> bit_cursor;
267 bool digitizer_seen = false;
268
269 const auto clear_locals = [&]() {
270 local_usages.clear();
271 usage_min = 0;
272 usage_max = 0;
273 has_usage_range = false;
274 };
275
276 size_t i = 0;
277 while (i < descriptor.size()) {
278 const uint8_t prefix = descriptor[i];
279
280 if (prefix == 0xFE) {
281 if (i + 1 >= descriptor.size())
282 break;
283 i += 2U + descriptor[i + 1];
284 continue;
285 }
286
287 size_t size = prefix & 0x03U;
288 if (size == 3)
289 size = 4;
290 const uint8_t type = (prefix >> 2U) & 0x03U;
291 const uint8_t tag = (prefix >> 4U) & 0x0FU;
292
293 ++i;
294 if (i + size > descriptor.size())
295 break;
296
297 const uint8_t* payload = descriptor.data() + i;
298 i += size;
299
300 if (type == 1) {
301 switch (tag) {
302 case 0x0:
303 global.usage_page = static_cast<uint16_t>(item_unsigned(payload, size));
304 if (global.usage_page == k_page_digitizer)
305 digitizer_seen = true;
306 break;
307
308 case 0x1:
309 global.logical_min = item_signed(payload, size);
310 break;
311
312 case 0x2: {
313 const int32_t as_signed = item_signed(payload, size);
314 global.logical_max = (global.logical_min >= 0 && as_signed < 0)
315 ? static_cast<int32_t>(item_unsigned(payload, size))
316 : as_signed;
317 break;
318 }
319
320 case 0x7:
321 global.report_size = item_unsigned(payload, size);
322 break;
323
324 case 0x8:
325 global.report_id = static_cast<uint8_t>(item_unsigned(payload, size));
326 layout.uses_report_ids = true;
327 break;
328
329 case 0x9:
330 global.report_count = item_unsigned(payload, size);
331 break;
332
333 case 0xA:
334 global_stack.push_back(global);
335 break;
336
337 case 0xB:
338 if (!global_stack.empty()) {
339 global = global_stack.back();
340 global_stack.pop_back();
341 }
342 break;
343
344 default:
345 break;
346 }
347 continue;
348 }
349
350 if (type == 2) {
351 switch (tag) {
352 case 0x0:
353 if (size == 4) {
354 const uint32_t full = item_unsigned(payload, size);
355 const auto page = static_cast<uint16_t>(full >> 16U);
356 local_usages.push_back({ .page = page, .usage = static_cast<uint16_t>(full & 0xFFFFU) });
357 if (page == k_page_digitizer)
358 digitizer_seen = true;
359 } else {
360 local_usages.push_back({ .page = global.usage_page,
361 .usage = static_cast<uint16_t>(item_unsigned(payload, size)) });
362 }
363 break;
364
365 case 0x1:
366 usage_min = item_unsigned(payload, size);
367 has_usage_range = true;
368 break;
369
370 case 0x2:
371 usage_max = item_unsigned(payload, size);
372 has_usage_range = true;
373 break;
374
375 default:
376 break;
377 }
378 continue;
379 }
380
381 if (type != 0) {
382 continue;
383 }
384
385 if (tag != 0x8) {
386 clear_locals();
387 continue;
388 }
389
390 const uint32_t flags = item_unsigned(payload, size);
391 const bool is_constant = (flags & 0x01U) != 0U;
392
393 if (global.report_size == 0 || global.report_count == 0) {
394 clear_locals();
395 continue;
396 }
397
398 uint32_t& cursor = bit_cursor[global.report_id];
399
400 for (uint32_t index = 0; index < global.report_count; ++index) {
401 LocalUsage current { .page = global.usage_page, .usage = 0 };
402
403 if (!is_constant) {
404 if (index < local_usages.size()) {
405 current = local_usages[index];
406 } else if (!local_usages.empty()) {
407 current = local_usages.back();
408 } else if (has_usage_range) {
409 current.usage = static_cast<uint16_t>(
410 std::min(usage_min + index, usage_max));
411 }
412 }
413
414 const size_t slot = is_constant
415 ? k_slot_none
416 : slot_for_usage(current.page, current.usage);
417
418 if (slot != k_slot_none) {
419 TabletField field;
420 field.report_id = global.report_id;
421 field.usage_page = current.page;
422 field.usage = current.usage;
423 field.bit_offset = cursor;
424 field.bit_size = global.report_size;
425 field.logical_min = global.logical_min;
426 field.logical_max = global.logical_max;
427 field.slot = slot;
428 layout.fields.push_back(field);
429
430 if (slot < TabletFrame::SLOT_COUNT)
431 layout.axes |= axis_for_slot(slot);
432 if (slot == k_pseudo_invert)
433 layout.has_invert = true;
434 if (slot == k_pseudo_serial)
435 layout.has_serial = true;
436 }
437
438 cursor += global.report_size;
439 }
440
441 clear_locals();
442 }
443
444 if (!digitizer_seen)
445 layout.fields.clear();
446
447 return layout;
448}
449
450// =============================================================================
451// Construction
452// =============================================================================
453
458
460 : m_config(config)
461{
462}
463
465{
466 if (m_initialized.load()) {
467 shutdown();
468 }
469}
470
471// =============================================================================
472// Lifecycle
473// =============================================================================
474
476{
477 if (m_initialized.load()) {
478 return true;
479 }
480
481 if (hid_init() != 0) {
483 "Failed to initialize HIDAPI for tablet backend: {}",
484 hid_failure_reason(nullptr));
485 return false;
486 }
487
488#ifdef MAYAFLUX_PLATFORM_MACOS
489 hid_darwin_set_open_exclusive(0);
490#endif
491
492 m_initialized.store(true);
493
494 const size_t found = refresh_devices();
495
497 "TabletBackend initialized, {} tool(s)", found);
498
499 return true;
500}
501
503{
504 if (!m_initialized.load()) {
506 "Cannot start TabletBackend: not initialized");
507 return;
508 }
509
510 if (m_running.load()) {
511 return;
512 }
513
514 m_stop_requested.store(false);
515 m_running.store(true);
517
519 "TabletBackend started");
520}
521
523{
524 if (!m_running.load()) {
525 return;
526 }
527
528 m_stop_requested.store(true);
529
530 if (m_poll_thread.joinable()) {
531 m_poll_thread.join();
532 }
533
534 m_running.store(false);
535}
536
538{
539 if (!m_initialized.load()) {
540 return;
541 }
542
543 stop();
544
545 {
546 std::lock_guard lock(m_devices_mutex);
547 for (auto& [path, device] : m_devices) {
548 if (device->handle) {
549 hid_close(device->handle);
550 device->handle = nullptr;
551 }
552 }
553 m_devices.clear();
554 m_tools.clear();
555 m_tool_paths.clear();
556 }
557
558 hid_exit();
559 m_initialized.store(false);
560
562 "TabletBackend shutdown complete");
563}
564
565// =============================================================================
566// Enumeration
567// =============================================================================
568
570{
571 if (!m_initialized.load()) {
572 return 0;
573 }
574
575 hid_device_info* devs = hid_enumerate(0x0, 0x0);
576
577 for (hid_device_info* cur = devs; cur != nullptr; cur = cur->next) {
579 && cur->usage_page != k_page_digitizer
580 && cur->usage_page != 0)
581 continue;
582
583 std::string path(cur->path);
584
585 {
586 std::lock_guard lock(m_devices_mutex);
587 if (m_devices.find(path) != m_devices.end())
588 continue;
589 }
590
591 std::string name = wide_to_utf8(cur->product_string);
592 if (name.empty())
593 name = "Tablet";
594
595 adopt_device(path, cur->vendor_id, cur->product_id, std::move(name));
596 }
597
598 hid_free_enumeration(devs);
599
600 std::lock_guard lock(m_devices_mutex);
601 return m_tools.size();
602}
603
604bool TabletBackend::adopt_device(const std::string& path, uint16_t vid,
605 uint16_t pid, std::string name)
606{
607 hid_device* handle = hid_open_path(path.c_str());
608 if (!handle) {
610 "Could not open HID device {} ({}): {}",
611 name, path, hid_failure_reason(nullptr));
612 return false;
613 }
614
615 std::vector<uint8_t> descriptor(4096);
616 const int written = hid_get_report_descriptor(handle,
617 descriptor.data(), descriptor.size());
618
619 if (written <= 0) {
621 "Could not read report descriptor of {} ({}): {}",
622 name, path, hid_failure_reason(handle));
623 hid_close(handle);
624 return false;
625 }
626 descriptor.resize(static_cast<size_t>(written));
627
628 TabletLayout layout = parse_descriptor(descriptor);
629 if (layout.fields.empty()) {
630 hid_close(handle);
631 return false;
632 }
633
634 hid_set_nonblocking(handle, 0);
635
636 auto device = std::make_shared<TabletDevice>();
637 device->handle = handle;
638 device->path = path;
639 device->name = std::move(name);
640 device->vendor_id = vid;
641 device->product_id = pid;
642 device->layout = std::move(layout);
643 device->read_buffer.resize(m_config.read_buffer_size);
644 device->active.store(true);
645
646 std::vector<TabletToolInfo> announced;
647
648 {
649 std::lock_guard lock(m_devices_mutex);
650
651 TabletToolInfo pen;
652 pen.id = m_next_device_id++;
654 pen.is_connected = true;
655 pen.is_input = true;
656 pen.vendor_id = vid;
657 pen.product_id = pid;
658 pen.tablet_name = device->name;
659 pen.name = device->name + " Pen";
661 pen.axes = device->layout.axes;
662
663 device->pen_id = pen.id;
664 m_tools[pen.id] = pen;
665 m_tool_paths[pen.id] = path;
666 announced.push_back(pen);
667
668 if (m_config.split_eraser && device->layout.has_invert) {
669 TabletToolInfo eraser = pen;
670 eraser.id = m_next_device_id++;
671 eraser.name = device->name + " Eraser";
673
674 device->eraser_id = eraser.id;
675 m_tools[eraser.id] = eraser;
676 m_tool_paths[eraser.id] = path;
677 announced.push_back(eraser);
678 } else {
679 device->eraser_id = device->pen_id;
680 }
681
682 m_devices[path] = device;
683 }
684
686 "Tablet adopted: {} (VID:{:04X} PID:{:04X}, {} field(s))",
687 device->name, vid, pid, device->layout.fields.size());
688
689 for (const auto& info : announced) {
690 notify_device_change(info, true);
691 }
692
693 return true;
694}
695
696// =============================================================================
697// Polling
698// =============================================================================
699
701{
702 while (!m_stop_requested.load()) {
703 std::vector<std::shared_ptr<TabletDevice>> snapshot;
704
705 {
706 std::lock_guard lock(m_devices_mutex);
707 snapshot.reserve(m_devices.size());
708 for (auto& [path, device] : m_devices) {
709 if (device->active.load() && device->handle)
710 snapshot.push_back(device);
711 }
712 }
713
714 if (snapshot.empty()) {
715 std::this_thread::sleep_for(std::chrono::milliseconds(20));
716 continue;
717 }
718
719 for (auto& device : snapshot) {
721 }
722 }
723}
724
726{
727 const int bytes = hid_read_timeout(device.handle,
728 device.read_buffer.data(), device.read_buffer.size(),
730
731 if (bytes > 0) {
733 std::span<const uint8_t>(device.read_buffer.data(),
734 static_cast<size_t>(bytes)));
735 return;
736 }
737
738 if (bytes < 0) {
740 "Tablet read error on {}: {}", device.name,
741 hid_failure_reason(device.handle));
742 device.active.store(false);
743 }
744}
745
746// =============================================================================
747// Report unpacking
748// =============================================================================
749
750void TabletBackend::unpack_report(TabletDevice& device, std::span<const uint8_t> report)
751{
752 if (report.empty())
753 return;
754
755 uint8_t report_id = 0;
756 std::span<const uint8_t> body = report;
757
758 if (device.layout.uses_report_ids) {
759 report_id = report[0];
760 body = report.subspan(1);
761 }
762
763 bool matched = false;
764 bool eraser_flag = false;
765 bool invert_seen = false;
766
767 for (const auto& field : device.layout.fields) {
768 if (field.report_id != report_id)
769 continue;
770
771 matched = true;
772 const int32_t raw = extract_field(body, field);
773
774 switch (field.slot) {
775 case TabletFrame::X:
776 case TabletFrame::Y:
779 device.slots[field.slot] = normalise(raw, field.logical_min, field.logical_max);
780 break;
781
784 device.slots[field.slot] = to_degrees(raw, field.logical_min, field.logical_max, 90.0);
785 break;
786
788 device.slots[field.slot] = to_degrees(raw, field.logical_min, field.logical_max, 180.0);
789 break;
790
792 device.slots[field.slot] = normalise(raw, field.logical_min, field.logical_max) * 2.0 - 1.0;
793 break;
794
796 device.slots[TabletFrame::WHEEL] = static_cast<double>(raw);
797 device.slots[TabletFrame::WHEEL_CLICKS] = static_cast<double>(raw);
798 break;
799
800 case k_pseudo_in_range:
801 set_state_bit(device.slots, TabletState::IN_PROXIMITY, raw != 0);
802 break;
803
804 case k_pseudo_tip:
805 set_state_bit(device.slots, TabletState::IN_CONTACT, raw != 0);
806 break;
807
808 case k_pseudo_invert:
809 invert_seen = true;
810 eraser_flag = eraser_flag || (raw != 0);
811 break;
812
813 case k_pseudo_eraser:
814 eraser_flag = eraser_flag || (raw != 0);
815 set_button_bit(device.slots, 2, raw != 0);
816 break;
817
818 case k_pseudo_barrel:
819 set_button_bit(device.slots, 0, raw != 0);
820 break;
821
822 case k_pseudo_barrel_2:
823 set_button_bit(device.slots, 1, raw != 0);
824 break;
825
826 case k_pseudo_serial: {
827 std::lock_guard lock(m_devices_mutex);
828 auto it = m_tools.find(device.pen_id);
829 if (it != m_tools.end()) {
830 it->second.hardware_serial = static_cast<uint64_t>(
831 static_cast<uint32_t>(raw));
832 }
833 break;
834 }
835
836 default:
837 break;
838 }
839 }
840
841 if (!matched)
842 return;
843
844 if (invert_seen)
845 device.inverted = eraser_flag;
846
848}
849
851{
852 if (!m_running.load())
853 return;
854
857 value.data = std::vector<double>(device.slots.begin(), device.slots.end());
858 value.timestamp_ns = static_cast<uint64_t>(
859 std::chrono::steady_clock::now().time_since_epoch().count());
860 value.device_id = device.inverted ? device.eraser_id : device.pen_id;
861 value.source_type = InputType::TABLET;
862
864
865 device.slots[TabletFrame::WHEEL] = 0.0;
867}
868
869// =============================================================================
870// Queries
871// =============================================================================
872
873std::vector<InputDeviceInfo> TabletBackend::get_devices() const
874{
875 std::lock_guard lock(m_devices_mutex);
876
877 std::vector<InputDeviceInfo> result;
878 result.reserve(m_tools.size());
879 for (const auto& [id, info] : m_tools) {
880 result.push_back(info);
881 }
882 return result;
883}
884
885bool TabletBackend::open_device(uint32_t device_id)
886{
887 std::lock_guard lock(m_devices_mutex);
888 if (m_tools.find(device_id) == m_tools.end())
889 return false;
890
891 auto path_it = m_tool_paths.find(device_id);
892 if (path_it != m_tool_paths.end()) {
893 auto dev_it = m_devices.find(path_it->second);
894 if (dev_it != m_devices.end())
895 dev_it->second->active.store(true);
896 }
897 return true;
898}
899
900void TabletBackend::close_device(uint32_t device_id)
901{
902 std::shared_ptr<TabletDevice> device;
903 std::string path;
904
905 {
906 std::lock_guard lock(m_devices_mutex);
907 auto path_it = m_tool_paths.find(device_id);
908 if (path_it == m_tool_paths.end())
909 return;
910 path = path_it->second;
911
912 auto dev_it = m_devices.find(path);
913 if (dev_it == m_devices.end())
914 return;
915 device = dev_it->second;
916 }
917
918 device->active.store(false);
919}
920
921bool TabletBackend::is_device_open(uint32_t device_id) const
922{
923 std::lock_guard lock(m_devices_mutex);
924 auto it = m_tool_paths.find(device_id);
925 if (it == m_tool_paths.end())
926 return false;
927 auto dev = m_devices.find(it->second);
928 return dev != m_devices.end() && dev->second->active.load();
929}
930
931std::vector<uint32_t> TabletBackend::get_open_devices() const
932{
933 std::lock_guard lock(m_devices_mutex);
934
935 std::vector<uint32_t> result;
936 result.reserve(m_tools.size());
937 for (const auto& [id, info] : m_tools) {
938 result.push_back(id);
939 }
940 return result;
941}
942
943std::optional<TabletToolInfo> TabletBackend::get_tool_info(uint32_t device_id) const
944{
945 std::lock_guard lock(m_devices_mutex);
946 auto it = m_tools.find(device_id);
947 if (it == m_tools.end())
948 return std::nullopt;
949 return it->second;
950}
951
952std::optional<TabletLayout> TabletBackend::get_layout(uint32_t device_id) const
953{
954 std::lock_guard lock(m_devices_mutex);
955
956 auto path_it = m_tool_paths.find(device_id);
957 if (path_it == m_tool_paths.end())
958 return std::nullopt;
959
960 auto dev_it = m_devices.find(path_it->second);
961 if (dev_it == m_devices.end())
962 return std::nullopt;
963
964 return dev_it->second->layout;
965}
966
967std::string TabletBackend::get_version() const
968{
969 const hid_api_version* ver = hid_version();
970 if (!ver)
971 return "HIDAPI unknown";
972 return "HIDAPI " + std::to_string(ver->major) + "."
973 + std::to_string(ver->minor) + "." + std::to_string(ver->patch);
974}
975
976// =============================================================================
977// Callbacks
978// =============================================================================
979
981{
982 std::lock_guard lock(m_callback_mutex);
983 m_input_callback = std::move(callback);
984}
985
991
993{
994 std::lock_guard lock(m_callback_mutex);
995 if (m_input_callback) {
997 }
998}
999
1001{
1002 std::lock_guard lock(m_callback_mutex);
1003 if (m_device_callback) {
1004 m_device_callback(info, connected);
1005 }
1006}
1007
1008} // namespace MayaFlux::Core
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
uint64_t payload
Definition Composite.cpp:18
bool size
glm::vec2 current
hid_device_ hid_device
Definition HIDBackend.hpp:5
PickCallback callback
Definition Picker.cpp:22
uint16_t usage
int32_t logical_min
uint32_t report_count
uint16_t usage_page
uint32_t report_size
uint8_t report_id
uint16_t page
int32_t logical_max
glm::vec3 value
vk::PhysicalDevice device
Definition VKDevice.cpp:121
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
std::string name
Definition VKDevice.cpp:123
uint32_t index
Definition VKDevice.cpp:122
void stop() override
Stop listening for input events.
std::string get_version() const override
Get backend version string.
std::atomic< bool > m_initialized
bool is_device_open(uint32_t device_id) const override
Check if a device is currently open.
void shutdown() override
Shutdown and release all resources.
std::atomic< bool > m_stop_requested
std::optional< TabletLayout > get_layout(uint32_t device_id) const
Parsed report layout for the device backing a tool.
void set_input_callback(InputCallback callback) override
Register callback for input values.
void notify_device_change(const InputDeviceInfo &info, bool connected)
std::optional< TabletToolInfo > get_tool_info(uint32_t device_id) const
Extended information for a tool.
void set_device_callback(DeviceCallback callback) override
Register callback for device connect/disconnect events.
std::vector< uint32_t > get_open_devices() const override
Get list of currently open device IDs.
static TabletLayout parse_descriptor(std::span< const uint8_t > descriptor)
Parse a HID report descriptor into a tablet layout.
void poll_device(TabletDevice &device)
void notify_input(const InputValue &value)
bool open_device(uint32_t device_id) override
Open a device for input.
bool initialize() override
Initialize the input backend.
std::unordered_map< uint32_t, std::string > m_tool_paths
void emit_frame(TabletDevice &device)
size_t refresh_devices() override
Refresh the device list.
void start() override
Start listening for input events.
std::vector< InputDeviceInfo > get_devices() const override
Get list of available devices.
void unpack_report(TabletDevice &device, std::span< const uint8_t > report)
std::unordered_map< std::string, std::shared_ptr< TabletDevice > > m_devices
std::unordered_map< uint32_t, TabletToolInfo > m_tools
void close_device(uint32_t device_id) override
Close a previously opened device.
bool adopt_device(const std::string &path, uint16_t vid, uint16_t pid, std::string name)
Cross-platform tablet and stylus backend over raw HID.
@ TABLET
Digitizers and styluses (pressure, tilt, rotation)
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.
TabletState
State bits packed into the STATE slot.
MAYAFLUX_API std::string wide_to_utf8(const wchar_t *ws)
Encode a null-terminated wide string as UTF-8.
TabletAxes
Axes a tool actually reports.
@ InputBackend
Input device backend (HID, MIDI, OSC)
@ Core
Core engine, backend, subsystems.
bool is_connected
Current connection state.
std::string name
Human-readable device name.
uint16_t product_id
USB Product ID.
uint32_t id
Unique device identifier within backend.
bool is_input
Can receive MIDI.
uint16_t vendor_id
USB Vendor ID.
InputType backend_type
Which backend manages this device.
Information about a connected input device.
@ VECTOR
Multiple float values (e.g., accelerometer xyz)
Generic input value container.
bool split_eraser
Report the eraser end as its own tool.
int poll_timeout_ms
Timeout for hid_read_timeout.
size_t read_buffer_size
Per-device read buffer.
bool probe_all_devices
Read every HID descriptor rather than trusting enumeration.
Configuration for the tablet backend.
One physical HID device and the tools it presents.
size_t slot
SLOT_COUNT means not a slot field.
One field located in a report by descriptor parsing.
@ DISTANCE
0.0 to 1.0, hover height
@ ROTATION
-180.0 to 180.0 degrees
@ X
0.0 to 1.0 across the active area
@ WHEEL_CLICKS
Integral detents this sample.
@ STATE
TabletState bitmask.
@ TILT_Y
-90.0 to 90.0 degrees
@ WHEEL
Degrees of wheel rotation this sample.
@ SLIDER
-1.0 to 1.0, barrel pressure or finger wheel
@ Y
0.0 to 1.0 across the active area
@ TILT_X
-90.0 to 90.0 degrees
@ BUTTONS
Bit 0 barrel, bit 1 secondary barrel, bit 2 eraser.
static constexpr size_t SLOT_COUNT
std::vector< TabletField > fields
Parsed layout of one device's input reports.
std::string tablet_name
Parent device, shared by pen and eraser.
Extended information for a tablet tool.