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