@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.
- package/CMakeLists.txt +198 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/cmake/mikrojs_bytecode.cmake +146 -0
- package/cmake.js +22 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +132 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/include/byteorder_apple.h +11 -0
- package/include/byteorder_windows.h +12 -0
- package/include/mikrojs/cbor_helpers.h +24 -0
- package/include/mikrojs/cutils_wrap.h +59 -0
- package/include/mikrojs/errors.h +144 -0
- package/include/mikrojs/mem.h +11 -0
- package/include/mikrojs/mik_color.h +32 -0
- package/include/mikrojs/mikrojs.h +331 -0
- package/include/mikrojs/platform.h +82 -0
- package/include/mikrojs/private.h +281 -0
- package/include/mikrojs/utils.h +125 -0
- package/package.json +100 -0
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/ble/ble.ts +231 -0
- package/runtime/ble/types.ts +194 -0
- package/runtime/ble/uuid.ts +89 -0
- package/runtime/ble/validators.ts +61 -0
- package/runtime/cbor/cbor.ts +1 -0
- package/runtime/cbor/types.ts +8 -0
- package/runtime/console/types.ts +50 -0
- package/runtime/env/env.ts +17 -0
- package/runtime/env/types.ts +12 -0
- package/runtime/format/types.ts +4 -0
- package/runtime/fs/fs.ts +93 -0
- package/runtime/fs/types.ts +92 -0
- package/runtime/globals.d.ts +87 -0
- package/runtime/http/helpers.ts +222 -0
- package/runtime/http/native.ts +151 -0
- package/runtime/http/request.ts +25 -0
- package/runtime/i2c/i2c.ts +35 -0
- package/runtime/i2c/types.ts +55 -0
- package/runtime/inspect/types.ts +10 -0
- package/runtime/internal.d.ts +456 -0
- package/runtime/kv/nvs.ts +17 -0
- package/runtime/kv/rtc.ts +17 -0
- package/runtime/kv/shared.ts +107 -0
- package/runtime/kv/types.ts +150 -0
- package/runtime/neopixel/neopixel.ts +38 -0
- package/runtime/neopixel/types.ts +27 -0
- package/runtime/pin/pin.ts +51 -0
- package/runtime/pin/types.ts +49 -0
- package/runtime/pwm/pwm.ts +32 -0
- package/runtime/pwm/types.ts +29 -0
- package/runtime/reader/reader.ts +167 -0
- package/runtime/reader/types.ts +34 -0
- package/runtime/result/native-result.node-shim.ts +44 -0
- package/runtime/result/result.ts +26 -0
- package/runtime/result/types.ts +60 -0
- package/runtime/schema/schema.ts +321 -0
- package/runtime/schema/types.ts +152 -0
- package/runtime/sleep/sleep.ts +14 -0
- package/runtime/sleep/types.ts +44 -0
- package/runtime/sntp/sntp.ts +54 -0
- package/runtime/sntp/types.ts +38 -0
- package/runtime/spi/spi.ts +31 -0
- package/runtime/spi/types.ts +42 -0
- package/runtime/stdio/stdio.ts +44 -0
- package/runtime/stdio/types.ts +22 -0
- package/runtime/stream/stream.ts +150 -0
- package/runtime/stream/types.ts +47 -0
- package/runtime/sys/sys.ts +90 -0
- package/runtime/sys/types.ts +131 -0
- package/runtime/test/test.ts +595 -0
- package/runtime/test/types.ts +97 -0
- package/runtime/uart/types.ts +75 -0
- package/runtime/uart/uart.ts +51 -0
- package/runtime/wifi/types.ts +156 -0
- package/runtime/wifi/wifi.ts +208 -0
- package/scripts/bundle-runtime.js +149 -0
- package/scripts/compare-minifiers.js +189 -0
- package/scripts/compile-bytecode.sh +38 -0
- package/scripts/copy-prebuild.js +20 -0
- package/scripts/generate-symbol-map.js +146 -0
- package/src/builtins.cpp +82 -0
- package/src/cutils_compat.c +38 -0
- package/src/eval_bytecode.cpp +42 -0
- package/src/fs.cpp +878 -0
- package/src/mem.cpp +63 -0
- package/src/mik_abort.cpp +160 -0
- package/src/mik_app_config.cpp +358 -0
- package/src/mik_cbor.cpp +334 -0
- package/src/mik_color.cpp +46 -0
- package/src/mik_console.cpp +422 -0
- package/src/mik_inspect.cpp +850 -0
- package/src/mik_repl.cpp +1122 -0
- package/src/mik_result.cpp +344 -0
- package/src/mik_stdio.cpp +147 -0
- package/src/mik_sys.cpp +239 -0
- package/src/mik_text_encoding.cpp +443 -0
- package/src/mikrojs.cpp +942 -0
- package/src/modules.cpp +944 -0
- package/src/platform_posix.cpp +134 -0
- package/src/timers.cpp +208 -0
- 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
|