@moldable-ai/ui 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/chat/chat-panel.d.ts +18 -12
- package/dist/components/chat/chat-panel.d.ts.map +1 -1
- package/dist/components/chat/chat-panel.js +73 -19
- package/dist/components/chat/index.d.ts +2 -1
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +2 -1
- package/dist/components/chat/model-effort-power-effects.d.ts +7 -0
- package/dist/components/chat/model-effort-power-effects.d.ts.map +1 -0
- package/dist/components/chat/model-effort-power-effects.js +54 -0
- package/dist/components/chat/model-effort-selector.d.ts +26 -0
- package/dist/components/chat/model-effort-selector.d.ts.map +1 -0
- package/dist/components/chat/model-effort-selector.js +179 -0
- package/dist/components/chat/model-effort-ultra-effects.d.ts +5 -0
- package/dist/components/chat/model-effort-ultra-effects.d.ts.map +1 -0
- package/dist/components/chat/model-effort-ultra-effects.js +56 -0
- package/dist/components/hardware/bluetooth-panel-helpers.d.ts +9 -0
- package/dist/components/hardware/bluetooth-panel-helpers.d.ts.map +1 -0
- package/dist/components/hardware/bluetooth-panel-helpers.js +37 -0
- package/dist/components/hardware/bluetooth-panel.d.ts +56 -0
- package/dist/components/hardware/bluetooth-panel.d.ts.map +1 -0
- package/dist/components/hardware/bluetooth-panel.js +470 -0
- package/dist/components/hardware/camera-preview.d.ts +39 -0
- package/dist/components/hardware/camera-preview.d.ts.map +1 -0
- package/dist/components/hardware/camera-preview.js +228 -0
- package/dist/components/hardware/capability-badge.d.ts +23 -0
- package/dist/components/hardware/capability-badge.d.ts.map +1 -0
- package/dist/components/hardware/capability-badge.js +39 -0
- package/dist/components/hardware/capability-matrix.d.ts +14 -0
- package/dist/components/hardware/capability-matrix.d.ts.map +1 -0
- package/dist/components/hardware/capability-matrix.js +97 -0
- package/dist/components/hardware/clipboard-button.d.ts +26 -0
- package/dist/components/hardware/clipboard-button.d.ts.map +1 -0
- package/dist/components/hardware/clipboard-button.js +114 -0
- package/dist/components/hardware/device-io-controls.d.ts +4 -0
- package/dist/components/hardware/device-io-controls.d.ts.map +1 -0
- package/dist/components/hardware/device-io-controls.js +28 -0
- package/dist/components/hardware/device-list.d.ts +20 -0
- package/dist/components/hardware/device-list.d.ts.map +1 -0
- package/dist/components/hardware/device-list.js +14 -0
- package/dist/components/hardware/display-map.d.ts +23 -0
- package/dist/components/hardware/display-map.d.ts.map +1 -0
- package/dist/components/hardware/display-map.js +114 -0
- package/dist/components/hardware/haptic-button.d.ts +26 -0
- package/dist/components/hardware/haptic-button.d.ts.map +1 -0
- package/dist/components/hardware/haptic-button.js +94 -0
- package/dist/components/hardware/hardware-panel.d.ts +11 -0
- package/dist/components/hardware/hardware-panel.d.ts.map +1 -0
- package/dist/components/hardware/hardware-panel.js +28 -0
- package/dist/components/hardware/hid-panel.d.ts +40 -0
- package/dist/components/hardware/hid-panel.d.ts.map +1 -0
- package/dist/components/hardware/hid-panel.js +373 -0
- package/dist/components/hardware/index.d.ts +27 -0
- package/dist/components/hardware/index.d.ts.map +1 -0
- package/dist/components/hardware/index.js +26 -0
- package/dist/components/hardware/live-indicator.d.ts +15 -0
- package/dist/components/hardware/live-indicator.d.ts.map +1 -0
- package/dist/components/hardware/live-indicator.js +26 -0
- package/dist/components/hardware/local-auth-button.d.ts +33 -0
- package/dist/components/hardware/local-auth-button.d.ts.map +1 -0
- package/dist/components/hardware/local-auth-button.js +115 -0
- package/dist/components/hardware/location-panel.d.ts +29 -0
- package/dist/components/hardware/location-panel.d.ts.map +1 -0
- package/dist/components/hardware/location-panel.js +154 -0
- package/dist/components/hardware/microphone-meter.d.ts +35 -0
- package/dist/components/hardware/microphone-meter.d.ts.map +1 -0
- package/dist/components/hardware/microphone-meter.js +244 -0
- package/dist/components/hardware/midi-monitor.d.ts +54 -0
- package/dist/components/hardware/midi-monitor.d.ts.map +1 -0
- package/dist/components/hardware/midi-monitor.js +534 -0
- package/dist/components/hardware/notification-panel.d.ts +24 -0
- package/dist/components/hardware/notification-panel.d.ts.map +1 -0
- package/dist/components/hardware/notification-panel.js +124 -0
- package/dist/components/hardware/power-blocker-controller.d.ts +19 -0
- package/dist/components/hardware/power-blocker-controller.d.ts.map +1 -0
- package/dist/components/hardware/power-blocker-controller.js +83 -0
- package/dist/components/hardware/power-panel.d.ts +67 -0
- package/dist/components/hardware/power-panel.d.ts.map +1 -0
- package/dist/components/hardware/power-panel.js +309 -0
- package/dist/components/hardware/readout.d.ts +10 -0
- package/dist/components/hardware/readout.d.ts.map +1 -0
- package/dist/components/hardware/readout.js +11 -0
- package/dist/components/hardware/screen-share-preview.d.ts +36 -0
- package/dist/components/hardware/screen-share-preview.d.ts.map +1 -0
- package/dist/components/hardware/screen-share-preview.js +191 -0
- package/dist/components/hardware/secure-storage-panel.d.ts +36 -0
- package/dist/components/hardware/secure-storage-panel.d.ts.map +1 -0
- package/dist/components/hardware/secure-storage-panel.js +193 -0
- package/dist/components/hardware/serial-console.d.ts +64 -0
- package/dist/components/hardware/serial-console.d.ts.map +1 -0
- package/dist/components/hardware/serial-console.js +399 -0
- package/dist/components/hardware/shortcut-recorder.d.ts +35 -0
- package/dist/components/hardware/shortcut-recorder.d.ts.map +1 -0
- package/dist/components/hardware/shortcut-recorder.js +217 -0
- package/dist/components/hardware/stream-log.d.ts +35 -0
- package/dist/components/hardware/stream-log.d.ts.map +1 -0
- package/dist/components/hardware/stream-log.js +65 -0
- package/dist/components/hardware/system-audio-monitor.d.ts +34 -0
- package/dist/components/hardware/system-audio-monitor.d.ts.map +1 -0
- package/dist/components/hardware/system-audio-monitor.js +275 -0
- package/dist/components/hardware/usb-panel.d.ts +54 -0
- package/dist/components/hardware/usb-panel.d.ts.map +1 -0
- package/dist/components/hardware/usb-panel.js +363 -0
- package/dist/components/hardware/use-capability.d.ts +20 -0
- package/dist/components/hardware/use-capability.d.ts.map +1 -0
- package/dist/components/hardware/use-capability.js +61 -0
- package/dist/components/ui/input-group.d.ts +1 -1
- package/dist/components/ui/slider.d.ts +4 -1
- package/dist/components/ui/slider.d.ts.map +1 -1
- package/dist/components/ui/slider.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/native-hardware/bluetooth-access.d.ts +14 -0
- package/dist/lib/native-hardware/bluetooth-access.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-access.js +40 -0
- package/dist/lib/native-hardware/bluetooth-connection.d.ts +19 -0
- package/dist/lib/native-hardware/bluetooth-connection.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-connection.js +88 -0
- package/dist/lib/native-hardware/bluetooth-gatt.d.ts +7 -0
- package/dist/lib/native-hardware/bluetooth-gatt.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-gatt.js +77 -0
- package/dist/lib/native-hardware/bluetooth-internal.d.ts +10 -0
- package/dist/lib/native-hardware/bluetooth-internal.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-internal.js +105 -0
- package/dist/lib/native-hardware/bluetooth-native-fallback.d.ts +33 -0
- package/dist/lib/native-hardware/bluetooth-native-fallback.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-native-fallback.js +214 -0
- package/dist/lib/native-hardware/bluetooth-types.d.ts +85 -0
- package/dist/lib/native-hardware/bluetooth-types.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth-types.js +2 -0
- package/dist/lib/native-hardware/bluetooth.d.ts +7 -0
- package/dist/lib/native-hardware/bluetooth.d.ts.map +1 -0
- package/dist/lib/native-hardware/bluetooth.js +4 -0
- package/dist/lib/native-hardware/bridge.d.ts +23 -0
- package/dist/lib/native-hardware/bridge.d.ts.map +1 -0
- package/dist/lib/native-hardware/bridge.js +266 -0
- package/dist/lib/native-hardware/capabilities.d.ts +7 -0
- package/dist/lib/native-hardware/capabilities.d.ts.map +1 -0
- package/dist/lib/native-hardware/capabilities.js +95 -0
- package/dist/lib/native-hardware/clipboard.d.ts +11 -0
- package/dist/lib/native-hardware/clipboard.d.ts.map +1 -0
- package/dist/lib/native-hardware/clipboard.js +55 -0
- package/dist/lib/native-hardware/device-io-errors.d.ts +11 -0
- package/dist/lib/native-hardware/device-io-errors.d.ts.map +1 -0
- package/dist/lib/native-hardware/device-io-errors.js +82 -0
- package/dist/lib/native-hardware/displays.d.ts +38 -0
- package/dist/lib/native-hardware/displays.d.ts.map +1 -0
- package/dist/lib/native-hardware/displays.js +12 -0
- package/dist/lib/native-hardware/global-shortcuts.d.ts +86 -0
- package/dist/lib/native-hardware/global-shortcuts.d.ts.map +1 -0
- package/dist/lib/native-hardware/global-shortcuts.js +351 -0
- package/dist/lib/native-hardware/haptics.d.ts +25 -0
- package/dist/lib/native-hardware/haptics.d.ts.map +1 -0
- package/dist/lib/native-hardware/haptics.js +18 -0
- package/dist/lib/native-hardware/hid-native.d.ts +26 -0
- package/dist/lib/native-hardware/hid-native.d.ts.map +1 -0
- package/dist/lib/native-hardware/hid-native.js +229 -0
- package/dist/lib/native-hardware/hid.d.ts +52 -0
- package/dist/lib/native-hardware/hid.d.ts.map +1 -0
- package/dist/lib/native-hardware/hid.js +160 -0
- package/dist/lib/native-hardware/index.d.ts +38 -0
- package/dist/lib/native-hardware/index.d.ts.map +1 -0
- package/dist/lib/native-hardware/index.js +20 -0
- package/dist/lib/native-hardware/local-authentication.d.ts +32 -0
- package/dist/lib/native-hardware/local-authentication.d.ts.map +1 -0
- package/dist/lib/native-hardware/local-authentication.js +46 -0
- package/dist/lib/native-hardware/location.d.ts +49 -0
- package/dist/lib/native-hardware/location.d.ts.map +1 -0
- package/dist/lib/native-hardware/location.js +114 -0
- package/dist/lib/native-hardware/media-permissions.d.ts +28 -0
- package/dist/lib/native-hardware/media-permissions.d.ts.map +1 -0
- package/dist/lib/native-hardware/media-permissions.js +37 -0
- package/dist/lib/native-hardware/media.d.ts +17 -0
- package/dist/lib/native-hardware/media.d.ts.map +1 -0
- package/dist/lib/native-hardware/media.js +99 -0
- package/dist/lib/native-hardware/midi-access.d.ts +13 -0
- package/dist/lib/native-hardware/midi-access.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi-access.js +66 -0
- package/dist/lib/native-hardware/midi-internal.d.ts +12 -0
- package/dist/lib/native-hardware/midi-internal.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi-internal.js +88 -0
- package/dist/lib/native-hardware/midi-native-fallback.d.ts +27 -0
- package/dist/lib/native-hardware/midi-native-fallback.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi-native-fallback.js +127 -0
- package/dist/lib/native-hardware/midi-ports.d.ts +6 -0
- package/dist/lib/native-hardware/midi-ports.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi-ports.js +115 -0
- package/dist/lib/native-hardware/midi-types.d.ts +25 -0
- package/dist/lib/native-hardware/midi-types.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi-types.js +1 -0
- package/dist/lib/native-hardware/midi.d.ts +6 -0
- package/dist/lib/native-hardware/midi.d.ts.map +1 -0
- package/dist/lib/native-hardware/midi.js +3 -0
- package/dist/lib/native-hardware/native-device-io.d.ts +28 -0
- package/dist/lib/native-hardware/native-device-io.d.ts.map +1 -0
- package/dist/lib/native-hardware/native-device-io.js +91 -0
- package/dist/lib/native-hardware/native-device-lifecycle.d.ts +13 -0
- package/dist/lib/native-hardware/native-device-lifecycle.d.ts.map +1 -0
- package/dist/lib/native-hardware/native-device-lifecycle.js +40 -0
- package/dist/lib/native-hardware/notifications.d.ts +38 -0
- package/dist/lib/native-hardware/notifications.d.ts.map +1 -0
- package/dist/lib/native-hardware/notifications.js +50 -0
- package/dist/lib/native-hardware/power-session.d.ts +113 -0
- package/dist/lib/native-hardware/power-session.d.ts.map +1 -0
- package/dist/lib/native-hardware/power-session.js +264 -0
- package/dist/lib/native-hardware/secure-storage.d.ts +36 -0
- package/dist/lib/native-hardware/secure-storage.d.ts.map +1 -0
- package/dist/lib/native-hardware/secure-storage.js +64 -0
- package/dist/lib/native-hardware/serial-native.d.ts +23 -0
- package/dist/lib/native-hardware/serial-native.d.ts.map +1 -0
- package/dist/lib/native-hardware/serial-native.js +181 -0
- package/dist/lib/native-hardware/serial.d.ts +76 -0
- package/dist/lib/native-hardware/serial.d.ts.map +1 -0
- package/dist/lib/native-hardware/serial.js +212 -0
- package/dist/lib/native-hardware/system-audio.d.ts +127 -0
- package/dist/lib/native-hardware/system-audio.d.ts.map +1 -0
- package/dist/lib/native-hardware/system-audio.js +330 -0
- package/dist/lib/native-hardware/types.d.ts +62 -0
- package/dist/lib/native-hardware/types.d.ts.map +1 -0
- package/dist/lib/native-hardware/types.js +27 -0
- package/dist/lib/native-hardware/usb-native.d.ts +39 -0
- package/dist/lib/native-hardware/usb-native.d.ts.map +1 -0
- package/dist/lib/native-hardware/usb-native.js +217 -0
- package/dist/lib/native-hardware/usb.d.ts +85 -0
- package/dist/lib/native-hardware/usb.d.ts.map +1 -0
- package/dist/lib/native-hardware/usb.js +222 -0
- package/package.json +1 -1
- package/src/styles/index.css +377 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const TRACK_PARTICLES = [
|
|
3
|
+
[6, 72, 3, -4, 2.1, -0.8, 0.72],
|
|
4
|
+
[9, 35, -2, 4, 2.8, -1.9, 0.48],
|
|
5
|
+
[18, 58, 4, -3, 1.9, -0.4, 0.82],
|
|
6
|
+
[29, 26, -3, 5, 3.1, -2.4, 0.56],
|
|
7
|
+
[38, 70, 2, -5, 2.5, -1.1, 0.68],
|
|
8
|
+
[46, 43, -4, 3, 2.9, -2.2, 0.9],
|
|
9
|
+
[55, 72, 3, -4, 2.2, -1.6, 0.58],
|
|
10
|
+
[63, 31, -2, 5, 3.2, -0.7, 0.72],
|
|
11
|
+
[71, 61, 4, -3, 2.4, -1.8, 0.48],
|
|
12
|
+
[78, 27, -3, 4, 2.7, -2.5, 0.86],
|
|
13
|
+
[84, 70, 2, -5, 2, -0.6, 0.62],
|
|
14
|
+
[89, 43, -4, 3, 3, -2.1, 0.9],
|
|
15
|
+
[93, 67, 3, -4, 2.3, -1.3, 0.7],
|
|
16
|
+
[96, 32, -2, 5, 2.8, -0.2, 0.54],
|
|
17
|
+
];
|
|
18
|
+
const BURST_PARTICLES = [
|
|
19
|
+
[-32, -3],
|
|
20
|
+
[-28, -17],
|
|
21
|
+
[-19, -29],
|
|
22
|
+
[-5, -34],
|
|
23
|
+
[10, -32],
|
|
24
|
+
[24, -24],
|
|
25
|
+
[32, -11],
|
|
26
|
+
[35, 4],
|
|
27
|
+
[29, 19],
|
|
28
|
+
[18, 30],
|
|
29
|
+
[3, 35],
|
|
30
|
+
[-12, 32],
|
|
31
|
+
[-25, 24],
|
|
32
|
+
[-34, 11],
|
|
33
|
+
[20, -7],
|
|
34
|
+
[-16, 9],
|
|
35
|
+
];
|
|
36
|
+
export function UltraRangeEffects({ reveal }) {
|
|
37
|
+
return (_jsxs("span", { "aria-hidden": "true", className: "model-effort-ultra-fill", "data-reveal": reveal, children: [_jsx("span", { className: "model-effort-ultra-field" }), _jsx("span", { className: "model-effort-ultra-particles", children: TRACK_PARTICLES.map(([left, top, driftX, driftY, duration, delay, opacity], index) => (_jsx("span", { className: "model-effort-ultra-particle", style: {
|
|
38
|
+
left: `${left}%`,
|
|
39
|
+
top: `${top}%`,
|
|
40
|
+
'--particle-drift-x': `${driftX}px`,
|
|
41
|
+
'--particle-drift-y': `${driftY}px`,
|
|
42
|
+
'--particle-opacity': opacity,
|
|
43
|
+
animationDuration: `${duration}s`,
|
|
44
|
+
animationDelay: `${delay}s`,
|
|
45
|
+
} }, index))) })] }));
|
|
46
|
+
}
|
|
47
|
+
export function UltraThumbBurst() {
|
|
48
|
+
return (_jsx("span", { "aria-hidden": "true", className: "model-effort-ultra-burst", children: BURST_PARTICLES.map(([x, y], index) => (_jsx("span", { className: "model-effort-ultra-burst-particle", style: {
|
|
49
|
+
'--burst-x': `${x}px`,
|
|
50
|
+
'--burst-y': `${y}px`,
|
|
51
|
+
animationDelay: `${(index % 4) * 12}ms`,
|
|
52
|
+
backgroundColor: index % 3 === 0
|
|
53
|
+
? 'var(--model-effort-blue)'
|
|
54
|
+
: 'var(--model-effort-purple)',
|
|
55
|
+
} }, index))) }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type BluetoothSubscriptionCleanup = () => Promise<void>;
|
|
2
|
+
/**
|
|
3
|
+
* Stops every active observer before returning. Successful entries are removed,
|
|
4
|
+
* while failed entries stay registered so the caller can retry cleanup.
|
|
5
|
+
*/
|
|
6
|
+
export declare function cleanupBluetoothSubscriptions(subscriptions: Map<string, BluetoothSubscriptionCleanup>): Promise<void>;
|
|
7
|
+
/** Parses a compact or separator-delimited hexadecimal Bluetooth value. */
|
|
8
|
+
export declare function parseBluetoothHexBytes(value: string): Uint8Array<ArrayBuffer>;
|
|
9
|
+
//# sourceMappingURL=bluetooth-panel-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bluetooth-panel-helpers.d.ts","sourceRoot":"","sources":["../../../src/components/hardware/bluetooth-panel-helpers.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,4BAA4B,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;AAE9D;;;GAGG;AACH,wBAAsB,6BAA6B,CACjD,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,4BAA4B,CAAC,GACvD,OAAO,CAAC,IAAI,CAAC,CAef;AAED,2EAA2E;AAC3E,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,CAqB7E"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stops every active observer before returning. Successful entries are removed,
|
|
3
|
+
* while failed entries stay registered so the caller can retry cleanup.
|
|
4
|
+
*/
|
|
5
|
+
export async function cleanupBluetoothSubscriptions(subscriptions) {
|
|
6
|
+
const failures = [];
|
|
7
|
+
await Promise.all([...subscriptions].map(async ([key, stop]) => {
|
|
8
|
+
try {
|
|
9
|
+
await stop();
|
|
10
|
+
if (subscriptions.get(key) === stop)
|
|
11
|
+
subscriptions.delete(key);
|
|
12
|
+
}
|
|
13
|
+
catch (cause) {
|
|
14
|
+
failures.push(cause);
|
|
15
|
+
}
|
|
16
|
+
}));
|
|
17
|
+
if (failures.length > 0)
|
|
18
|
+
throw failures[0];
|
|
19
|
+
}
|
|
20
|
+
/** Parses a compact or separator-delimited hexadecimal Bluetooth value. */
|
|
21
|
+
export function parseBluetoothHexBytes(value) {
|
|
22
|
+
const normalized = value
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/0x/gi, '')
|
|
25
|
+
.replace(/[\s,:-]+/g, '');
|
|
26
|
+
if (normalized.length === 0) {
|
|
27
|
+
throw new TypeError('Enter at least one hexadecimal byte.');
|
|
28
|
+
}
|
|
29
|
+
if (normalized.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(normalized)) {
|
|
30
|
+
throw new TypeError('Use complete hexadecimal bytes, such as 01 A0 FF.');
|
|
31
|
+
}
|
|
32
|
+
const bytes = new Uint8Array(new ArrayBuffer(normalized.length / 2));
|
|
33
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
34
|
+
bytes[index] = Number.parseInt(normalized.slice(index * 2, index * 2 + 2), 16);
|
|
35
|
+
}
|
|
36
|
+
return bytes;
|
|
37
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type MoldableBluetoothConnection, type MoldableBluetoothDevice, type MoldableBluetoothRemoteGATTCharacteristic, type MoldableNativeBluetoothDevice } from '../../lib/native-hardware';
|
|
3
|
+
export type MoldableBluetoothStatus = 'idle' | 'requesting' | 'connecting' | 'connected' | 'error';
|
|
4
|
+
export interface MoldableBluetoothCharacteristicInfo {
|
|
5
|
+
serviceUuid: string;
|
|
6
|
+
uuid: string;
|
|
7
|
+
/** Property names when the runtime exposes them; empty when unknown. */
|
|
8
|
+
properties: string[];
|
|
9
|
+
/** Web Bluetooth handle; null on the desktop bridge (UUIDs drive I/O). */
|
|
10
|
+
characteristic: MoldableBluetoothRemoteGATTCharacteristic | null;
|
|
11
|
+
}
|
|
12
|
+
export interface MoldableBluetoothServiceInfo {
|
|
13
|
+
uuid: string;
|
|
14
|
+
characteristics: MoldableBluetoothCharacteristicInfo[];
|
|
15
|
+
}
|
|
16
|
+
export interface UseMoldableBluetoothOptions {
|
|
17
|
+
onValue?: (characteristic: MoldableBluetoothCharacteristicInfo, value: DataView) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Bluetooth LE state machine behind `BluetoothPanel`. Handles both transports
|
|
21
|
+
* returned by `requestMoldableBluetoothWithFallback`: Web Bluetooth (GATT
|
|
22
|
+
* connection + service discovery) and the desktop bridge (UUID-addressed I/O
|
|
23
|
+
* on an already-connected device).
|
|
24
|
+
*/
|
|
25
|
+
export declare function useMoldableBluetooth(options?: UseMoldableBluetoothOptions): {
|
|
26
|
+
status: MoldableBluetoothStatus;
|
|
27
|
+
device: MoldableBluetoothDevice | MoldableNativeBluetoothDevice | null;
|
|
28
|
+
connection: MoldableBluetoothConnection | null;
|
|
29
|
+
disconnecting: boolean;
|
|
30
|
+
services: MoldableBluetoothServiceInfo[];
|
|
31
|
+
subscribedUuids: string[];
|
|
32
|
+
error: Error | null;
|
|
33
|
+
entries: import("./stream-log").StreamLogEntry[];
|
|
34
|
+
request: (serviceUuid?: string) => Promise<void>;
|
|
35
|
+
connect: () => Promise<void>;
|
|
36
|
+
disconnect: () => Promise<void>;
|
|
37
|
+
read: (characteristic: MoldableBluetoothCharacteristicInfo) => Promise<void>;
|
|
38
|
+
subscribe: (characteristic: MoldableBluetoothCharacteristicInfo) => Promise<void>;
|
|
39
|
+
unsubscribe: (characteristic: MoldableBluetoothCharacteristicInfo) => Promise<void>;
|
|
40
|
+
write: (characteristic: MoldableBluetoothCharacteristicInfo, bytes: BufferSource) => Promise<void>;
|
|
41
|
+
writeHex: (characteristic: MoldableBluetoothCharacteristicInfo, value: string) => Promise<void>;
|
|
42
|
+
clearEntries: () => void;
|
|
43
|
+
};
|
|
44
|
+
export interface BluetoothPanelProps extends React.ComponentProps<'div'> {
|
|
45
|
+
/** Prefill and scope scanning/discovery to one GATT service UUID. */
|
|
46
|
+
serviceUuid?: string;
|
|
47
|
+
onValue?: (characteristic: MoldableBluetoothCharacteristicInfo, value: DataView) => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Bluetooth LE panel: scan (optionally scoped to a service UUID), connect,
|
|
51
|
+
* browse discovered characteristics, and read, write, or subscribe to values
|
|
52
|
+
* with a live hex log.
|
|
53
|
+
*/
|
|
54
|
+
declare function BluetoothPanel({ className, serviceUuid, onValue, ...props }: BluetoothPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
55
|
+
export { BluetoothPanel };
|
|
56
|
+
//# sourceMappingURL=bluetooth-panel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bluetooth-panel.d.ts","sourceRoot":"","sources":["../../../src/components/hardware/bluetooth-panel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EACL,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC5B,KAAK,yCAAyC,EAG9C,KAAK,6BAA6B,EAQnC,MAAM,2BAA2B,CAAA;AAqBlC,MAAM,MAAM,uBAAuB,GAC/B,MAAM,GACN,YAAY,GACZ,YAAY,GACZ,WAAW,GACX,OAAO,CAAA;AAgDX,MAAM,WAAW,mCAAmC;IAClD,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,wEAAwE;IACxE,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,0EAA0E;IAC1E,cAAc,EAAE,yCAAyC,GAAG,IAAI,CAAA;CACjE;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,EAAE,mCAAmC,EAAE,CAAA;CACvD;AA+ED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,CAAC,EAAE,CACR,cAAc,EAAE,mCAAmC,EACnD,KAAK,EAAE,QAAQ,KACZ,IAAI,CAAA;CACV;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,GAAE,2BAAgC;;;;;;;;;4BAkJlB,MAAM;;;2BAiDJ,mCAAmC;gCA0BnC,mCAAmC;kCAqCnC,mCAAmC;4BAqBxC,mCAAmC,SAC5C,YAAY;+BAmCH,mCAAmC,SAC5C,MAAM;;EAsClB;AAED,MAAM,WAAW,mBAAoB,SAAQ,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;IACtE,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,CACR,cAAc,EAAE,mCAAmC,EACnD,KAAK,EAAE,QAAQ,KACZ,IAAI,CAAA;CACV;AAED;;;;GAIG;AACH,iBAAS,cAAc,CAAC,EACtB,SAAS,EACT,WAAW,EACX,OAAO,EACP,GAAG,KAAK,EACT,EAAE,mBAAmB,2CAiPrB;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Binary, Bluetooth } from 'lucide-react';
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { NativeHardwareBridgeError, connectMoldableBluetoothDevice, isMoldableBluetoothSupported, observeMoldableBluetoothNotifications, readMoldableBluetoothValue, requestMoldableBluetoothWithFallback, writeMoldableBluetoothValue, } from '../../lib/native-hardware';
|
|
6
|
+
import { Button } from '../ui/button';
|
|
7
|
+
import { Input } from '../ui/input';
|
|
8
|
+
import { Spinner } from '../ui/spinner';
|
|
9
|
+
import { cleanupBluetoothSubscriptions, parseBluetoothHexBytes, } from './bluetooth-panel-helpers';
|
|
10
|
+
import { CapabilityBadge } from './capability-badge';
|
|
11
|
+
import { DeviceList, DeviceListEmpty, DeviceListItem } from './device-list';
|
|
12
|
+
import { HardwarePanel, HardwarePanelAction, HardwarePanelContent, HardwarePanelHeader, HardwarePanelIcon, HardwarePanelTitle, } from './hardware-panel';
|
|
13
|
+
import { LiveIndicator } from './live-indicator';
|
|
14
|
+
import { StreamLog, useStreamLog } from './stream-log';
|
|
15
|
+
const HEX_DUMP_MAX_BYTES = 32;
|
|
16
|
+
const BLUETOOTH_BASE_UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb';
|
|
17
|
+
function toError(cause) {
|
|
18
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
19
|
+
}
|
|
20
|
+
function isDismissedChooser(cause) {
|
|
21
|
+
return (cause instanceof NativeHardwareBridgeError &&
|
|
22
|
+
(cause.code === 'cancelled' || cause.code === 'aborted'));
|
|
23
|
+
}
|
|
24
|
+
function asBytes(data) {
|
|
25
|
+
return data instanceof ArrayBuffer
|
|
26
|
+
? new Uint8Array(data)
|
|
27
|
+
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
28
|
+
}
|
|
29
|
+
function viewBytes(view) {
|
|
30
|
+
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
31
|
+
}
|
|
32
|
+
function formatHexDump(bytes) {
|
|
33
|
+
if (bytes.byteLength === 0)
|
|
34
|
+
return '(empty)';
|
|
35
|
+
const shown = Array.from(bytes.subarray(0, HEX_DUMP_MAX_BYTES))
|
|
36
|
+
.map((byte) => byte.toString(16).padStart(2, '0').toUpperCase())
|
|
37
|
+
.join(' ');
|
|
38
|
+
const hidden = bytes.byteLength - HEX_DUMP_MAX_BYTES;
|
|
39
|
+
return hidden > 0 ? `${shown} +${hidden} more` : shown;
|
|
40
|
+
}
|
|
41
|
+
/** `0x2A19` for SIG-assigned UUIDs, middle-truncated hex for vendor ones. */
|
|
42
|
+
function shortUuid(uuid) {
|
|
43
|
+
const value = uuid.toLowerCase();
|
|
44
|
+
if (value.startsWith('0000') && value.endsWith(BLUETOOTH_BASE_UUID_SUFFIX)) {
|
|
45
|
+
return `0x${value.slice(4, 8).toUpperCase()}`;
|
|
46
|
+
}
|
|
47
|
+
return value.length > 13 ? `${value.slice(0, 8)}…${value.slice(-4)}` : value;
|
|
48
|
+
}
|
|
49
|
+
function middleTruncate(uuid) {
|
|
50
|
+
return uuid.length > 22 ? `${uuid.slice(0, 13)}…${uuid.slice(-8)}` : uuid;
|
|
51
|
+
}
|
|
52
|
+
function characteristicKey(characteristic) {
|
|
53
|
+
return `${characteristic.serviceUuid}/${characteristic.uuid}`;
|
|
54
|
+
}
|
|
55
|
+
/** Web Bluetooth exposes `properties`; the structural type keeps it optional. */
|
|
56
|
+
function describeProperties(characteristic) {
|
|
57
|
+
const flags = characteristic
|
|
58
|
+
.properties;
|
|
59
|
+
if (!flags)
|
|
60
|
+
return [];
|
|
61
|
+
const names = [];
|
|
62
|
+
if (flags.read)
|
|
63
|
+
names.push('read');
|
|
64
|
+
if (flags.write || flags.writeWithoutResponse)
|
|
65
|
+
names.push('write');
|
|
66
|
+
if (flags.notify)
|
|
67
|
+
names.push('notify');
|
|
68
|
+
if (flags.indicate)
|
|
69
|
+
names.push('indicate');
|
|
70
|
+
return names;
|
|
71
|
+
}
|
|
72
|
+
function requireWebCharacteristic(characteristic) {
|
|
73
|
+
if (!characteristic.characteristic) {
|
|
74
|
+
throw new Error('This characteristic has no Web Bluetooth handle.');
|
|
75
|
+
}
|
|
76
|
+
return characteristic.characteristic;
|
|
77
|
+
}
|
|
78
|
+
function observeGattDisconnect(transport, listener) {
|
|
79
|
+
if (transport.kind === 'web-bluetooth') {
|
|
80
|
+
transport.device.addEventListener('gattserverdisconnected', listener);
|
|
81
|
+
return () => transport.device.removeEventListener('gattserverdisconnected', listener);
|
|
82
|
+
}
|
|
83
|
+
transport.device.addEventListener('gattserverdisconnected', listener);
|
|
84
|
+
return () => transport.device.removeEventListener('gattserverdisconnected', listener);
|
|
85
|
+
}
|
|
86
|
+
async function discoverWebServices(connection, serviceUuid) {
|
|
87
|
+
const services = serviceUuid
|
|
88
|
+
? [await connection.getPrimaryService(serviceUuid)]
|
|
89
|
+
: await connection.server.getPrimaryServices();
|
|
90
|
+
return Promise.all(services.map(async (service) => {
|
|
91
|
+
const characteristics = await service
|
|
92
|
+
.getCharacteristics()
|
|
93
|
+
.catch(() => []);
|
|
94
|
+
return {
|
|
95
|
+
uuid: service.uuid,
|
|
96
|
+
characteristics: characteristics.map((characteristic) => ({
|
|
97
|
+
serviceUuid: service.uuid,
|
|
98
|
+
uuid: characteristic.uuid,
|
|
99
|
+
properties: describeProperties(characteristic),
|
|
100
|
+
characteristic,
|
|
101
|
+
})),
|
|
102
|
+
};
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Bluetooth LE state machine behind `BluetoothPanel`. Handles both transports
|
|
107
|
+
* returned by `requestMoldableBluetoothWithFallback`: Web Bluetooth (GATT
|
|
108
|
+
* connection + service discovery) and the desktop bridge (UUID-addressed I/O
|
|
109
|
+
* on an already-connected device).
|
|
110
|
+
*/
|
|
111
|
+
export function useMoldableBluetooth(options = {}) {
|
|
112
|
+
const [status, setStatus] = useState('idle');
|
|
113
|
+
const [device, setDevice] = useState(null);
|
|
114
|
+
const [connection, setConnection] = useState(null);
|
|
115
|
+
const [services, setServices] = useState([]);
|
|
116
|
+
const [subscribedUuids, setSubscribedUuids] = useState([]);
|
|
117
|
+
const [disconnecting, setDisconnecting] = useState(false);
|
|
118
|
+
const [error, setError] = useState(null);
|
|
119
|
+
const { entries, append, clear } = useStreamLog();
|
|
120
|
+
const generationRef = useRef(0);
|
|
121
|
+
const transportRef = useRef(null);
|
|
122
|
+
const connectionRef = useRef(null);
|
|
123
|
+
const requestedServiceRef = useRef(undefined);
|
|
124
|
+
const subscriptionsRef = useRef(new Map());
|
|
125
|
+
const detachDeviceRef = useRef(null);
|
|
126
|
+
const disconnectingRef = useRef(false);
|
|
127
|
+
const unmountedRef = useRef(false);
|
|
128
|
+
const onValueRef = useRef(options.onValue);
|
|
129
|
+
onValueRef.current = options.onValue;
|
|
130
|
+
const reset = useCallback(async (bestEffort = false) => {
|
|
131
|
+
generationRef.current++;
|
|
132
|
+
const wasDisconnecting = disconnectingRef.current;
|
|
133
|
+
disconnectingRef.current = true;
|
|
134
|
+
try {
|
|
135
|
+
try {
|
|
136
|
+
await cleanupBluetoothSubscriptions(subscriptionsRef.current);
|
|
137
|
+
}
|
|
138
|
+
catch (cause) {
|
|
139
|
+
if (!unmountedRef.current) {
|
|
140
|
+
setSubscribedUuids([...subscriptionsRef.current.keys()]);
|
|
141
|
+
}
|
|
142
|
+
if (!bestEffort)
|
|
143
|
+
throw cause;
|
|
144
|
+
}
|
|
145
|
+
const transport = transportRef.current;
|
|
146
|
+
try {
|
|
147
|
+
if (transport?.kind === 'desktop-bridge') {
|
|
148
|
+
await transport.device.disconnect();
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
connectionRef.current?.disconnect();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (cause) {
|
|
155
|
+
if (!bestEffort)
|
|
156
|
+
throw cause;
|
|
157
|
+
}
|
|
158
|
+
detachDeviceRef.current?.();
|
|
159
|
+
detachDeviceRef.current = null;
|
|
160
|
+
transportRef.current = null;
|
|
161
|
+
connectionRef.current = null;
|
|
162
|
+
requestedServiceRef.current = undefined;
|
|
163
|
+
subscriptionsRef.current.clear();
|
|
164
|
+
if (!unmountedRef.current) {
|
|
165
|
+
setSubscribedUuids([]);
|
|
166
|
+
setDevice(null);
|
|
167
|
+
setConnection(null);
|
|
168
|
+
setServices([]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
disconnectingRef.current = wasDisconnecting;
|
|
173
|
+
}
|
|
174
|
+
}, []);
|
|
175
|
+
const disconnect = useCallback(async () => {
|
|
176
|
+
setDisconnecting(true);
|
|
177
|
+
setError(null);
|
|
178
|
+
try {
|
|
179
|
+
await reset();
|
|
180
|
+
setStatus('idle');
|
|
181
|
+
}
|
|
182
|
+
catch (cause) {
|
|
183
|
+
setError(toError(cause));
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
setDisconnecting(false);
|
|
187
|
+
}
|
|
188
|
+
}, [reset]);
|
|
189
|
+
const handleDisconnected = useCallback(() => {
|
|
190
|
+
if (disconnectingRef.current)
|
|
191
|
+
return;
|
|
192
|
+
void reset(true).then(() => {
|
|
193
|
+
if (unmountedRef.current)
|
|
194
|
+
return;
|
|
195
|
+
setStatus('idle');
|
|
196
|
+
append({ direction: 'info', data: 'Device disconnected' });
|
|
197
|
+
});
|
|
198
|
+
}, [append, reset]);
|
|
199
|
+
const handleDisconnectedRef = useRef(handleDisconnected);
|
|
200
|
+
handleDisconnectedRef.current = handleDisconnected;
|
|
201
|
+
const appendValue = useCallback((characteristic, value) => {
|
|
202
|
+
append({
|
|
203
|
+
direction: 'in',
|
|
204
|
+
data: `${shortUuid(characteristic.uuid)} ${formatHexDump(viewBytes(value))}`,
|
|
205
|
+
});
|
|
206
|
+
onValueRef.current?.(characteristic, value);
|
|
207
|
+
}, [append]);
|
|
208
|
+
const connect = useCallback(async () => {
|
|
209
|
+
const transport = transportRef.current;
|
|
210
|
+
if (!transport)
|
|
211
|
+
return;
|
|
212
|
+
const generation = generationRef.current;
|
|
213
|
+
setStatus('connecting');
|
|
214
|
+
setError(null);
|
|
215
|
+
try {
|
|
216
|
+
let discovered;
|
|
217
|
+
if (transport.kind === 'web-bluetooth') {
|
|
218
|
+
const nextConnection = await connectMoldableBluetoothDevice(transport.device);
|
|
219
|
+
if (generationRef.current !== generation) {
|
|
220
|
+
nextConnection.disconnect();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
connectionRef.current = nextConnection;
|
|
224
|
+
setConnection(nextConnection);
|
|
225
|
+
discovered = await discoverWebServices(nextConnection, requestedServiceRef.current);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
discovered = (await transport.device.getServices()).map((service) => ({
|
|
229
|
+
uuid: service.uuid,
|
|
230
|
+
characteristics: service.characteristicUuids.map((uuid) => ({
|
|
231
|
+
serviceUuid: service.uuid,
|
|
232
|
+
uuid,
|
|
233
|
+
properties: [],
|
|
234
|
+
characteristic: null,
|
|
235
|
+
})),
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
if (generationRef.current !== generation)
|
|
239
|
+
return;
|
|
240
|
+
setServices(discovered);
|
|
241
|
+
setStatus('connected');
|
|
242
|
+
}
|
|
243
|
+
catch (cause) {
|
|
244
|
+
if (generationRef.current !== generation)
|
|
245
|
+
return;
|
|
246
|
+
setError(toError(cause));
|
|
247
|
+
setStatus('error');
|
|
248
|
+
}
|
|
249
|
+
}, []);
|
|
250
|
+
const request = useCallback(async (serviceUuid) => {
|
|
251
|
+
setError(null);
|
|
252
|
+
try {
|
|
253
|
+
await reset();
|
|
254
|
+
const generation = generationRef.current;
|
|
255
|
+
const trimmed = serviceUuid?.trim() || undefined;
|
|
256
|
+
requestedServiceRef.current = trimmed;
|
|
257
|
+
setStatus('requesting');
|
|
258
|
+
if (!trimmed && !isMoldableBluetoothSupported()) {
|
|
259
|
+
throw new Error('The desktop Bluetooth chooser needs a service UUID to scan.');
|
|
260
|
+
}
|
|
261
|
+
const transport = await requestMoldableBluetoothWithFallback(trimmed
|
|
262
|
+
? {
|
|
263
|
+
filters: [{ services: [trimmed] }],
|
|
264
|
+
optionalServices: [trimmed],
|
|
265
|
+
}
|
|
266
|
+
: { acceptAllDevices: true });
|
|
267
|
+
if (generationRef.current !== generation) {
|
|
268
|
+
if (transport.kind === 'desktop-bridge') {
|
|
269
|
+
await transport.device.disconnect().catch(() => undefined);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
transport.device.gatt?.disconnect();
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
transportRef.current = transport;
|
|
277
|
+
setDevice(transport.device);
|
|
278
|
+
detachDeviceRef.current = observeGattDisconnect(transport, () => {
|
|
279
|
+
handleDisconnectedRef.current();
|
|
280
|
+
});
|
|
281
|
+
await connect();
|
|
282
|
+
}
|
|
283
|
+
catch (cause) {
|
|
284
|
+
if (isDismissedChooser(cause)) {
|
|
285
|
+
setStatus('idle');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (unmountedRef.current)
|
|
289
|
+
return;
|
|
290
|
+
setError(toError(cause));
|
|
291
|
+
setStatus(transportRef.current ? 'connected' : 'error');
|
|
292
|
+
}
|
|
293
|
+
}, [connect, reset]);
|
|
294
|
+
const read = useCallback(async (characteristic) => {
|
|
295
|
+
const transport = transportRef.current;
|
|
296
|
+
if (!transport)
|
|
297
|
+
return;
|
|
298
|
+
const generation = generationRef.current;
|
|
299
|
+
setError(null);
|
|
300
|
+
try {
|
|
301
|
+
const value = transport.kind === 'desktop-bridge'
|
|
302
|
+
? await transport.device.read(characteristic.serviceUuid, characteristic.uuid)
|
|
303
|
+
: await readMoldableBluetoothValue(requireWebCharacteristic(characteristic));
|
|
304
|
+
if (generationRef.current !== generation)
|
|
305
|
+
return;
|
|
306
|
+
appendValue(characteristic, value);
|
|
307
|
+
}
|
|
308
|
+
catch (cause) {
|
|
309
|
+
if (generationRef.current !== generation)
|
|
310
|
+
return;
|
|
311
|
+
setError(toError(cause));
|
|
312
|
+
}
|
|
313
|
+
}, [appendValue]);
|
|
314
|
+
const subscribe = useCallback(async (characteristic) => {
|
|
315
|
+
const transport = transportRef.current;
|
|
316
|
+
const key = characteristicKey(characteristic);
|
|
317
|
+
if (!transport || subscriptionsRef.current.has(key))
|
|
318
|
+
return;
|
|
319
|
+
const generation = generationRef.current;
|
|
320
|
+
setError(null);
|
|
321
|
+
try {
|
|
322
|
+
let stop;
|
|
323
|
+
if (transport.kind === 'desktop-bridge') {
|
|
324
|
+
stop = await transport.device.observe(characteristic.serviceUuid, characteristic.uuid, (value) => appendValue(characteristic, value));
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
const target = requireWebCharacteristic(characteristic);
|
|
328
|
+
stop = await observeMoldableBluetoothNotifications(target, () => {
|
|
329
|
+
if (target.value)
|
|
330
|
+
appendValue(characteristic, target.value);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (generationRef.current !== generation) {
|
|
334
|
+
await stop().catch(() => undefined);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
subscriptionsRef.current.set(key, stop);
|
|
338
|
+
setSubscribedUuids((previous) => previous.includes(key) ? previous : [...previous, key]);
|
|
339
|
+
}
|
|
340
|
+
catch (cause) {
|
|
341
|
+
if (generationRef.current !== generation)
|
|
342
|
+
return;
|
|
343
|
+
setError(toError(cause));
|
|
344
|
+
}
|
|
345
|
+
}, [appendValue]);
|
|
346
|
+
const unsubscribe = useCallback(async (characteristic) => {
|
|
347
|
+
const key = characteristicKey(characteristic);
|
|
348
|
+
const stop = subscriptionsRef.current.get(key);
|
|
349
|
+
if (!stop)
|
|
350
|
+
return;
|
|
351
|
+
setError(null);
|
|
352
|
+
try {
|
|
353
|
+
await stop();
|
|
354
|
+
if (subscriptionsRef.current.get(key) !== stop)
|
|
355
|
+
return;
|
|
356
|
+
subscriptionsRef.current.delete(key);
|
|
357
|
+
setSubscribedUuids((previous) => previous.filter((value) => value !== key));
|
|
358
|
+
}
|
|
359
|
+
catch (cause) {
|
|
360
|
+
setError(toError(cause));
|
|
361
|
+
}
|
|
362
|
+
}, []);
|
|
363
|
+
const write = useCallback(async (characteristic, bytes) => {
|
|
364
|
+
const transport = transportRef.current;
|
|
365
|
+
if (!transport)
|
|
366
|
+
return;
|
|
367
|
+
const generation = generationRef.current;
|
|
368
|
+
setError(null);
|
|
369
|
+
try {
|
|
370
|
+
if (transport.kind === 'desktop-bridge') {
|
|
371
|
+
await transport.device.write(characteristic.serviceUuid, characteristic.uuid, bytes);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
await writeMoldableBluetoothValue(requireWebCharacteristic(characteristic), bytes, 'automatic');
|
|
375
|
+
}
|
|
376
|
+
if (generationRef.current !== generation)
|
|
377
|
+
return;
|
|
378
|
+
append({
|
|
379
|
+
direction: 'out',
|
|
380
|
+
data: `${shortUuid(characteristic.uuid)} ${formatHexDump(asBytes(bytes))}`,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
catch (cause) {
|
|
384
|
+
if (generationRef.current !== generation)
|
|
385
|
+
return;
|
|
386
|
+
setError(toError(cause));
|
|
387
|
+
}
|
|
388
|
+
}, [append]);
|
|
389
|
+
const writeHex = useCallback(async (characteristic, value) => {
|
|
390
|
+
try {
|
|
391
|
+
await write(characteristic, parseBluetoothHexBytes(value));
|
|
392
|
+
}
|
|
393
|
+
catch (cause) {
|
|
394
|
+
setError(toError(cause));
|
|
395
|
+
}
|
|
396
|
+
}, [write]);
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
unmountedRef.current = false;
|
|
399
|
+
return () => {
|
|
400
|
+
unmountedRef.current = true;
|
|
401
|
+
void reset(true);
|
|
402
|
+
};
|
|
403
|
+
}, [reset]);
|
|
404
|
+
return {
|
|
405
|
+
status,
|
|
406
|
+
device,
|
|
407
|
+
connection,
|
|
408
|
+
disconnecting,
|
|
409
|
+
services,
|
|
410
|
+
subscribedUuids,
|
|
411
|
+
error,
|
|
412
|
+
entries,
|
|
413
|
+
request,
|
|
414
|
+
connect,
|
|
415
|
+
disconnect,
|
|
416
|
+
read,
|
|
417
|
+
subscribe,
|
|
418
|
+
unsubscribe,
|
|
419
|
+
write,
|
|
420
|
+
writeHex,
|
|
421
|
+
clearEntries: clear,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Bluetooth LE panel: scan (optionally scoped to a service UUID), connect,
|
|
426
|
+
* browse discovered characteristics, and read, write, or subscribe to values
|
|
427
|
+
* with a live hex log.
|
|
428
|
+
*/
|
|
429
|
+
function BluetoothPanel({ className, serviceUuid, onValue, ...props }) {
|
|
430
|
+
const bluetooth = useMoldableBluetooth({ onValue });
|
|
431
|
+
const [uuidInput, setUuidInput] = useState(serviceUuid ?? '');
|
|
432
|
+
const [writeValues, setWriteValues] = useState({});
|
|
433
|
+
const connected = bluetooth.status === 'connected';
|
|
434
|
+
const busy = bluetooth.status === 'requesting' || bluetooth.status === 'connecting';
|
|
435
|
+
const deviceName = bluetooth.device?.name || 'BLE device';
|
|
436
|
+
return (_jsxs(HardwarePanel, { "data-slot": "bluetooth-panel", "data-status": bluetooth.status, className: className, ...props, children: [_jsxs(HardwarePanelHeader, { children: [_jsx(HardwarePanelIcon, { children: _jsx(Bluetooth, {}) }), _jsx(HardwarePanelTitle, { children: "Bluetooth LE" }), _jsx(CapabilityBadge, { capability: "bluetooth" }), _jsx(HardwarePanelAction, { children: connected ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground inline-flex max-w-40 items-center rounded-full border px-2 py-0.5 text-[11px] font-medium", children: _jsx("span", { className: "truncate", children: deviceName }) }), _jsx(LiveIndicator, {}), _jsxs(Button, { size: "sm", variant: "outline", disabled: bluetooth.disconnecting, onClick: () => void bluetooth.disconnect(), children: [bluetooth.disconnecting ? _jsx(Spinner, {}) : null, bluetooth.disconnecting ? 'Disconnecting…' : 'Disconnect'] })] })) : (_jsxs(_Fragment, { children: [_jsx(Input, { value: uuidInput, onChange: (event) => setUuidInput(event.target.value), onKeyDown: (event) => {
|
|
437
|
+
if (event.key === 'Enter' && !busy) {
|
|
438
|
+
void bluetooth.request(uuidInput);
|
|
439
|
+
}
|
|
440
|
+
}, placeholder: "Service UUID (optional)", "aria-label": "Service UUID", disabled: busy, className: "h-8 w-44 font-mono text-xs md:text-xs" }), _jsxs(Button, { size: "sm", disabled: busy, onClick: () => void bluetooth.request(uuidInput), children: [busy ? _jsx(Spinner, {}) : null, bluetooth.status === 'requesting'
|
|
441
|
+
? 'Scanning…'
|
|
442
|
+
: bluetooth.status === 'connecting'
|
|
443
|
+
? 'Connecting…'
|
|
444
|
+
: 'Scan'] })] })) })] }), _jsx(HardwarePanelContent, { children: connected ? (_jsxs("div", { className: "animate-in fade-in space-y-4 duration-300 motion-reduce:animate-none", children: [bluetooth.services.length === 0 ? (_jsx(DeviceList, { children: _jsx(DeviceListEmpty, { icon: _jsx(Bluetooth, {}), children: "No GATT services discovered" }) })) : (bluetooth.services.map((service) => (_jsxs("section", { "data-slot": "bluetooth-service", className: "space-y-1.5", children: [_jsx("div", { className: "text-muted-foreground font-mono text-[10px] font-medium uppercase tracking-wider", children: middleTruncate(service.uuid) }), _jsx(DeviceList, { children: service.characteristics.length === 0 ? (_jsx(DeviceListEmpty, { children: "No characteristics" })) : (service.characteristics.map((characteristic) => {
|
|
445
|
+
const key = characteristicKey(characteristic);
|
|
446
|
+
const subscribed = bluetooth.subscribedUuids.includes(key);
|
|
447
|
+
const unknown = characteristic.properties.length === 0;
|
|
448
|
+
const canRead = unknown || characteristic.properties.includes('read');
|
|
449
|
+
const canWrite = unknown || characteristic.properties.includes('write');
|
|
450
|
+
const canNotify = unknown ||
|
|
451
|
+
characteristic.properties.includes('notify') ||
|
|
452
|
+
characteristic.properties.includes('indicate');
|
|
453
|
+
return (_jsx(DeviceListItem, { icon: _jsx(Binary, {}), name: _jsx("span", { className: "font-mono", children: shortUuid(characteristic.uuid) }), description: unknown
|
|
454
|
+
? undefined
|
|
455
|
+
: characteristic.properties.join(' · '), action: _jsxs(_Fragment, { children: [canWrite ? (_jsxs(_Fragment, { children: [_jsx(Input, { value: writeValues[key] ?? '', onChange: (event) => setWriteValues((previous) => ({
|
|
456
|
+
...previous,
|
|
457
|
+
[key]: event.target.value,
|
|
458
|
+
})), onKeyDown: (event) => {
|
|
459
|
+
if (event.key === 'Enter') {
|
|
460
|
+
void bluetooth.writeHex(characteristic, writeValues[key] ?? '');
|
|
461
|
+
}
|
|
462
|
+
}, placeholder: "01 A0 FF", "aria-label": `Hex value for ${shortUuid(characteristic.uuid)}`, className: "h-7 w-24 font-mono text-xs md:text-xs" }), _jsx(Button, { variant: "ghost", size: "xs", onClick: () => void bluetooth.writeHex(characteristic, writeValues[key] ?? ''), children: "Write" })] })) : null, canRead ? (_jsx(Button, { variant: "ghost", size: "xs", onClick: () => void bluetooth.read(characteristic), children: "Read" })) : null, canNotify ? (_jsx(Button, { variant: "outline", size: "xs", onClick: () => void (subscribed
|
|
463
|
+
? bluetooth.unsubscribe(characteristic)
|
|
464
|
+
: bluetooth.subscribe(characteristic)), children: subscribed ? 'Stop' : 'Notify' })) : null] }) }, key));
|
|
465
|
+
})) })] }, service.uuid)))), bluetooth.error ? (_jsx("p", { className: "text-status-error text-xs", children: bluetooth.error.message })) : null, _jsx(StreamLog, { entries: bluetooth.entries, maxHeight: 200, emptyLabel: "Waiting for characteristic data" })] })) : (_jsx("div", { className: "animate-in fade-in flex flex-col items-center gap-3 py-6 text-center duration-300 motion-reduce:animate-none", children: busy ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { className: "text-muted-foreground" }), _jsx("p", { className: "text-muted-foreground text-xs", children: bluetooth.status === 'requesting'
|
|
466
|
+
? 'Waiting for the device chooser…'
|
|
467
|
+
: 'Connecting to device…' })] })) : bluetooth.status === 'error' ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm font-medium", children: "Bluetooth request failed" }), _jsx("p", { className: "text-muted-foreground max-w-64 text-xs", children: bluetooth.error?.message ??
|
|
468
|
+
'The Bluetooth request could not complete.' })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-muted-foreground bg-background shadow-xs flex size-10 items-center justify-center rounded-lg border [&_svg]:size-5", children: _jsx(Bluetooth, {}) }), _jsx("p", { className: "text-sm font-medium", children: "Not connected" }), _jsx("p", { className: "text-muted-foreground text-xs", children: "Scan for a nearby BLE device." })] })) })) })] }));
|
|
469
|
+
}
|
|
470
|
+
export { BluetoothPanel };
|