@mikrojs/native 0.0.7

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.
Files changed (109) hide show
  1. package/CMakeLists.txt +198 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/cmake/mikrojs_bytecode.cmake +146 -0
  5. package/cmake.js +22 -0
  6. package/dist/index.d.ts +52 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +132 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/types.d.ts +43 -0
  11. package/dist/types.d.ts.map +1 -0
  12. package/dist/types.js +2 -0
  13. package/dist/types.js.map +1 -0
  14. package/include/byteorder_apple.h +11 -0
  15. package/include/byteorder_windows.h +12 -0
  16. package/include/mikrojs/cbor_helpers.h +24 -0
  17. package/include/mikrojs/cutils_wrap.h +59 -0
  18. package/include/mikrojs/errors.h +144 -0
  19. package/include/mikrojs/mem.h +11 -0
  20. package/include/mikrojs/mik_color.h +32 -0
  21. package/include/mikrojs/mikrojs.h +331 -0
  22. package/include/mikrojs/platform.h +82 -0
  23. package/include/mikrojs/private.h +281 -0
  24. package/include/mikrojs/utils.h +125 -0
  25. package/package.json +100 -0
  26. package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
  27. package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
  28. package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
  29. package/runtime/ble/ble.ts +231 -0
  30. package/runtime/ble/types.ts +194 -0
  31. package/runtime/ble/uuid.ts +89 -0
  32. package/runtime/ble/validators.ts +61 -0
  33. package/runtime/cbor/cbor.ts +1 -0
  34. package/runtime/cbor/types.ts +8 -0
  35. package/runtime/console/types.ts +50 -0
  36. package/runtime/env/env.ts +17 -0
  37. package/runtime/env/types.ts +12 -0
  38. package/runtime/format/types.ts +4 -0
  39. package/runtime/fs/fs.ts +93 -0
  40. package/runtime/fs/types.ts +92 -0
  41. package/runtime/globals.d.ts +87 -0
  42. package/runtime/http/helpers.ts +222 -0
  43. package/runtime/http/native.ts +151 -0
  44. package/runtime/http/request.ts +25 -0
  45. package/runtime/i2c/i2c.ts +35 -0
  46. package/runtime/i2c/types.ts +55 -0
  47. package/runtime/inspect/types.ts +10 -0
  48. package/runtime/internal.d.ts +456 -0
  49. package/runtime/kv/nvs.ts +17 -0
  50. package/runtime/kv/rtc.ts +17 -0
  51. package/runtime/kv/shared.ts +107 -0
  52. package/runtime/kv/types.ts +150 -0
  53. package/runtime/neopixel/neopixel.ts +38 -0
  54. package/runtime/neopixel/types.ts +27 -0
  55. package/runtime/pin/pin.ts +51 -0
  56. package/runtime/pin/types.ts +49 -0
  57. package/runtime/pwm/pwm.ts +32 -0
  58. package/runtime/pwm/types.ts +29 -0
  59. package/runtime/reader/reader.ts +167 -0
  60. package/runtime/reader/types.ts +34 -0
  61. package/runtime/result/native-result.node-shim.ts +44 -0
  62. package/runtime/result/result.ts +26 -0
  63. package/runtime/result/types.ts +60 -0
  64. package/runtime/schema/schema.ts +321 -0
  65. package/runtime/schema/types.ts +152 -0
  66. package/runtime/sleep/sleep.ts +14 -0
  67. package/runtime/sleep/types.ts +44 -0
  68. package/runtime/sntp/sntp.ts +54 -0
  69. package/runtime/sntp/types.ts +38 -0
  70. package/runtime/spi/spi.ts +31 -0
  71. package/runtime/spi/types.ts +42 -0
  72. package/runtime/stdio/stdio.ts +44 -0
  73. package/runtime/stdio/types.ts +22 -0
  74. package/runtime/stream/stream.ts +150 -0
  75. package/runtime/stream/types.ts +47 -0
  76. package/runtime/sys/sys.ts +90 -0
  77. package/runtime/sys/types.ts +131 -0
  78. package/runtime/test/test.ts +595 -0
  79. package/runtime/test/types.ts +97 -0
  80. package/runtime/uart/types.ts +75 -0
  81. package/runtime/uart/uart.ts +51 -0
  82. package/runtime/wifi/types.ts +156 -0
  83. package/runtime/wifi/wifi.ts +208 -0
  84. package/scripts/bundle-runtime.js +149 -0
  85. package/scripts/compare-minifiers.js +189 -0
  86. package/scripts/compile-bytecode.sh +38 -0
  87. package/scripts/copy-prebuild.js +20 -0
  88. package/scripts/generate-symbol-map.js +146 -0
  89. package/src/builtins.cpp +82 -0
  90. package/src/cutils_compat.c +38 -0
  91. package/src/eval_bytecode.cpp +42 -0
  92. package/src/fs.cpp +878 -0
  93. package/src/mem.cpp +63 -0
  94. package/src/mik_abort.cpp +160 -0
  95. package/src/mik_app_config.cpp +358 -0
  96. package/src/mik_cbor.cpp +334 -0
  97. package/src/mik_color.cpp +46 -0
  98. package/src/mik_console.cpp +422 -0
  99. package/src/mik_inspect.cpp +850 -0
  100. package/src/mik_repl.cpp +1122 -0
  101. package/src/mik_result.cpp +344 -0
  102. package/src/mik_stdio.cpp +147 -0
  103. package/src/mik_sys.cpp +239 -0
  104. package/src/mik_text_encoding.cpp +443 -0
  105. package/src/mikrojs.cpp +942 -0
  106. package/src/modules.cpp +944 -0
  107. package/src/platform_posix.cpp +134 -0
  108. package/src/timers.cpp +208 -0
  109. package/src/utils.cpp +173 -0
@@ -0,0 +1,231 @@
1
+ import {err, ok} from 'mikrojs/result'
2
+ import {Ble as NativeBle} from 'native:ble'
3
+
4
+ import type {Result} from '../result/types.js'
5
+ import type {
6
+ AdvertiseHandle,
7
+ AdvertiseOptions,
8
+ Ble,
9
+ BleError,
10
+ Characteristic,
11
+ CharacteristicProperty,
12
+ Peripheral,
13
+ PeripheralEventMap,
14
+ Service,
15
+ } from './types.js'
16
+ import {parseUuid} from './uuid.js'
17
+ import {ADV_PAYLOAD_MAX, computeAdvertisingPayloadSize, validateInterval} from './validators.js'
18
+
19
+ // Inline constructors for the JS-side validated variants (native:ble doesn't
20
+ // emit these — they come from validateInterval, validateServices, and the
21
+ // advertise payload-size check). Kept as `const` factories so validators.ts
22
+ // can stay decoupled from the BleError shape for unit testing.
23
+ const invalidIntervalFactory = {
24
+ InvalidInterval: (min: number, max: number) => ({name: 'InvalidInterval' as const, min, max}),
25
+ AdvertisingPayloadTooLarge: (bytes: number, max: number) => ({
26
+ name: 'AdvertisingPayloadTooLarge' as const,
27
+ bytes,
28
+ max,
29
+ }),
30
+ }
31
+
32
+ // ── Characteristic property bitmask (must match native side) ────────
33
+ // Native code reads these as BLE_GATT_CHR_F_* flags via a lookup table.
34
+ const PROP_READ = 0x01
35
+ const PROP_WRITE = 0x02
36
+ const PROP_WRITE_WITHOUT_RESPONSE = 0x04
37
+ const PROP_NOTIFY = 0x08
38
+ const PROP_INDICATE = 0x10
39
+
40
+ const PROP_BY_NAME: Record<CharacteristicProperty, number> = {
41
+ read: PROP_READ,
42
+ write: PROP_WRITE,
43
+ writeWithoutResponse: PROP_WRITE_WITHOUT_RESPONSE,
44
+ notify: PROP_NOTIFY,
45
+ indicate: PROP_INDICATE,
46
+ }
47
+
48
+ function propertiesToBitmask(props: readonly CharacteristicProperty[]): number {
49
+ let mask = 0
50
+ for (const p of props) mask |= PROP_BY_NAME[p] ?? 0
51
+ return mask
52
+ }
53
+
54
+ // ── Service/characteristic validation ───────────────────────────────
55
+
56
+ function invalidProps(uuid: string, reason: string): BleError {
57
+ return {name: 'InvalidProperties', uuid, reason}
58
+ }
59
+
60
+ function validateCharacteristic(char: Characteristic): BleError | undefined {
61
+ if (parseUuid(char.uuid) === null) return {name: 'InvalidUuid', value: char.uuid}
62
+
63
+ if (!Array.isArray(char.properties) || char.properties.length === 0) {
64
+ return invalidProps(char.uuid, 'at least one property required')
65
+ }
66
+ // Array.isArray widens readonly arrays to any[]; cast back for the lookup.
67
+ const props = char.properties as readonly CharacteristicProperty[]
68
+ for (const p of props) {
69
+ if (PROP_BY_NAME[p] === undefined) {
70
+ return invalidProps(char.uuid, `unknown property: ${String(p)}`)
71
+ }
72
+ }
73
+
74
+ if (char.writeMode !== undefined && char.writeMode !== 'advisory') {
75
+ return invalidProps(
76
+ char.uuid,
77
+ `writeMode '${String(char.writeMode)}' is not supported in this release`,
78
+ )
79
+ }
80
+ if (char.security !== undefined && char.security !== 'open') {
81
+ return invalidProps(
82
+ char.uuid,
83
+ `security '${String(char.security)}' is not supported in this release`,
84
+ )
85
+ }
86
+
87
+ return undefined
88
+ }
89
+
90
+ function validateService(service: Service): BleError | undefined {
91
+ if (parseUuid(service.uuid) === null) return {name: 'InvalidUuid', value: service.uuid}
92
+
93
+ if (!Array.isArray(service.characteristics) || service.characteristics.length === 0) {
94
+ return invalidProps(service.uuid, 'service must declare at least one characteristic')
95
+ }
96
+
97
+ const seen = new Set<string>()
98
+ for (const char of service.characteristics) {
99
+ const charErr = validateCharacteristic(char)
100
+ if (charErr) return charErr
101
+ const normalized = parseUuid(char.uuid)!.normalized
102
+ if (seen.has(normalized)) return {name: 'DuplicateCharacteristic', uuid: normalized}
103
+ seen.add(normalized)
104
+ }
105
+
106
+ return undefined
107
+ }
108
+
109
+ function validateServices(services: Service[]): BleError | undefined {
110
+ const seen = new Set<string>()
111
+ for (const service of services) {
112
+ const svcErr = validateService(service)
113
+ if (svcErr) return svcErr
114
+ const normalized = parseUuid(service.uuid)!.normalized
115
+ if (seen.has(normalized)) return invalidProps(service.uuid, 'duplicate service uuid')
116
+ seen.add(normalized)
117
+ }
118
+ return undefined
119
+ }
120
+
121
+ /**
122
+ * Converts the user-facing `Service[]` shape into the normalized form the
123
+ * native module expects: property arrays become bitmasks, UUIDs are
124
+ * normalized to their canonical lowercase form so that subsequent
125
+ * setValue / notify calls can use the same UUID strings in any case and
126
+ * still match what's registered. The `onWrite` callback is passed through
127
+ * as-is; the native side dup-refs it during GATT registration.
128
+ */
129
+ function normalizeServices(services: Service[]) {
130
+ return services.map((s) => ({
131
+ uuid: parseUuid(s.uuid)!.normalized,
132
+ characteristics: s.characteristics.map((c) => ({
133
+ uuid: parseUuid(c.uuid)!.normalized,
134
+ properties: propertiesToBitmask(c.properties),
135
+ value: c.value,
136
+ onWrite: c.onWrite,
137
+ })),
138
+ }))
139
+ }
140
+
141
+ const native = new NativeBle()
142
+
143
+ const ble: Ble = {
144
+ get name(): string {
145
+ return native.getName()
146
+ },
147
+ set name(value: string) {
148
+ native.setName(value)
149
+ },
150
+
151
+ get address(): string {
152
+ const result = native.getAddress()
153
+ return result.ok ? result.value : ''
154
+ },
155
+
156
+ get txPower(): number {
157
+ const result = native.getTxPower()
158
+ return result.ok ? result.value : 0
159
+ },
160
+ set txPower(dbm: number) {
161
+ native.setTxPower(dbm)
162
+ },
163
+
164
+ stop(): Result<void, BleError> {
165
+ return native.stop()
166
+ },
167
+ }
168
+
169
+ const peripheral: Peripheral = {
170
+ async advertise(options: AdvertiseOptions = {}): Promise<Result<AdvertiseHandle, BleError>> {
171
+ const intervalError = validateInterval(options.interval, invalidIntervalFactory)
172
+ if (intervalError) return err(intervalError)
173
+
174
+ if (options.services !== undefined) {
175
+ const svcErr = validateServices(options.services)
176
+ if (svcErr) return err(svcErr)
177
+ }
178
+
179
+ const payloadSize = computeAdvertisingPayloadSize(options, native.getName().length)
180
+ if (payloadSize > ADV_PAYLOAD_MAX) {
181
+ return err(invalidIntervalFactory.AdvertisingPayloadTooLarge(payloadSize, ADV_PAYLOAD_MAX))
182
+ }
183
+
184
+ const nativeOptions = {
185
+ ...options,
186
+ services: options.services ? normalizeServices(options.services) : undefined,
187
+ }
188
+
189
+ const result = native.advertise(nativeOptions as Parameters<typeof native.advertise>[0])
190
+ if (!result.ok) return result
191
+
192
+ const handle: AdvertiseHandle = {
193
+ stop(): Result<void, BleError> {
194
+ return native.stopAdvertising()
195
+ },
196
+ setValue(
197
+ serviceUuid: string,
198
+ characteristicUuid: string,
199
+ value: Uint8Array,
200
+ ): Result<void, BleError> {
201
+ const svc = parseUuid(serviceUuid)
202
+ if (!svc) return err({name: 'InvalidUuid' as const, value: serviceUuid})
203
+ const chr = parseUuid(characteristicUuid)
204
+ if (!chr) return err({name: 'InvalidUuid' as const, value: characteristicUuid})
205
+ return native.setValue(svc.normalized, chr.normalized, value)
206
+ },
207
+ notify(
208
+ serviceUuid: string,
209
+ characteristicUuid: string,
210
+ value: Uint8Array,
211
+ ): Result<void, BleError> {
212
+ const svc = parseUuid(serviceUuid)
213
+ if (!svc) return err({name: 'InvalidUuid' as const, value: serviceUuid})
214
+ const chr = parseUuid(characteristicUuid)
215
+ if (!chr) return err({name: 'InvalidUuid' as const, value: characteristicUuid})
216
+ return native.notify(svc.normalized, chr.normalized, value)
217
+ },
218
+ }
219
+ return ok(handle)
220
+ },
221
+
222
+ on<K extends keyof PeripheralEventMap>(event: K, listener: PeripheralEventMap[K]) {
223
+ native.on(event, listener as (...args: unknown[]) => void)
224
+ },
225
+
226
+ off<K extends keyof PeripheralEventMap>(event: K, listener: PeripheralEventMap[K]) {
227
+ native.off(event, listener as (...args: unknown[]) => void)
228
+ },
229
+ }
230
+
231
+ export {ble, peripheral}
@@ -0,0 +1,194 @@
1
+ import type {Result} from '../result/types.js'
2
+
3
+ /** Advertising interval range in milliseconds. */
4
+ export interface AdvertiseInterval {
5
+ /** Minimum interval. Non-connectable: ≥ 20ms. Connectable: ≥ 100ms. */
6
+ min: number
7
+ /** Maximum interval. ≤ 10240ms. Must be ≥ min. */
8
+ max: number
9
+ }
10
+
11
+ /**
12
+ * A property that a GATT characteristic exposes. The set of declared properties
13
+ * determines which operations centrals can perform.
14
+ */
15
+ export type CharacteristicProperty =
16
+ | 'read'
17
+ | 'write'
18
+ | 'writeWithoutResponse'
19
+ | 'notify'
20
+ | 'indicate'
21
+
22
+ /**
23
+ * A GATT characteristic declaration. The `uuid` is either a 16-bit shorthand
24
+ * (`"180f"`) or a full 128-bit form (`"6e400001-b5a3-f393-e0a9-e50e24dcca9e"`).
25
+ */
26
+ export interface Characteristic {
27
+ uuid: string
28
+ properties: readonly CharacteristicProperty[]
29
+ /**
30
+ * Initial value for the characteristic. Updated via `handle.setValue()` at
31
+ * runtime. When a central performs a GATT read, it sees the current value.
32
+ */
33
+ value?: Uint8Array
34
+ /**
35
+ * Called after a central writes to this characteristic. Runs asynchronously
36
+ * on the JS loop thread after the write has already been acknowledged at
37
+ * the GATT layer — it is advisory only and cannot reject the write. The
38
+ * only accepted `writeMode` is `'advisory'`, which is also the default.
39
+ */
40
+ onWrite?: (value: Uint8Array) => void
41
+ /**
42
+ * Only `'advisory'` is accepted. Advisory writes are acknowledged at the
43
+ * GATT layer before `onWrite` runs, so the handler cannot reject the write.
44
+ */
45
+ writeMode?: 'advisory'
46
+ /**
47
+ * Only `'open'` is accepted. Characteristics are accessible without
48
+ * authentication or encryption.
49
+ */
50
+ security?: 'open'
51
+ }
52
+
53
+ /** A GATT service declaration containing one or more characteristics. */
54
+ export interface Service {
55
+ uuid: string
56
+ characteristics: Characteristic[]
57
+ }
58
+
59
+ export interface AdvertiseOptions {
60
+ /** Device name included in the advertising packet. Defaults to `ble.name`. */
61
+ name?: string
62
+ /**
63
+ * Whether centrals can connect to the advertising device. Defaults to
64
+ * `false` (broadcaster mode). Set to `true` together with `services` to
65
+ * run a connectable GATT peripheral.
66
+ */
67
+ connectable?: boolean
68
+ /**
69
+ * GATT services to register on the peripheral. Services are declared
70
+ * upfront and frozen for the lifetime of the BLE stack — to change them,
71
+ * call `ble.stop()` and `advertise()` again.
72
+ */
73
+ services?: Service[]
74
+ /** Advertising interval range. Defaults to NimBLE's defaults (~100–150ms). */
75
+ interval?: AdvertiseInterval
76
+ /**
77
+ * Include the current TX power as a 3-byte field in the advertising payload.
78
+ * Useful for RSSI-based distance estimation.
79
+ */
80
+ includeTxPower?: boolean
81
+ /**
82
+ * Manufacturer-specific data bytes. The first two bytes must be the company ID
83
+ * (little-endian) per the BLE spec.
84
+ */
85
+ manufacturerData?: Uint8Array
86
+ }
87
+
88
+ export interface AdvertiseHandle {
89
+ /** Stops this advertising session. Does not tear down the BLE stack. */
90
+ stop(): Result<void, BleError>
91
+ /**
92
+ * Updates the cached value of a characteristic. Subsequent GATT reads by
93
+ * connected centrals will return the new bytes. Does not push updates to
94
+ * subscribers. Use `notify()` to both update and push in one call, or
95
+ * call `setValue()` followed by `notify()` if you want explicit control.
96
+ *
97
+ * Returns `err(NoSuchCharacteristic)` if either UUID is unknown.
98
+ */
99
+ setValue(
100
+ serviceUuid: string,
101
+ characteristicUuid: string,
102
+ value: Uint8Array,
103
+ ): Result<void, BleError>
104
+ /**
105
+ * Sends a notification with the given bytes to every currently-subscribed
106
+ * central. The characteristic must declare `'notify'` in its properties,
107
+ * and centrals must have enabled notifications via the CCCD descriptor.
108
+ *
109
+ * If no central is subscribed, this is a silent no-op and returns ok.
110
+ * If the payload exceeds the minimum MTU across active subscribers minus
111
+ * the 3-byte ATT header, returns `err(ValueTooLarge)` without sending.
112
+ * Per-subscriber send failures (backpressure, dropped connection) are
113
+ * logged at warn level and do not cause this call to return an error;
114
+ * notify is explicitly best-effort.
115
+ *
116
+ * This also updates the characteristic's cached value so subsequent
117
+ * reads return the notified bytes.
118
+ */
119
+ notify(serviceUuid: string, characteristicUuid: string, value: Uint8Array): Result<void, BleError>
120
+ }
121
+
122
+ export type BleError =
123
+ | {name: 'StackInitFailed'; message: string}
124
+ | {name: 'ControllerInitFailed'; message: string}
125
+ | {name: 'AlreadyAdvertising'}
126
+ | {name: 'NotInitialized'}
127
+ | {name: 'AdvertiseStartFailed'; message: string}
128
+ | {name: 'AdvertiseStopFailed'; message: string}
129
+ | {name: 'AdvertisingPayloadTooLarge'; bytes: number; max: number}
130
+ | {name: 'InvalidInterval'; min: number; max: number}
131
+ | {name: 'InvalidUuid'; value: string}
132
+ | {name: 'DuplicateCharacteristic'; uuid: string}
133
+ | {name: 'InvalidProperties'; uuid: string; reason: string}
134
+ | {name: 'GattRegistrationFailed'; message: string}
135
+ | {name: 'GattAlreadyRegistered'}
136
+ | {name: 'NoSuchCharacteristic'; uuid: string}
137
+ | {name: 'NotConnected'}
138
+ | {name: 'ValueTooLarge'; uuid: string; bytes: number; max: number}
139
+ | {name: 'NotifyFailed'; message: string}
140
+ | {name: 'StackShutdown'}
141
+ | {name: 'SetFailed'; message: string}
142
+ | {name: 'GetFailed'; message: string}
143
+
144
+ export interface Ble {
145
+ /** Device name used in advertising. Defaults to `"mikrojs-xxxxxx"` (last 3 bytes of MAC). */
146
+ name: string
147
+ /** Global radio TX power in dBm. Valid range depends on the chip. */
148
+ txPower: number
149
+ /** BLE MAC address as `"aa:bb:cc:dd:ee:ff"`. */
150
+ readonly address: string
151
+ /**
152
+ * Tears down the BLE stack entirely, reclaiming all RAM.
153
+ * The next BLE call re-initializes from scratch.
154
+ */
155
+ stop(): Result<void, BleError>
156
+ }
157
+
158
+ /** Information about a connected central. */
159
+ export interface ConnectionInfo {
160
+ /** Opaque stable identifier for this connection (NimBLE conn_handle). */
161
+ id: number
162
+ /** The peer's BLE address in canonical `"aa:bb:cc:dd:ee:ff"` form. */
163
+ address: string
164
+ /** Current negotiated ATT MTU in bytes. */
165
+ mtu: number
166
+ }
167
+
168
+ /** Information about a mid-session MTU change. */
169
+ export interface MtuInfo {
170
+ id: number
171
+ mtu: number
172
+ }
173
+
174
+ export interface PeripheralEventMap {
175
+ connect: (info: ConnectionInfo) => void
176
+ disconnect: (info: ConnectionInfo) => void
177
+ mtu: (info: MtuInfo) => void
178
+ }
179
+
180
+ export interface Peripheral {
181
+ /** Start advertising. Returns a handle whose `stop()` ends this session. */
182
+ advertise(options?: AdvertiseOptions): Promise<Result<AdvertiseHandle, BleError>>
183
+ /**
184
+ * Register a listener for a peripheral lifecycle event. Listeners are
185
+ * called on the JS loop thread in registration order. Safe to register
186
+ * before calling `advertise()`.
187
+ */
188
+ on<K extends keyof PeripheralEventMap>(event: K, listener: PeripheralEventMap[K]): void
189
+ /** Remove a previously-registered listener. */
190
+ off<K extends keyof PeripheralEventMap>(event: K, listener: PeripheralEventMap[K]): void
191
+ }
192
+
193
+ export declare const ble: Ble
194
+ export declare const peripheral: Peripheral
@@ -0,0 +1,89 @@
1
+ // BLE UUID parsing. Pure helper, no native dependency.
2
+ //
3
+ // BLE has two practical UUID sizes:
4
+ // - 16-bit: SIG-assigned UUIDs like 0x180F (Battery Service). Written as
5
+ // 4 hex characters: "180f".
6
+ // - 128-bit: vendor-custom UUIDs. Written in canonical dashed form:
7
+ // "6e400001-b5a3-f393-e0a9-e50e24dcca9e".
8
+ //
9
+ // 32-bit UUIDs exist in the spec but are nearly never used in practice; we
10
+ // don't accept them here. Users who need one can write the full 128-bit form.
11
+
12
+ /** Parsed 16-bit UUID. */
13
+ export interface ParsedUuid16 {
14
+ readonly kind: 16
15
+ /** The 16-bit value, 0..0xFFFF. */
16
+ readonly value: number
17
+ /** Normalized lowercase form, e.g. "180f". */
18
+ readonly normalized: string
19
+ }
20
+
21
+ /** Parsed 128-bit UUID. */
22
+ export interface ParsedUuid128 {
23
+ readonly kind: 128
24
+ /**
25
+ * 16 bytes in network / big-endian order — the same order you'd read off
26
+ * a formatted UUID string like "0000180f-0000-1000-8000-00805f9b34fb".
27
+ * Native code is responsible for converting to NimBLE's internal
28
+ * little-endian representation when building `ble_uuid128_t`.
29
+ */
30
+ readonly bytes: Uint8Array
31
+ /** Normalized lowercase form, e.g. "6e400001-b5a3-f393-e0a9-e50e24dcca9e". */
32
+ readonly normalized: string
33
+ }
34
+
35
+ export type ParsedUuid = ParsedUuid16 | ParsedUuid128
36
+
37
+ const HEX_16 = /^[0-9a-fA-F]{4}$/
38
+ const HEX_128 = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
39
+
40
+ /**
41
+ * Parses a BLE UUID string into a structured form. Accepts:
42
+ * - 4-char shorthand for 16-bit UUIDs: `"180f"`, `"180F"`
43
+ * - Full 128-bit form with dashes: `"6e400001-b5a3-f393-e0a9-e50e24dcca9e"`
44
+ *
45
+ * Leading/trailing whitespace is not accepted. Returns `null` for invalid
46
+ * input so the caller can wrap the original value in an `InvalidUuid` error.
47
+ */
48
+ export function parseUuid(input: unknown): ParsedUuid | null {
49
+ if (typeof input !== 'string') return null
50
+
51
+ if (HEX_16.test(input)) {
52
+ return {
53
+ kind: 16,
54
+ value: Number.parseInt(input, 16),
55
+ normalized: input.toLowerCase(),
56
+ }
57
+ }
58
+
59
+ if (HEX_128.test(input)) {
60
+ const normalized = input.toLowerCase()
61
+ const hexOnly = normalized.replace(/-/g, '')
62
+ const bytes = new Uint8Array(16)
63
+ for (let i = 0; i < 16; i++) {
64
+ bytes[i] = Number.parseInt(hexOnly.substring(i * 2, i * 2 + 2), 16)
65
+ }
66
+ return {kind: 128, bytes, normalized}
67
+ }
68
+
69
+ return null
70
+ }
71
+
72
+ /**
73
+ * Compares two parsed UUIDs for equality. Two 16-bit UUIDs are equal iff
74
+ * their values match; two 128-bit UUIDs are equal iff all 16 bytes match.
75
+ * A 16-bit and a 128-bit UUID are never equal here — callers that want to
76
+ * compare a 16-bit shorthand against its canonical 128-bit expansion should
77
+ * normalize to one form first.
78
+ */
79
+ export function uuidsEqual(a: ParsedUuid, b: ParsedUuid): boolean {
80
+ if (a.kind !== b.kind) return false
81
+ if (a.kind === 16) return a.value === (b as ParsedUuid16).value
82
+ const ab = a.bytes
83
+ const bb = (b as ParsedUuid128).bytes
84
+ if (ab.length !== bb.length) return false
85
+ for (let i = 0; i < ab.length; i++) {
86
+ if (ab[i] !== bb[i]) return false
87
+ }
88
+ return true
89
+ }
@@ -0,0 +1,61 @@
1
+ // Pure validation helpers for the BLE module. Extracted so they can be
2
+ // unit-tested on the host without pulling in the native `native:ble` module.
3
+
4
+ import type {AdvertiseInterval, AdvertiseOptions, BleError} from './types.js'
5
+
6
+ // BLE spec bounds for advertising interval, in milliseconds.
7
+ // Non-connectable and connectable modes have different minimums at the spec
8
+ // level. We use the looser non-connectable bound in JS and let NimBLE enforce
9
+ // the stricter connectable limit at advertise_start time.
10
+ export const MIN_INTERVAL_MS = 20
11
+ export const MAX_INTERVAL_MS = 10240
12
+
13
+ // BLE advertising packet is 31 bytes.
14
+ export const ADV_PAYLOAD_MAX = 31
15
+
16
+ // Must mirror the BleError factory in ble.ts. Decoupled here so validators
17
+ // don't import from a file that touches the native module.
18
+ type BleErrorFactory = {
19
+ InvalidInterval: (min: number, max: number) => BleError
20
+ AdvertisingPayloadTooLarge: (bytes: number, max: number) => BleError
21
+ }
22
+
23
+ export function validateInterval(
24
+ interval: AdvertiseInterval | undefined,
25
+ factory: BleErrorFactory,
26
+ ): BleError | undefined {
27
+ if (interval === undefined) return undefined
28
+ const {min, max} = interval
29
+ if (
30
+ !Number.isFinite(min) ||
31
+ !Number.isFinite(max) ||
32
+ min < MIN_INTERVAL_MS ||
33
+ max > MAX_INTERVAL_MS ||
34
+ min > max
35
+ ) {
36
+ return factory.InvalidInterval(min, max)
37
+ }
38
+ return undefined
39
+ }
40
+
41
+ /**
42
+ * Estimates the total bytes the advertising packet will occupy with the
43
+ * given options. Accounts for the mandatory flags field, the device name,
44
+ * the optional TX power field, and optional manufacturer data.
45
+ *
46
+ * Each AD structure costs 2 bytes of overhead (length byte + type byte)
47
+ * plus the payload size.
48
+ */
49
+ export function computeAdvertisingPayloadSize(
50
+ options: AdvertiseOptions,
51
+ fallbackNameLength: number,
52
+ ): number {
53
+ let total = 3 // flags: 1 length + 1 type + 1 byte value
54
+ const nameLen = options.name !== undefined ? options.name.length : fallbackNameLength
55
+ if (nameLen > 0) total += 2 + nameLen
56
+ if (options.includeTxPower) total += 3
57
+ if (options.manufacturerData !== undefined) {
58
+ total += 2 + options.manufacturerData.length
59
+ }
60
+ return total
61
+ }
@@ -0,0 +1 @@
1
+ export {decode, encode} from 'native:cbor'
@@ -0,0 +1,8 @@
1
+ import type {Result} from '../result/types.js'
2
+
3
+ export type CborError =
4
+ | {name: 'EncodeFailed'; message: string}
5
+ | {name: 'DecodeFailed'; message: string}
6
+
7
+ export declare function encode(value: unknown): Result<Uint8Array, CborError>
8
+ export declare function decode(data: Uint8Array): Result<unknown, CborError>
@@ -0,0 +1,50 @@
1
+ import type {FormatFn, FormatString} from '../format/types.js'
2
+ import type {InspectFn} from '../inspect/types.js'
3
+
4
+ type ThemeToken =
5
+ | 'annotation'
6
+ | 'boolean'
7
+ | 'comment'
8
+ | 'date'
9
+ | 'error'
10
+ | 'warning'
11
+ | 'function'
12
+ | 'identifier'
13
+ | 'keyword'
14
+ | 'null'
15
+ | 'number'
16
+ | 'other'
17
+ | 'regexp'
18
+ | 'string'
19
+ | 'symbol'
20
+ | 'type'
21
+ | 'undefined'
22
+ | 'bigint'
23
+
24
+ export type ColorizeFn = (themeToken: ThemeToken, value: string) => string
25
+
26
+ export interface LogFn {
27
+ (fmt: FormatString, ...args: any[]): void
28
+
29
+ (...args: any[]): void
30
+ }
31
+
32
+ export type ConsoleAPI = {
33
+ log: LogFn
34
+ error: LogFn
35
+ info: LogFn
36
+ warn: LogFn
37
+ debug: LogFn
38
+ }
39
+
40
+ export declare function createConsole(config: {
41
+ inspect: InspectFn
42
+ colorize: ColorizeFn
43
+ format: FormatFn
44
+ stdout: {write: (output: string) => void}
45
+ }): ConsoleAPI
46
+
47
+ export declare const log: ConsoleAPI['log']
48
+ export declare const info: ConsoleAPI['info']
49
+ export declare const warn: ConsoleAPI['warn']
50
+ export declare const error: ConsoleAPI['error']
@@ -0,0 +1,17 @@
1
+ import type {Env} from './types.js'
2
+
3
+ export const env: Env = {
4
+ get(key: string): string | undefined {
5
+ return (import.meta.env as Record<string, string | undefined>)[key]
6
+ },
7
+ has(key: string): boolean {
8
+ return key in import.meta.env
9
+ },
10
+ require(key: string): string {
11
+ const value = (import.meta.env as Record<string, string | undefined>)[key]
12
+ if (value === undefined) {
13
+ throw new TypeError(`Required environment variable "${key}" is not set`)
14
+ }
15
+ return value
16
+ },
17
+ }
@@ -0,0 +1,12 @@
1
+ export interface Env {
2
+ /** Get an environment variable, or `undefined` if not set. */
3
+ get(key: string): string | undefined
4
+
5
+ /** Check whether an environment variable is set. */
6
+ has(key: string): boolean
7
+
8
+ /** Get a required environment variable. Panics if not set. */
9
+ require(key: string): string
10
+ }
11
+
12
+ export declare const env: Env
@@ -0,0 +1,4 @@
1
+ export type FormatString = string & {}
2
+
3
+ export type FormatFn = (fmt: FormatString, ...args: any[]) => string
4
+ export declare const format: FormatFn