@mikrojs/firmware 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/LICENSE +21 -0
- package/README.md +30 -0
- package/bin/idf.py +7 -0
- package/chips.json +3 -0
- package/cmake.js +9 -0
- package/components/mikrojs/CMakeLists.txt +187 -0
- package/components/mikrojs/Kconfig +55 -0
- package/components/mikrojs/idf_component.yml +6 -0
- package/components/mikrojs/include/mem.h +3 -0
- package/components/mikrojs/include/mik_color.h +3 -0
- package/components/mikrojs/include/mik_http_internal.h +77 -0
- package/components/mikrojs/include/mikrojs.h +5 -0
- package/components/mikrojs/include/mikrojs_esp32.h +65 -0
- package/components/mikrojs/include/private.h +10 -0
- package/components/mikrojs/include/utils.h +3 -0
- package/components/mikrojs/mik_ble.cpp +1588 -0
- package/components/mikrojs/mik_ble_c_shim.c +61 -0
- package/components/mikrojs/mik_ble_c_shim.h +37 -0
- package/components/mikrojs/mik_config.cpp +167 -0
- package/components/mikrojs/mik_deploy.cpp +584 -0
- package/components/mikrojs/mik_http.cpp +916 -0
- package/components/mikrojs/mik_i2c.cpp +364 -0
- package/components/mikrojs/mik_main.cpp +542 -0
- package/components/mikrojs/mik_neopixel.cpp +437 -0
- package/components/mikrojs/mik_nvs_kv.cpp +219 -0
- package/components/mikrojs/mik_pin.cpp +195 -0
- package/components/mikrojs/mik_pwm.cpp +525 -0
- package/components/mikrojs/mik_recovery.cpp +86 -0
- package/components/mikrojs/mik_rtc.cpp +305 -0
- package/components/mikrojs/mik_serial_io.cpp +362 -0
- package/components/mikrojs/mik_sleep.cpp +226 -0
- package/components/mikrojs/mik_sntp.cpp +275 -0
- package/components/mikrojs/mik_spi.cpp +330 -0
- package/components/mikrojs/mik_uart.cpp +497 -0
- package/components/mikrojs/mik_wifi.cpp +1434 -0
- package/components/mikrojs/platform_esp32.cpp +192 -0
- package/components/mikrojs/test/CMakeLists.txt +32 -0
- package/components/mikrojs/test/abort_test.cpp +254 -0
- package/components/mikrojs/test/ble_test.cpp +714 -0
- package/components/mikrojs/test/fs_js_test.cpp +458 -0
- package/components/mikrojs/test/fs_pub_test.cpp +312 -0
- package/components/mikrojs/test/http_test.cpp +475 -0
- package/components/mikrojs/test/i2c_test.cpp +138 -0
- package/components/mikrojs/test/modules_extended_test.cpp +137 -0
- package/components/mikrojs/test/modules_test.cpp +131 -0
- package/components/mikrojs/test/pins_test.cpp +47 -0
- package/components/mikrojs/test/pwm_test.cpp +166 -0
- package/components/mikrojs/test/repl_protocol_test.cpp +405 -0
- package/components/mikrojs/test/rtc_test.cpp +331 -0
- package/components/mikrojs/test/runtime_test.cpp +89 -0
- package/components/mikrojs/test/sleep_test.cpp +222 -0
- package/components/mikrojs/test/sntp_test.cpp +249 -0
- package/components/mikrojs/test/stdio_test.cpp +449 -0
- package/components/mikrojs/test/sys_test.cpp +165 -0
- package/components/mikrojs/test/text_encoding_test.cpp +224 -0
- package/components/mikrojs/test/timers_js_test.cpp +244 -0
- package/components/mikrojs/test/timers_test.cpp +79 -0
- package/components/mikrojs/test/wifi_test.cpp +599 -0
- package/default-app/main/CMakeLists.txt +3 -0
- package/default-app/main/main.cpp +5 -0
- package/discover.js +77 -0
- package/index.d.ts +7 -0
- package/index.js +20 -0
- package/package.json +61 -0
- package/partitions.csv +5 -0
- package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32/flasher_args.json +24 -0
- package/prebuilds/esp32/mikrojs.bin +0 -0
- package/prebuilds/esp32/partition_table/partition-table.bin +0 -0
- package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c3/flasher_args.json +24 -0
- package/prebuilds/esp32c3/mikrojs.bin +0 -0
- package/prebuilds/esp32c3/partition_table/partition-table.bin +0 -0
- package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c6/flasher_args.json +24 -0
- package/prebuilds/esp32c6/mikrojs.bin +0 -0
- package/prebuilds/esp32c6/partition_table/partition-table.bin +0 -0
- package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32s3/flasher_args.json +24 -0
- package/prebuilds/esp32s3/mikrojs.bin +0 -0
- package/prebuilds/esp32s3/partition_table/partition-table.bin +0 -0
- package/project.cmake +101 -0
- package/resolve.js +54 -0
- package/sdkconfig.defaults +127 -0
- package/sdkconfig.defaults.esp32 +8 -0
- package/sdkconfig.defaults.esp32c3 +15 -0
- package/sdkconfig.defaults.esp32c6 +26 -0
- package/sdkconfig.defaults.esp32s3 +22 -0
|
@@ -0,0 +1,1588 @@
|
|
|
1
|
+
#include <cctype>
|
|
2
|
+
#include <cstdio>
|
|
3
|
+
#include <cstdlib>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <vector>
|
|
7
|
+
|
|
8
|
+
#include "esp_log.h"
|
|
9
|
+
#include "esp_mac.h"
|
|
10
|
+
#include "freertos/FreeRTOS.h"
|
|
11
|
+
#include "freertos/semphr.h"
|
|
12
|
+
#include "host/ble_gap.h"
|
|
13
|
+
#include "host/ble_gatt.h"
|
|
14
|
+
#include "host/ble_hs.h"
|
|
15
|
+
#include "host/util/util.h"
|
|
16
|
+
#include "nimble/nimble_port.h"
|
|
17
|
+
#include "nimble/nimble_port_freertos.h"
|
|
18
|
+
#include "services/gap/ble_svc_gap.h"
|
|
19
|
+
#include "services/gatt/ble_svc_gatt.h"
|
|
20
|
+
|
|
21
|
+
#include "mik_ble_c_shim.h"
|
|
22
|
+
#include "mikrojs/errors.h"
|
|
23
|
+
#include "private.h"
|
|
24
|
+
#include "utils.h"
|
|
25
|
+
|
|
26
|
+
/* Internal helpers below return `MIK_ERR_BLE_*` int codes. This converter
|
|
27
|
+
* maps those codes to the JS-side variant names used by mikrojs/ble, so
|
|
28
|
+
* the public JS-bound functions can emit `{name, message}` errors directly
|
|
29
|
+
* via mik__result_err_named without a JS-side mapError switch. */
|
|
30
|
+
static const char* mik__ble_code_to_name(int code) {
|
|
31
|
+
switch (code) {
|
|
32
|
+
case MIK_ERR_BLE_STACK_INIT_FAILED:
|
|
33
|
+
return "StackInitFailed";
|
|
34
|
+
case MIK_ERR_BLE_CONTROLLER_INIT_FAILED:
|
|
35
|
+
return "ControllerInitFailed";
|
|
36
|
+
case MIK_ERR_BLE_GATT_REGISTRATION_FAILED:
|
|
37
|
+
return "GattRegistrationFailed";
|
|
38
|
+
case MIK_ERR_BLE_GATT_ALREADY_REGISTERED:
|
|
39
|
+
return "GattAlreadyRegistered";
|
|
40
|
+
case MIK_ERR_BLE_INVALID_UUID:
|
|
41
|
+
return "InvalidUuid";
|
|
42
|
+
default:
|
|
43
|
+
return "StackInitFailed"; /* safest fallback */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Dynamic module data slot, allocated on first import */
|
|
48
|
+
static int mik__ble_slot = -1;
|
|
49
|
+
|
|
50
|
+
/* Per-runtime state. NimBLE is a single-instance stack with one global radio,
|
|
51
|
+
* so per-runtime state is just a lifecycle hook. */
|
|
52
|
+
struct MIKBleState {
|
|
53
|
+
uint8_t _unused;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
static inline MIKBleState*& mik__ble_st(MIKRuntime* rt) {
|
|
57
|
+
return reinterpret_cast<MIKBleState*&>(rt->module_data[mik__ble_slot]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#define MIK_BLE_TAG "native:ble"
|
|
61
|
+
|
|
62
|
+
/* Max advertising payload (31 bytes) minus mandatory flags overhead (3 bytes). */
|
|
63
|
+
#define MIK_BLE_NAME_MAX_LEN 29
|
|
64
|
+
|
|
65
|
+
/* Characteristic property bitmask — must match the PROP_* constants in ble.ts. */
|
|
66
|
+
enum MIKBleProp : uint8_t {
|
|
67
|
+
MIK_BLE_PROP_READ = 0x01,
|
|
68
|
+
MIK_BLE_PROP_WRITE = 0x02,
|
|
69
|
+
MIK_BLE_PROP_WRITE_WITHOUT_RESP = 0x04,
|
|
70
|
+
MIK_BLE_PROP_NOTIFY = 0x08,
|
|
71
|
+
MIK_BLE_PROP_INDICATE = 0x10,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/* Event queue event types. BLE GAP and GATT events fire on the NimBLE host
|
|
75
|
+
* task; we convert them into MIKBleEvent records, post to a FreeRTOS queue,
|
|
76
|
+
* and drain on the JS loop thread via mik__ble_consume. */
|
|
77
|
+
enum MIKBleEventType : uint8_t {
|
|
78
|
+
MIK_BLE_EVT_CONNECT = 0,
|
|
79
|
+
MIK_BLE_EVT_DISCONNECT = 1,
|
|
80
|
+
MIK_BLE_EVT_MTU = 2,
|
|
81
|
+
MIK_BLE_EVT_WRITE = 3,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
struct MIKBleEvent {
|
|
85
|
+
MIKBleEventType type;
|
|
86
|
+
uint16_t conn_handle;
|
|
87
|
+
uint16_t mtu; /* CONNECT, DISCONNECT, MTU */
|
|
88
|
+
uint8_t peer_addr[6]; /* CONNECT, DISCONNECT (zero for MTU) */
|
|
89
|
+
uint8_t disconnect_reason; /* DISCONNECT */
|
|
90
|
+
uint16_t attr_handle; /* WRITE */
|
|
91
|
+
uint8_t* write_data; /* WRITE: points into the write pool */
|
|
92
|
+
uint16_t write_data_len; /* WRITE */
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/* Write payload pool. The NimBLE host task grabs a buffer in the write
|
|
96
|
+
* access callback; the JS loop thread returns it after dispatching. Fixed
|
|
97
|
+
* size avoids heap fragmentation on the hot path. Drop-on-exhaustion. */
|
|
98
|
+
#define MIK_BLE_WRITE_POOL_SIZE 8
|
|
99
|
+
#define MIK_BLE_WRITE_POOL_BUF_SIZE 256
|
|
100
|
+
|
|
101
|
+
/* Event queue depth. BLE events can burst (connect then MTU then first write
|
|
102
|
+
* within milliseconds); 16 is roughly double what wifi uses. */
|
|
103
|
+
#define MIK_BLE_EVENT_QUEUE_DEPTH 16
|
|
104
|
+
|
|
105
|
+
/* Per-connection tracking. NimBLE's default max connections is usually 3–4;
|
|
106
|
+
* a fixed array of 4 slots matches the common case and avoids allocation
|
|
107
|
+
* on connect. */
|
|
108
|
+
#define MIK_BLE_MAX_CONNECTIONS 4
|
|
109
|
+
|
|
110
|
+
/* Maximum characteristics per registered GATT table, bounded by the uint32
|
|
111
|
+
* subscription bitmap width. 32 chars is plenty for typical peripherals. */
|
|
112
|
+
#define MIK_BLE_MAX_CHARS 32
|
|
113
|
+
|
|
114
|
+
/* ── GATT table data structures ────────────────────────────────────── */
|
|
115
|
+
|
|
116
|
+
struct MIKBleChar {
|
|
117
|
+
ble_uuid_any_t uuid;
|
|
118
|
+
std::string uuid_str; /* for deep-equal comparison and error messages */
|
|
119
|
+
uint8_t properties; /* MIK_BLE_PROP_* bitmask */
|
|
120
|
+
uint8_t global_idx; /* 0..MIK_BLE_MAX_CHARS-1, bit index for subs bitmap */
|
|
121
|
+
std::vector<uint8_t> value;
|
|
122
|
+
uint16_t val_handle; /* filled in by NimBLE at ble_gatts_add_svcs() */
|
|
123
|
+
JSValue on_write; /* strong ref to the JS onWrite handler, or JS_UNDEFINED */
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
struct MIKBleService {
|
|
127
|
+
ble_uuid_any_t uuid;
|
|
128
|
+
std::string uuid_str;
|
|
129
|
+
std::vector<MIKBleChar> chars;
|
|
130
|
+
/* NimBLE's chr_def array for this service. Must outlive NimBLE's use of
|
|
131
|
+
* the pointers it holds. One entry per char + a zero terminator. */
|
|
132
|
+
std::vector<ble_gatt_chr_def> chr_defs;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
struct MIKBleGattTable {
|
|
136
|
+
std::vector<MIKBleService> services;
|
|
137
|
+
/* NimBLE's svc_def array. One entry per service + a zero terminator. */
|
|
138
|
+
std::vector<ble_gatt_svc_def> svc_defs;
|
|
139
|
+
/* Shared mutex for characteristic value reads/writes, held briefly during
|
|
140
|
+
* access_cb (NimBLE host task) and setValue (JS loop thread). */
|
|
141
|
+
SemaphoreHandle_t value_mutex;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/* ── Single-instance BLE stack state ───────────────────────────────── */
|
|
145
|
+
|
|
146
|
+
static bool s_nimble_initialized = false;
|
|
147
|
+
static bool s_nimble_sync_done = false;
|
|
148
|
+
static bool s_advertising = false;
|
|
149
|
+
static uint8_t s_own_addr_type = 0;
|
|
150
|
+
static SemaphoreHandle_t s_sync_sem = nullptr;
|
|
151
|
+
static char s_device_name[MIK_BLE_NAME_MAX_LEN + 1] = {};
|
|
152
|
+
static MIKBleGattTable* s_gatt_table = nullptr;
|
|
153
|
+
|
|
154
|
+
/* Event queue, shared between NimBLE host task (producers) and the JS
|
|
155
|
+
* loop thread (consumer). */
|
|
156
|
+
static QueueHandle_t s_ble_event_queue = nullptr;
|
|
157
|
+
|
|
158
|
+
/* Write payload pool. The mux guards both the data slots and the usage
|
|
159
|
+
* bitmap; critical sections are microseconds long. The data/usage buffers
|
|
160
|
+
* are heap-allocated lazily in mik__ble_ensure_initialized so builds that
|
|
161
|
+
* never activate BLE don't pay for ~2 KB of .bss. */
|
|
162
|
+
static uint8_t (*s_write_pool_data)[MIK_BLE_WRITE_POOL_BUF_SIZE] = nullptr;
|
|
163
|
+
static bool* s_write_pool_used = nullptr;
|
|
164
|
+
static portMUX_TYPE s_write_pool_mux = portMUX_INITIALIZER_UNLOCKED;
|
|
165
|
+
|
|
166
|
+
/* JS event listeners. BLE state is global (single stack), so these live
|
|
167
|
+
* outside the MIKBleState struct. Accessed only from the JS loop thread. */
|
|
168
|
+
static std::vector<JSValue> s_on_connect;
|
|
169
|
+
static std::vector<JSValue> s_on_disconnect;
|
|
170
|
+
static std::vector<JSValue> s_on_mtu;
|
|
171
|
+
|
|
172
|
+
/* Per-connection tracking. Updated from the NimBLE host task in the GAP
|
|
173
|
+
* event callback; read from the JS loop thread in the notify method. The
|
|
174
|
+
* small critical section protects concurrent access — a spinlock is plenty
|
|
175
|
+
* for this scale. */
|
|
176
|
+
struct MIKBleConnection {
|
|
177
|
+
bool active;
|
|
178
|
+
uint16_t handle;
|
|
179
|
+
uint8_t peer_addr[6];
|
|
180
|
+
uint16_t mtu;
|
|
181
|
+
uint32_t notify_subs; /* bit i = subscribed to notify on char with idx i */
|
|
182
|
+
uint32_t indicate_subs; /* bit i = subscribed to indicate on char with idx i */
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/* Heap-allocated in mik__ble_ensure_initialized alongside the write pool. */
|
|
186
|
+
static MIKBleConnection* s_connections = nullptr;
|
|
187
|
+
static portMUX_TYPE s_connections_mux = portMUX_INITIALIZER_UNLOCKED;
|
|
188
|
+
|
|
189
|
+
/* Forward declarations */
|
|
190
|
+
static void mik__ble_host_task(void* param);
|
|
191
|
+
static void mik__ble_on_sync(void);
|
|
192
|
+
static void mik__ble_on_reset(int reason);
|
|
193
|
+
static int mik__ble_gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle,
|
|
194
|
+
struct ble_gatt_access_ctxt* ctxt, void* arg);
|
|
195
|
+
static int mik__ble_gap_event_cb(struct ble_gap_event* event, void* arg);
|
|
196
|
+
static MIKBleChar* mik__ble_find_char_by_handle(uint16_t attr_handle);
|
|
197
|
+
|
|
198
|
+
/* ── Write pool ─────────────────────────────────────────────────────── */
|
|
199
|
+
|
|
200
|
+
static uint8_t* mik__ble_pool_alloc(void) {
|
|
201
|
+
portENTER_CRITICAL(&s_write_pool_mux);
|
|
202
|
+
for (int i = 0; i < MIK_BLE_WRITE_POOL_SIZE; i++) {
|
|
203
|
+
if (!s_write_pool_used[i]) {
|
|
204
|
+
s_write_pool_used[i] = true;
|
|
205
|
+
portEXIT_CRITICAL(&s_write_pool_mux);
|
|
206
|
+
return s_write_pool_data[i];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
portEXIT_CRITICAL(&s_write_pool_mux);
|
|
210
|
+
return nullptr;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static void mik__ble_pool_free(uint8_t* buf) {
|
|
214
|
+
if (!buf) return;
|
|
215
|
+
portENTER_CRITICAL(&s_write_pool_mux);
|
|
216
|
+
for (int i = 0; i < MIK_BLE_WRITE_POOL_SIZE; i++) {
|
|
217
|
+
if (s_write_pool_data[i] == buf) {
|
|
218
|
+
s_write_pool_used[i] = false;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
portEXIT_CRITICAL(&s_write_pool_mux);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ── Per-connection tracking ───────────────────────────────────────── */
|
|
226
|
+
|
|
227
|
+
/* Find or allocate a connection slot by conn_handle. Returns nullptr if
|
|
228
|
+
* all slots are full. Must be called with s_connections_mux held. */
|
|
229
|
+
static MIKBleConnection* mik__ble_conn_find_or_alloc_locked(uint16_t handle) {
|
|
230
|
+
for (int i = 0; i < MIK_BLE_MAX_CONNECTIONS; i++) {
|
|
231
|
+
if (s_connections[i].active && s_connections[i].handle == handle) {
|
|
232
|
+
return &s_connections[i];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (int i = 0; i < MIK_BLE_MAX_CONNECTIONS; i++) {
|
|
236
|
+
if (!s_connections[i].active) {
|
|
237
|
+
s_connections[i] = {};
|
|
238
|
+
s_connections[i].active = true;
|
|
239
|
+
s_connections[i].handle = handle;
|
|
240
|
+
return &s_connections[i];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return nullptr;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static MIKBleConnection* mik__ble_conn_find_locked(uint16_t handle) {
|
|
247
|
+
for (int i = 0; i < MIK_BLE_MAX_CONNECTIONS; i++) {
|
|
248
|
+
if (s_connections[i].active && s_connections[i].handle == handle) {
|
|
249
|
+
return &s_connections[i];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return nullptr;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* ── Helpers ────────────────────────────────────────────────────────── */
|
|
256
|
+
|
|
257
|
+
static void mik__ble_set_default_name(void) {
|
|
258
|
+
if (s_device_name[0] != '\0') return;
|
|
259
|
+
uint8_t mac[6] = {};
|
|
260
|
+
esp_err_t err = esp_read_mac(mac, ESP_MAC_BT);
|
|
261
|
+
if (err == ESP_OK) {
|
|
262
|
+
snprintf(s_device_name, sizeof(s_device_name), "mikrojs-%02x%02x%02x", mac[3], mac[4],
|
|
263
|
+
mac[5]);
|
|
264
|
+
} else {
|
|
265
|
+
snprintf(s_device_name, sizeof(s_device_name), "mikrojs");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* Parse a UUID string into a ble_uuid_any_t. Accepts 4-char 16-bit form
|
|
270
|
+
* (e.g. "180f") or the 36-char canonical 128-bit form with dashes. The JS
|
|
271
|
+
* wrapper does stricter validation; this parser trusts well-formed input
|
|
272
|
+
* and returns -1 only for shapes it cannot handle. */
|
|
273
|
+
static int mik__ble_parse_uuid(const char* str, ble_uuid_any_t* out) {
|
|
274
|
+
if (!str) return -1;
|
|
275
|
+
size_t len = strlen(str);
|
|
276
|
+
|
|
277
|
+
if (len == 4) {
|
|
278
|
+
for (size_t i = 0; i < 4; i++) {
|
|
279
|
+
if (!isxdigit(static_cast<unsigned char>(str[i]))) return -1;
|
|
280
|
+
}
|
|
281
|
+
uint16_t val = static_cast<uint16_t>(strtoul(str, nullptr, 16));
|
|
282
|
+
out->u16.u.type = BLE_UUID_TYPE_16;
|
|
283
|
+
out->u16.value = val;
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (len == 36 && str[8] == '-' && str[13] == '-' && str[18] == '-' && str[23] == '-') {
|
|
288
|
+
/* Parse into a network-order (big-endian) byte buffer first. */
|
|
289
|
+
uint8_t bytes[16] = {};
|
|
290
|
+
const char* p = str;
|
|
291
|
+
for (int i = 0; i < 16; i++) {
|
|
292
|
+
if (*p == '-') p++;
|
|
293
|
+
if (!isxdigit(static_cast<unsigned char>(p[0])) ||
|
|
294
|
+
!isxdigit(static_cast<unsigned char>(p[1]))) {
|
|
295
|
+
return -1;
|
|
296
|
+
}
|
|
297
|
+
char pair[3] = {p[0], p[1], 0};
|
|
298
|
+
bytes[i] = static_cast<uint8_t>(strtoul(pair, nullptr, 16));
|
|
299
|
+
p += 2;
|
|
300
|
+
}
|
|
301
|
+
/* NimBLE stores 128-bit UUIDs in little-endian — reverse the string's
|
|
302
|
+
* byte order when populating ble_uuid128_t.value. */
|
|
303
|
+
out->u128.u.type = BLE_UUID_TYPE_128;
|
|
304
|
+
for (int i = 0; i < 16; i++) {
|
|
305
|
+
out->u128.value[i] = bytes[15 - i];
|
|
306
|
+
}
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return -1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Map our property bitmask to NimBLE's ble_gatt_chr_flags. */
|
|
314
|
+
static ble_gatt_chr_flags mik__ble_translate_properties(uint8_t props) {
|
|
315
|
+
ble_gatt_chr_flags flags = 0;
|
|
316
|
+
if (props & MIK_BLE_PROP_READ) flags |= BLE_GATT_CHR_F_READ;
|
|
317
|
+
if (props & MIK_BLE_PROP_WRITE) flags |= BLE_GATT_CHR_F_WRITE;
|
|
318
|
+
if (props & MIK_BLE_PROP_WRITE_WITHOUT_RESP) flags |= BLE_GATT_CHR_F_WRITE_NO_RSP;
|
|
319
|
+
if (props & MIK_BLE_PROP_NOTIFY) flags |= BLE_GATT_CHR_F_NOTIFY;
|
|
320
|
+
if (props & MIK_BLE_PROP_INDICATE) flags |= BLE_GATT_CHR_F_INDICATE;
|
|
321
|
+
return flags;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* Deep-equal comparison of two GATT tables by UUID strings and property
|
|
325
|
+
* bitmasks. Used to reject advertise() calls that try to register a
|
|
326
|
+
* different service set without a prior ble.stop(). */
|
|
327
|
+
static bool mik__ble_tables_match(const MIKBleGattTable* a, const MIKBleGattTable* b) {
|
|
328
|
+
if (a->services.size() != b->services.size()) return false;
|
|
329
|
+
for (size_t i = 0; i < a->services.size(); i++) {
|
|
330
|
+
const auto& sa = a->services[i];
|
|
331
|
+
const auto& sb = b->services[i];
|
|
332
|
+
if (sa.uuid_str != sb.uuid_str) return false;
|
|
333
|
+
if (sa.chars.size() != sb.chars.size()) return false;
|
|
334
|
+
for (size_t j = 0; j < sa.chars.size(); j++) {
|
|
335
|
+
if (sa.chars[j].uuid_str != sb.chars[j].uuid_str) return false;
|
|
336
|
+
if (sa.chars[j].properties != sb.chars[j].properties) return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
static void mik__ble_free_gatt_table(JSContext* ctx, MIKBleGattTable* table) {
|
|
343
|
+
if (!table) return;
|
|
344
|
+
if (ctx) {
|
|
345
|
+
for (auto& svc : table->services) {
|
|
346
|
+
for (auto& chr : svc.chars) {
|
|
347
|
+
if (!JS_IsUndefined(chr.on_write)) {
|
|
348
|
+
JS_FreeValue(ctx, chr.on_write);
|
|
349
|
+
chr.on_write = JS_UNDEFINED;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (table->value_mutex) {
|
|
355
|
+
vSemaphoreDelete(table->value_mutex);
|
|
356
|
+
}
|
|
357
|
+
delete table;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Build a GATT table from a JS services array. The JS wrapper has already
|
|
361
|
+
* validated UUIDs, properties, and shape — this function trusts the input
|
|
362
|
+
* and returns a MIK_ERR_BLE_* code only for edge cases like allocation
|
|
363
|
+
* failures. On success, *out_table is populated and the caller owns it;
|
|
364
|
+
* on failure, *out_table is nullptr.
|
|
365
|
+
*
|
|
366
|
+
* Returns 0 on success or a MIK_ERR_BLE_* code on failure. */
|
|
367
|
+
static int mik__ble_build_gatt_table(JSContext* ctx, JSValue services_val,
|
|
368
|
+
MIKBleGattTable** out_table) {
|
|
369
|
+
*out_table = nullptr;
|
|
370
|
+
if (!JS_IsArray(services_val)) {
|
|
371
|
+
return MIK_ERR_BLE_GATT_REGISTRATION_FAILED;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
JSValue len_val = JS_GetPropertyStr(ctx, services_val, "length");
|
|
375
|
+
uint32_t service_count = 0;
|
|
376
|
+
JS_ToUint32(ctx, &service_count, len_val);
|
|
377
|
+
JS_FreeValue(ctx, len_val);
|
|
378
|
+
|
|
379
|
+
if (service_count == 0) {
|
|
380
|
+
/* Empty services — treat as broadcaster mode, no GATT registration. */
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
auto* table = new MIKBleGattTable();
|
|
385
|
+
table->services.reserve(service_count);
|
|
386
|
+
table->svc_defs.reserve(service_count + 1);
|
|
387
|
+
|
|
388
|
+
/* Running counter for the global characteristic index, used as the
|
|
389
|
+
* bit position in per-connection subscription bitmaps. Capped at
|
|
390
|
+
* MIK_BLE_MAX_CHARS; any overflow fails the entire registration. */
|
|
391
|
+
uint8_t next_global_idx = 0;
|
|
392
|
+
|
|
393
|
+
for (uint32_t i = 0; i < service_count; i++) {
|
|
394
|
+
JSValue svc_val = JS_GetPropertyUint32(ctx, services_val, i);
|
|
395
|
+
|
|
396
|
+
/* Service UUID */
|
|
397
|
+
JSValue svc_uuid_val = JS_GetPropertyStr(ctx, svc_val, "uuid");
|
|
398
|
+
const char* svc_uuid_cstr = JS_ToCString(ctx, svc_uuid_val);
|
|
399
|
+
if (!svc_uuid_cstr) {
|
|
400
|
+
JS_FreeValue(ctx, svc_uuid_val);
|
|
401
|
+
JS_FreeValue(ctx, svc_val);
|
|
402
|
+
mik__ble_free_gatt_table(ctx, table);
|
|
403
|
+
return MIK_ERR_BLE_INVALID_UUID;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
MIKBleService& service = table->services.emplace_back();
|
|
407
|
+
service.uuid_str = svc_uuid_cstr;
|
|
408
|
+
if (mik__ble_parse_uuid(svc_uuid_cstr, &service.uuid) != 0) {
|
|
409
|
+
JS_FreeCString(ctx, svc_uuid_cstr);
|
|
410
|
+
JS_FreeValue(ctx, svc_uuid_val);
|
|
411
|
+
JS_FreeValue(ctx, svc_val);
|
|
412
|
+
mik__ble_free_gatt_table(ctx, table);
|
|
413
|
+
return MIK_ERR_BLE_INVALID_UUID;
|
|
414
|
+
}
|
|
415
|
+
JS_FreeCString(ctx, svc_uuid_cstr);
|
|
416
|
+
JS_FreeValue(ctx, svc_uuid_val);
|
|
417
|
+
|
|
418
|
+
/* Characteristics array */
|
|
419
|
+
JSValue chars_val = JS_GetPropertyStr(ctx, svc_val, "characteristics");
|
|
420
|
+
JSValue chars_len_val = JS_GetPropertyStr(ctx, chars_val, "length");
|
|
421
|
+
uint32_t char_count = 0;
|
|
422
|
+
JS_ToUint32(ctx, &char_count, chars_len_val);
|
|
423
|
+
JS_FreeValue(ctx, chars_len_val);
|
|
424
|
+
|
|
425
|
+
service.chars.reserve(char_count);
|
|
426
|
+
service.chr_defs.reserve(char_count + 1);
|
|
427
|
+
|
|
428
|
+
for (uint32_t j = 0; j < char_count; j++) {
|
|
429
|
+
JSValue char_val = JS_GetPropertyUint32(ctx, chars_val, j);
|
|
430
|
+
|
|
431
|
+
/* Char UUID */
|
|
432
|
+
JSValue char_uuid_val = JS_GetPropertyStr(ctx, char_val, "uuid");
|
|
433
|
+
const char* char_uuid_cstr = JS_ToCString(ctx, char_uuid_val);
|
|
434
|
+
if (!char_uuid_cstr) {
|
|
435
|
+
JS_FreeValue(ctx, char_uuid_val);
|
|
436
|
+
JS_FreeValue(ctx, char_val);
|
|
437
|
+
JS_FreeValue(ctx, chars_val);
|
|
438
|
+
JS_FreeValue(ctx, svc_val);
|
|
439
|
+
mik__ble_free_gatt_table(ctx, table);
|
|
440
|
+
return MIK_ERR_BLE_INVALID_UUID;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (next_global_idx >= MIK_BLE_MAX_CHARS) {
|
|
444
|
+
JS_FreeValue(ctx, char_val);
|
|
445
|
+
JS_FreeValue(ctx, chars_val);
|
|
446
|
+
JS_FreeValue(ctx, svc_val);
|
|
447
|
+
mik__ble_free_gatt_table(ctx, table);
|
|
448
|
+
return MIK_ERR_BLE_GATT_REGISTRATION_FAILED;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
MIKBleChar& chr = service.chars.emplace_back();
|
|
452
|
+
chr.uuid_str = char_uuid_cstr;
|
|
453
|
+
chr.val_handle = 0;
|
|
454
|
+
chr.global_idx = next_global_idx++;
|
|
455
|
+
chr.on_write = JS_UNDEFINED;
|
|
456
|
+
|
|
457
|
+
if (mik__ble_parse_uuid(char_uuid_cstr, &chr.uuid) != 0) {
|
|
458
|
+
JS_FreeCString(ctx, char_uuid_cstr);
|
|
459
|
+
JS_FreeValue(ctx, char_uuid_val);
|
|
460
|
+
JS_FreeValue(ctx, char_val);
|
|
461
|
+
JS_FreeValue(ctx, chars_val);
|
|
462
|
+
JS_FreeValue(ctx, svc_val);
|
|
463
|
+
mik__ble_free_gatt_table(ctx, table);
|
|
464
|
+
return MIK_ERR_BLE_INVALID_UUID;
|
|
465
|
+
}
|
|
466
|
+
JS_FreeCString(ctx, char_uuid_cstr);
|
|
467
|
+
JS_FreeValue(ctx, char_uuid_val);
|
|
468
|
+
|
|
469
|
+
/* Properties bitmask */
|
|
470
|
+
JSValue props_val = JS_GetPropertyStr(ctx, char_val, "properties");
|
|
471
|
+
uint32_t props = 0;
|
|
472
|
+
JS_ToUint32(ctx, &props, props_val);
|
|
473
|
+
chr.properties = static_cast<uint8_t>(props);
|
|
474
|
+
JS_FreeValue(ctx, props_val);
|
|
475
|
+
|
|
476
|
+
/* Initial value (optional) */
|
|
477
|
+
JSValue value_val = JS_GetPropertyStr(ctx, char_val, "value");
|
|
478
|
+
if (!JS_IsUndefined(value_val) && !JS_IsNull(value_val)) {
|
|
479
|
+
size_t vlen = 0;
|
|
480
|
+
const uint8_t* vbuf = JS_GetUint8Array(ctx, &vlen, value_val);
|
|
481
|
+
if (vbuf && vlen > 0) {
|
|
482
|
+
chr.value.assign(vbuf, vbuf + vlen);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
JS_FreeValue(ctx, value_val);
|
|
486
|
+
|
|
487
|
+
/* onWrite handler (optional). Dup so we can call it later
|
|
488
|
+
* from the loop consumer. Freed in mik__ble_free_gatt_table. */
|
|
489
|
+
JSValue on_write_val = JS_GetPropertyStr(ctx, char_val, "onWrite");
|
|
490
|
+
if (JS_IsFunction(ctx, on_write_val)) {
|
|
491
|
+
chr.on_write = JS_DupValue(ctx, on_write_val);
|
|
492
|
+
}
|
|
493
|
+
JS_FreeValue(ctx, on_write_val);
|
|
494
|
+
|
|
495
|
+
JS_FreeValue(ctx, char_val);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/* Now build chr_defs pointing into service.chars. The chars vector
|
|
499
|
+
* has its final size (we reserved upfront), so element addresses
|
|
500
|
+
* are stable. */
|
|
501
|
+
for (size_t j = 0; j < service.chars.size(); j++) {
|
|
502
|
+
ble_gatt_chr_def chr_def = {};
|
|
503
|
+
chr_def.uuid = &service.chars[j].uuid.u;
|
|
504
|
+
chr_def.access_cb = mik__ble_gatt_access_cb;
|
|
505
|
+
chr_def.arg = &service.chars[j];
|
|
506
|
+
chr_def.flags = mik__ble_translate_properties(service.chars[j].properties);
|
|
507
|
+
chr_def.val_handle = &service.chars[j].val_handle;
|
|
508
|
+
service.chr_defs.push_back(chr_def);
|
|
509
|
+
}
|
|
510
|
+
service.chr_defs.push_back({}); /* zero terminator */
|
|
511
|
+
|
|
512
|
+
ble_gatt_svc_def svc_def = {};
|
|
513
|
+
svc_def.type = BLE_GATT_SVC_TYPE_PRIMARY;
|
|
514
|
+
svc_def.uuid = &service.uuid.u;
|
|
515
|
+
svc_def.characteristics = service.chr_defs.data();
|
|
516
|
+
table->svc_defs.push_back(svc_def);
|
|
517
|
+
|
|
518
|
+
JS_FreeValue(ctx, chars_val);
|
|
519
|
+
JS_FreeValue(ctx, svc_val);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
table->svc_defs.push_back({}); /* zero terminator */
|
|
523
|
+
|
|
524
|
+
table->value_mutex = xSemaphoreCreateMutex();
|
|
525
|
+
if (!table->value_mutex) {
|
|
526
|
+
mik__ble_free_gatt_table(ctx, table);
|
|
527
|
+
return MIK_ERR_BLE_GATT_REGISTRATION_FAILED;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
*out_table = table;
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/* ── GATT access callback (chunk 1 stub) ───────────────────────────── */
|
|
535
|
+
|
|
536
|
+
/* Runs on the NimBLE host task when a central reads or writes a
|
|
537
|
+
* characteristic. Chunk 1: serves the cached value buffer on read, accepts
|
|
538
|
+
* writes into the buffer without dispatching to JS. Chunks 2/3 will wire
|
|
539
|
+
* up the event-queue dispatch for onWrite. */
|
|
540
|
+
static int mik__ble_gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle,
|
|
541
|
+
struct ble_gatt_access_ctxt* ctxt, void* arg) {
|
|
542
|
+
(void)conn_handle;
|
|
543
|
+
(void)attr_handle;
|
|
544
|
+
auto* chr = static_cast<MIKBleChar*>(arg);
|
|
545
|
+
if (!chr || !s_gatt_table || !s_gatt_table->value_mutex) {
|
|
546
|
+
return BLE_ATT_ERR_UNLIKELY;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
|
550
|
+
xSemaphoreTake(s_gatt_table->value_mutex, portMAX_DELAY);
|
|
551
|
+
int rc = os_mbuf_append(ctxt->om, chr->value.data(), chr->value.size());
|
|
552
|
+
xSemaphoreGive(s_gatt_table->value_mutex);
|
|
553
|
+
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
|
557
|
+
uint16_t len = OS_MBUF_PKTLEN(ctxt->om);
|
|
558
|
+
xSemaphoreTake(s_gatt_table->value_mutex, portMAX_DELAY);
|
|
559
|
+
chr->value.resize(len);
|
|
560
|
+
uint16_t copied = 0;
|
|
561
|
+
int rc = ble_hs_mbuf_to_flat(ctxt->om, chr->value.data(), len, &copied);
|
|
562
|
+
xSemaphoreGive(s_gatt_table->value_mutex);
|
|
563
|
+
if (rc != 0) return BLE_ATT_ERR_UNLIKELY;
|
|
564
|
+
|
|
565
|
+
/* Post a WRITE event to the loop consumer so the JS onWrite handler
|
|
566
|
+
* runs on the JS thread. Copy the bytes into a pool-allocated buffer
|
|
567
|
+
* so the event struct can travel through the FreeRTOS queue. Drop
|
|
568
|
+
* silently if the pool is exhausted or the data exceeds one buffer
|
|
569
|
+
* (rare — pool bufs are 256 bytes, bigger than default MTU). */
|
|
570
|
+
if (s_ble_event_queue && len > 0 && len <= MIK_BLE_WRITE_POOL_BUF_SIZE) {
|
|
571
|
+
uint8_t* pool_buf = mik__ble_pool_alloc();
|
|
572
|
+
if (pool_buf) {
|
|
573
|
+
memcpy(pool_buf, chr->value.data(), len);
|
|
574
|
+
MIKBleEvent evt = {};
|
|
575
|
+
evt.type = MIK_BLE_EVT_WRITE;
|
|
576
|
+
evt.conn_handle = conn_handle;
|
|
577
|
+
evt.attr_handle = attr_handle;
|
|
578
|
+
evt.write_data = pool_buf;
|
|
579
|
+
evt.write_data_len = len;
|
|
580
|
+
if (xQueueSend(s_ble_event_queue, &evt, 0) != pdTRUE) {
|
|
581
|
+
/* Queue full — return the buffer to the pool. */
|
|
582
|
+
mik__ble_pool_free(pool_buf);
|
|
583
|
+
ESP_LOGW(MIK_BLE_TAG, "event queue full, dropping write event");
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
ESP_LOGW(MIK_BLE_TAG, "write pool exhausted, dropping write event");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return BLE_ATT_ERR_UNLIKELY;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* ── GAP event callback ────────────────────────────────────────────── */
|
|
596
|
+
|
|
597
|
+
/* Runs on the NimBLE host task. Translates GAP events into MIKBleEvent
|
|
598
|
+
* records and posts them to the event queue for the loop consumer.
|
|
599
|
+
* Must not touch JS — all JS access happens in mik__ble_consume. */
|
|
600
|
+
static int mik__ble_gap_event_cb(struct ble_gap_event* event, void* arg) {
|
|
601
|
+
(void)arg;
|
|
602
|
+
MIKBleEvent evt = {};
|
|
603
|
+
|
|
604
|
+
switch (event->type) {
|
|
605
|
+
case BLE_GAP_EVENT_CONNECT: {
|
|
606
|
+
if (event->connect.status != 0) {
|
|
607
|
+
/* Connection attempt failed. NimBLE will have already stopped
|
|
608
|
+
* advertising; user code should call advertise() again to
|
|
609
|
+
* become discoverable again. */
|
|
610
|
+
s_advertising = false;
|
|
611
|
+
return 0;
|
|
612
|
+
}
|
|
613
|
+
struct ble_gap_conn_desc desc = {};
|
|
614
|
+
uint8_t peer_addr[6] = {};
|
|
615
|
+
if (ble_gap_conn_find(event->connect.conn_handle, &desc) == 0) {
|
|
616
|
+
memcpy(peer_addr, desc.peer_ota_addr.val, 6);
|
|
617
|
+
}
|
|
618
|
+
uint16_t mtu = ble_att_mtu(event->connect.conn_handle);
|
|
619
|
+
|
|
620
|
+
/* Track the new connection. If all slots are full the connection
|
|
621
|
+
* is still valid at the NimBLE layer, but we won't be able to
|
|
622
|
+
* route notifications to it — log a warn and continue. */
|
|
623
|
+
portENTER_CRITICAL(&s_connections_mux);
|
|
624
|
+
MIKBleConnection* conn =
|
|
625
|
+
mik__ble_conn_find_or_alloc_locked(event->connect.conn_handle);
|
|
626
|
+
if (conn) {
|
|
627
|
+
memcpy(conn->peer_addr, peer_addr, 6);
|
|
628
|
+
conn->mtu = mtu;
|
|
629
|
+
conn->notify_subs = 0;
|
|
630
|
+
conn->indicate_subs = 0;
|
|
631
|
+
}
|
|
632
|
+
portEXIT_CRITICAL(&s_connections_mux);
|
|
633
|
+
if (!conn) {
|
|
634
|
+
ESP_LOGW(MIK_BLE_TAG, "connection slots full, untracked conn %u",
|
|
635
|
+
event->connect.conn_handle);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
evt.type = MIK_BLE_EVT_CONNECT;
|
|
639
|
+
evt.conn_handle = event->connect.conn_handle;
|
|
640
|
+
memcpy(evt.peer_addr, peer_addr, 6);
|
|
641
|
+
evt.mtu = mtu;
|
|
642
|
+
/* Connection formed — NimBLE stopped advertising automatically. */
|
|
643
|
+
s_advertising = false;
|
|
644
|
+
if (s_ble_event_queue) xQueueSend(s_ble_event_queue, &evt, 0);
|
|
645
|
+
return 0;
|
|
646
|
+
}
|
|
647
|
+
case BLE_GAP_EVENT_DISCONNECT: {
|
|
648
|
+
/* Look up cached state BEFORE releasing the slot so the event
|
|
649
|
+
* reports the last-known MTU instead of 0. NimBLE's ATT context
|
|
650
|
+
* is already torn down by the time this fires, so
|
|
651
|
+
* ble_att_mtu() returns 0 here — we rely on our cache. */
|
|
652
|
+
uint16_t conn_handle = event->disconnect.conn.conn_handle;
|
|
653
|
+
uint8_t peer_addr[6] = {};
|
|
654
|
+
uint16_t cached_mtu = 0;
|
|
655
|
+
|
|
656
|
+
portENTER_CRITICAL(&s_connections_mux);
|
|
657
|
+
MIKBleConnection* conn = mik__ble_conn_find_locked(conn_handle);
|
|
658
|
+
if (conn) {
|
|
659
|
+
memcpy(peer_addr, conn->peer_addr, 6);
|
|
660
|
+
cached_mtu = conn->mtu;
|
|
661
|
+
conn->active = false;
|
|
662
|
+
} else {
|
|
663
|
+
memcpy(peer_addr, event->disconnect.conn.peer_ota_addr.val, 6);
|
|
664
|
+
}
|
|
665
|
+
portEXIT_CRITICAL(&s_connections_mux);
|
|
666
|
+
|
|
667
|
+
evt.type = MIK_BLE_EVT_DISCONNECT;
|
|
668
|
+
evt.conn_handle = conn_handle;
|
|
669
|
+
memcpy(evt.peer_addr, peer_addr, 6);
|
|
670
|
+
evt.mtu = cached_mtu;
|
|
671
|
+
evt.disconnect_reason = static_cast<uint8_t>(event->disconnect.reason & 0xff);
|
|
672
|
+
if (s_ble_event_queue) xQueueSend(s_ble_event_queue, &evt, 0);
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
case BLE_GAP_EVENT_MTU: {
|
|
676
|
+
/* Update the cached MTU on the tracked connection so the
|
|
677
|
+
* disconnect event later reflects the negotiated value, and so
|
|
678
|
+
* notify payload validation uses the right ceiling. */
|
|
679
|
+
portENTER_CRITICAL(&s_connections_mux);
|
|
680
|
+
MIKBleConnection* conn = mik__ble_conn_find_locked(event->mtu.conn_handle);
|
|
681
|
+
if (conn) conn->mtu = event->mtu.value;
|
|
682
|
+
portEXIT_CRITICAL(&s_connections_mux);
|
|
683
|
+
|
|
684
|
+
evt.type = MIK_BLE_EVT_MTU;
|
|
685
|
+
evt.conn_handle = event->mtu.conn_handle;
|
|
686
|
+
evt.mtu = event->mtu.value;
|
|
687
|
+
if (s_ble_event_queue) xQueueSend(s_ble_event_queue, &evt, 0);
|
|
688
|
+
return 0;
|
|
689
|
+
}
|
|
690
|
+
case BLE_GAP_EVENT_SUBSCRIBE: {
|
|
691
|
+
/* Flip the subscription bits in the connection's notify/indicate
|
|
692
|
+
* bitmaps. Used by mik__ble_notify to only send to subscribers. */
|
|
693
|
+
MIKBleChar* chr = mik__ble_find_char_by_handle(event->subscribe.attr_handle);
|
|
694
|
+
if (!chr) return 0;
|
|
695
|
+
|
|
696
|
+
portENTER_CRITICAL(&s_connections_mux);
|
|
697
|
+
MIKBleConnection* conn =
|
|
698
|
+
mik__ble_conn_find_locked(event->subscribe.conn_handle);
|
|
699
|
+
if (conn) {
|
|
700
|
+
uint32_t bit = 1u << chr->global_idx;
|
|
701
|
+
if (event->subscribe.cur_notify) {
|
|
702
|
+
conn->notify_subs |= bit;
|
|
703
|
+
} else {
|
|
704
|
+
conn->notify_subs &= ~bit;
|
|
705
|
+
}
|
|
706
|
+
if (event->subscribe.cur_indicate) {
|
|
707
|
+
conn->indicate_subs |= bit;
|
|
708
|
+
} else {
|
|
709
|
+
conn->indicate_subs &= ~bit;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
portEXIT_CRITICAL(&s_connections_mux);
|
|
713
|
+
return 0;
|
|
714
|
+
}
|
|
715
|
+
default:
|
|
716
|
+
return 0;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/* ── NimBLE host task + callbacks ──────────────────────────────────── */
|
|
721
|
+
|
|
722
|
+
static void mik__ble_host_task(void* param) {
|
|
723
|
+
(void)param;
|
|
724
|
+
nimble_port_run();
|
|
725
|
+
nimble_port_freertos_deinit();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
static void mik__ble_on_sync(void) {
|
|
729
|
+
int rc = ble_hs_util_ensure_addr(0);
|
|
730
|
+
if (rc == 0) {
|
|
731
|
+
rc = ble_hs_id_infer_auto(0, &s_own_addr_type);
|
|
732
|
+
}
|
|
733
|
+
if (rc != 0) {
|
|
734
|
+
ESP_LOGW(MIK_BLE_TAG, "ble_hs_id_infer_auto failed: %d", rc);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
s_nimble_sync_done = true;
|
|
738
|
+
if (s_sync_sem) {
|
|
739
|
+
xSemaphoreGive(s_sync_sem);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
static void mik__ble_on_reset(int reason) {
|
|
744
|
+
ESP_LOGW(MIK_BLE_TAG, "NimBLE host reset (reason=%d)", reason);
|
|
745
|
+
s_nimble_sync_done = false;
|
|
746
|
+
s_advertising = false;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/* ── Lazy init and teardown ────────────────────────────────────────── */
|
|
750
|
+
|
|
751
|
+
/* Initializes the NimBLE stack on first use, optionally registering a GATT
|
|
752
|
+
* table before the host task starts. Subsequent calls verify that any
|
|
753
|
+
* provided new_table matches the already-registered set; mismatches return
|
|
754
|
+
* MIK_ERR_BLE_GATT_ALREADY_REGISTERED.
|
|
755
|
+
*
|
|
756
|
+
* On success, ownership of new_table may transfer to s_gatt_table. The
|
|
757
|
+
* caller should compare s_gatt_table == new_table to decide whether to
|
|
758
|
+
* free the one it passed in.
|
|
759
|
+
*
|
|
760
|
+
* Returns 0 on success or a MIK_ERR_BLE_* code on failure. */
|
|
761
|
+
static int mik__ble_ensure_initialized(MIKBleGattTable* new_table) {
|
|
762
|
+
if (s_nimble_initialized && s_nimble_sync_done) {
|
|
763
|
+
if (new_table) {
|
|
764
|
+
if (!s_gatt_table) {
|
|
765
|
+
/* Stack came up without services, can't add them later. */
|
|
766
|
+
return MIK_ERR_BLE_GATT_ALREADY_REGISTERED;
|
|
767
|
+
}
|
|
768
|
+
if (!mik__ble_tables_match(s_gatt_table, new_table)) {
|
|
769
|
+
return MIK_ERR_BLE_GATT_ALREADY_REGISTERED;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return 0;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (!s_sync_sem) {
|
|
776
|
+
s_sync_sem = xSemaphoreCreateBinary();
|
|
777
|
+
if (!s_sync_sem) return MIK_ERR_BLE_STACK_INIT_FAILED;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (!s_nimble_initialized) {
|
|
781
|
+
/* Allocate write pool + connection table on first activation. Freed
|
|
782
|
+
* in mik__ble_teardown. Any partial-failure path below must also
|
|
783
|
+
* free these — teardown handles that unconditionally. */
|
|
784
|
+
if (!s_write_pool_data) {
|
|
785
|
+
s_write_pool_data = (uint8_t(*)[MIK_BLE_WRITE_POOL_BUF_SIZE])calloc(
|
|
786
|
+
MIK_BLE_WRITE_POOL_SIZE, MIK_BLE_WRITE_POOL_BUF_SIZE);
|
|
787
|
+
s_write_pool_used = (bool*)calloc(MIK_BLE_WRITE_POOL_SIZE, sizeof(bool));
|
|
788
|
+
s_connections =
|
|
789
|
+
(MIKBleConnection*)calloc(MIK_BLE_MAX_CONNECTIONS, sizeof(MIKBleConnection));
|
|
790
|
+
if (!s_write_pool_data || !s_write_pool_used || !s_connections) {
|
|
791
|
+
free(s_write_pool_data);
|
|
792
|
+
s_write_pool_data = nullptr;
|
|
793
|
+
free(s_write_pool_used);
|
|
794
|
+
s_write_pool_used = nullptr;
|
|
795
|
+
free(s_connections);
|
|
796
|
+
s_connections = nullptr;
|
|
797
|
+
return MIK_ERR_BLE_STACK_INIT_FAILED;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
esp_err_t err = nimble_port_init();
|
|
802
|
+
if (err != ESP_OK) {
|
|
803
|
+
ESP_LOGE(MIK_BLE_TAG, "nimble_port_init failed: %d", err);
|
|
804
|
+
return MIK_ERR_BLE_STACK_INIT_FAILED;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
ble_hs_cfg.sync_cb = mik__ble_on_sync;
|
|
808
|
+
ble_hs_cfg.reset_cb = mik__ble_on_reset;
|
|
809
|
+
|
|
810
|
+
ble_svc_gap_init();
|
|
811
|
+
ble_svc_gatt_init();
|
|
812
|
+
|
|
813
|
+
if (new_table && !new_table->svc_defs.empty()) {
|
|
814
|
+
int rc = ble_gatts_count_cfg(new_table->svc_defs.data());
|
|
815
|
+
if (rc != 0) {
|
|
816
|
+
ESP_LOGE(MIK_BLE_TAG, "ble_gatts_count_cfg failed: %d", rc);
|
|
817
|
+
return MIK_ERR_BLE_GATT_REGISTRATION_FAILED;
|
|
818
|
+
}
|
|
819
|
+
rc = ble_gatts_add_svcs(new_table->svc_defs.data());
|
|
820
|
+
if (rc != 0) {
|
|
821
|
+
ESP_LOGE(MIK_BLE_TAG, "ble_gatts_add_svcs failed: %d", rc);
|
|
822
|
+
return MIK_ERR_BLE_GATT_REGISTRATION_FAILED;
|
|
823
|
+
}
|
|
824
|
+
s_gatt_table = new_table; /* take ownership */
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
mik__ble_set_default_name();
|
|
828
|
+
ble_svc_gap_device_name_set(s_device_name);
|
|
829
|
+
|
|
830
|
+
nimble_port_freertos_init(mik__ble_host_task);
|
|
831
|
+
s_nimble_initialized = true;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (!s_nimble_sync_done) {
|
|
835
|
+
if (xSemaphoreTake(s_sync_sem, pdMS_TO_TICKS(5000)) != pdTRUE) {
|
|
836
|
+
ESP_LOGE(MIK_BLE_TAG, "NimBLE sync timeout");
|
|
837
|
+
return MIK_ERR_BLE_STACK_INIT_FAILED;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return 0;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/* Free all JS event listeners and drain pending events from the queue.
|
|
845
|
+
* Pool buffers attached to queued WRITE events are returned. */
|
|
846
|
+
static void mik__ble_cleanup_listeners_and_queue(JSContext* ctx) {
|
|
847
|
+
if (ctx) {
|
|
848
|
+
for (auto& v : s_on_connect) JS_FreeValue(ctx, v);
|
|
849
|
+
s_on_connect.clear();
|
|
850
|
+
for (auto& v : s_on_disconnect) JS_FreeValue(ctx, v);
|
|
851
|
+
s_on_disconnect.clear();
|
|
852
|
+
for (auto& v : s_on_mtu) JS_FreeValue(ctx, v);
|
|
853
|
+
s_on_mtu.clear();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (s_ble_event_queue) {
|
|
857
|
+
MIKBleEvent evt;
|
|
858
|
+
while (xQueueReceive(s_ble_event_queue, &evt, 0) == pdTRUE) {
|
|
859
|
+
if (evt.type == MIK_BLE_EVT_WRITE && evt.write_data) {
|
|
860
|
+
mik__ble_pool_free(evt.write_data);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
static esp_err_t mik__ble_teardown(JSContext* ctx) {
|
|
867
|
+
if (!s_nimble_initialized) {
|
|
868
|
+
if (s_gatt_table) {
|
|
869
|
+
mik__ble_free_gatt_table(ctx, s_gatt_table);
|
|
870
|
+
s_gatt_table = nullptr;
|
|
871
|
+
}
|
|
872
|
+
mik__ble_cleanup_listeners_and_queue(ctx);
|
|
873
|
+
/* Release lazy-allocated buffers if init bailed partway. */
|
|
874
|
+
free(s_write_pool_data);
|
|
875
|
+
s_write_pool_data = nullptr;
|
|
876
|
+
free(s_write_pool_used);
|
|
877
|
+
s_write_pool_used = nullptr;
|
|
878
|
+
free(s_connections);
|
|
879
|
+
s_connections = nullptr;
|
|
880
|
+
return ESP_OK;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (s_advertising) {
|
|
884
|
+
ble_gap_adv_stop();
|
|
885
|
+
s_advertising = false;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
int rc = nimble_port_stop();
|
|
889
|
+
if (rc == 0) {
|
|
890
|
+
nimble_port_deinit();
|
|
891
|
+
} else {
|
|
892
|
+
ESP_LOGW(MIK_BLE_TAG, "nimble_port_stop failed: %d", rc);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* Disable + deinit the BT controller to reclaim RAM. These calls live
|
|
896
|
+
* in mik_ble_c_shim.c because esp_bt.h cannot be included from C++. */
|
|
897
|
+
mik_ble_controller_disable();
|
|
898
|
+
mik_ble_controller_deinit();
|
|
899
|
+
|
|
900
|
+
if (s_sync_sem) {
|
|
901
|
+
vSemaphoreDelete(s_sync_sem);
|
|
902
|
+
s_sync_sem = nullptr;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (s_gatt_table) {
|
|
906
|
+
mik__ble_free_gatt_table(ctx, s_gatt_table);
|
|
907
|
+
s_gatt_table = nullptr;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
mik__ble_cleanup_listeners_and_queue(ctx);
|
|
911
|
+
|
|
912
|
+
/* Host task + GATT callbacks are gone after nimble_port_deinit, so
|
|
913
|
+
* nothing else can touch these buffers. Free unconditionally — the
|
|
914
|
+
* spinlock is unneeded once producers/consumers are torn down. */
|
|
915
|
+
free(s_write_pool_data);
|
|
916
|
+
s_write_pool_data = nullptr;
|
|
917
|
+
free(s_write_pool_used);
|
|
918
|
+
s_write_pool_used = nullptr;
|
|
919
|
+
free(s_connections);
|
|
920
|
+
s_connections = nullptr;
|
|
921
|
+
|
|
922
|
+
s_nimble_initialized = false;
|
|
923
|
+
s_nimble_sync_done = false;
|
|
924
|
+
return ESP_OK;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/* ── Method implementations ────────────────────────────────────────── */
|
|
928
|
+
|
|
929
|
+
static JSValue mik__ble_get_name(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
930
|
+
(void)this_val;
|
|
931
|
+
(void)argc;
|
|
932
|
+
(void)argv;
|
|
933
|
+
if (s_device_name[0] == '\0') {
|
|
934
|
+
mik__ble_set_default_name();
|
|
935
|
+
}
|
|
936
|
+
return JS_NewString(ctx, s_device_name);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
static JSValue mik__ble_set_name(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
940
|
+
(void)this_val;
|
|
941
|
+
if (argc < 1) {
|
|
942
|
+
return mik__result_err_named(ctx, "SetFailed", "missing name argument");
|
|
943
|
+
}
|
|
944
|
+
const char* name = JS_ToCString(ctx, argv[0]);
|
|
945
|
+
if (!name) return JS_EXCEPTION;
|
|
946
|
+
|
|
947
|
+
strncpy(s_device_name, name, sizeof(s_device_name) - 1);
|
|
948
|
+
s_device_name[sizeof(s_device_name) - 1] = '\0';
|
|
949
|
+
JS_FreeCString(ctx, name);
|
|
950
|
+
|
|
951
|
+
if (s_nimble_initialized) {
|
|
952
|
+
int rc = ble_svc_gap_device_name_set(s_device_name);
|
|
953
|
+
if (rc != 0) {
|
|
954
|
+
return mik__result_err_named(ctx, "SetFailed",
|
|
955
|
+
"ble_svc_gap_device_name_set failed (rc=%d)", rc);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return mik__result_ok_void(ctx);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
static JSValue mik__ble_get_address(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
962
|
+
(void)this_val;
|
|
963
|
+
(void)argc;
|
|
964
|
+
(void)argv;
|
|
965
|
+
|
|
966
|
+
/* Read the BT MAC directly from efuse. This does not require the NimBLE
|
|
967
|
+
* stack to be running — so callers can read the address without locking
|
|
968
|
+
* themselves into broadcaster-only mode. */
|
|
969
|
+
uint8_t addr[6] = {};
|
|
970
|
+
esp_err_t err = esp_read_mac(addr, ESP_MAC_BT);
|
|
971
|
+
if (err != ESP_OK) {
|
|
972
|
+
return mik__result_err_named(ctx, "GetFailed",
|
|
973
|
+
"failed to read BT MAC address (err=0x%x)", err);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
char addr_str[18];
|
|
977
|
+
snprintf(addr_str, sizeof(addr_str), "%02x:%02x:%02x:%02x:%02x:%02x", addr[0], addr[1],
|
|
978
|
+
addr[2], addr[3], addr[4], addr[5]);
|
|
979
|
+
return mik__result_ok(ctx, JS_NewString(ctx, addr_str));
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
static JSValue mik__ble_get_tx_power(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
983
|
+
(void)this_val;
|
|
984
|
+
(void)argc;
|
|
985
|
+
(void)argv;
|
|
986
|
+
|
|
987
|
+
int dbm = mik_ble_get_tx_power_dbm();
|
|
988
|
+
return mik__result_ok(ctx, JS_NewInt32(ctx, dbm));
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
static JSValue mik__ble_set_tx_power(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
992
|
+
(void)this_val;
|
|
993
|
+
if (argc < 1) {
|
|
994
|
+
return mik__result_err_named(ctx, "SetFailed", "missing txPower argument");
|
|
995
|
+
}
|
|
996
|
+
int32_t dbm;
|
|
997
|
+
if (JS_ToInt32(ctx, &dbm, argv[0]) != 0) return JS_EXCEPTION;
|
|
998
|
+
|
|
999
|
+
int rc = mik_ble_set_tx_power_dbm(static_cast<int>(dbm));
|
|
1000
|
+
if (rc != 0) {
|
|
1001
|
+
return mik__result_err_named(ctx, "SetFailed",
|
|
1002
|
+
"failed to set TX power to %d dBm (rc=%d)", (int)dbm, rc);
|
|
1003
|
+
}
|
|
1004
|
+
return mik__result_ok_void(ctx);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
static JSValue mik__ble_advertise(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
1008
|
+
(void)this_val;
|
|
1009
|
+
|
|
1010
|
+
JSValue opts = argc > 0 ? argv[0] : JS_UNDEFINED;
|
|
1011
|
+
|
|
1012
|
+
/* Parse GATT services from the options (if any). Ownership of new_table
|
|
1013
|
+
* transfers to s_gatt_table on successful first-time registration. */
|
|
1014
|
+
MIKBleGattTable* new_table = nullptr;
|
|
1015
|
+
if (JS_IsObject(opts)) {
|
|
1016
|
+
JSValue services_val = JS_GetPropertyStr(ctx, opts, "services");
|
|
1017
|
+
if (JS_IsArray(services_val)) {
|
|
1018
|
+
int build_err = mik__ble_build_gatt_table(ctx, services_val, &new_table);
|
|
1019
|
+
if (build_err != 0) {
|
|
1020
|
+
JS_FreeValue(ctx, services_val);
|
|
1021
|
+
return mik__result_err_named(ctx, mik__ble_code_to_name(build_err),
|
|
1022
|
+
"failed to build GATT service table");
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
JS_FreeValue(ctx, services_val);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
int init_err = mik__ble_ensure_initialized(new_table);
|
|
1029
|
+
if (init_err != 0) {
|
|
1030
|
+
/* If new_table wasn't adopted, free it. */
|
|
1031
|
+
if (new_table && s_gatt_table != new_table) {
|
|
1032
|
+
mik__ble_free_gatt_table(ctx, new_table);
|
|
1033
|
+
}
|
|
1034
|
+
return mik__result_err_named(ctx, mik__ble_code_to_name(init_err),
|
|
1035
|
+
"BLE stack initialization failed");
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/* new_table was either adopted (s_gatt_table == new_table) or rejected
|
|
1039
|
+
* as a duplicate of the already-registered table (match case). In the
|
|
1040
|
+
* latter case, free the duplicate. */
|
|
1041
|
+
if (new_table && s_gatt_table != new_table) {
|
|
1042
|
+
mik__ble_free_gatt_table(ctx, new_table);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (s_advertising) {
|
|
1046
|
+
return mik__result_err_named(ctx, "AlreadyAdvertising",
|
|
1047
|
+
"BLE already advertising");
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
struct ble_hs_adv_fields fields = {};
|
|
1051
|
+
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
|
1052
|
+
|
|
1053
|
+
bool connectable = false;
|
|
1054
|
+
JSValue mfr_val = JS_UNDEFINED;
|
|
1055
|
+
JSValue iv_val = JS_UNDEFINED;
|
|
1056
|
+
JSValue min_val = JS_UNDEFINED;
|
|
1057
|
+
JSValue max_val = JS_UNDEFINED;
|
|
1058
|
+
|
|
1059
|
+
if (JS_IsObject(opts)) {
|
|
1060
|
+
JSValue conn_val = JS_GetPropertyStr(ctx, opts, "connectable");
|
|
1061
|
+
if (!JS_IsUndefined(conn_val)) {
|
|
1062
|
+
connectable = JS_ToBool(ctx, conn_val);
|
|
1063
|
+
}
|
|
1064
|
+
JS_FreeValue(ctx, conn_val);
|
|
1065
|
+
|
|
1066
|
+
JSValue name_val = JS_GetPropertyStr(ctx, opts, "name");
|
|
1067
|
+
if (JS_IsString(name_val)) {
|
|
1068
|
+
const char* name_str = JS_ToCString(ctx, name_val);
|
|
1069
|
+
if (name_str) {
|
|
1070
|
+
strncpy(s_device_name, name_str, sizeof(s_device_name) - 1);
|
|
1071
|
+
s_device_name[sizeof(s_device_name) - 1] = '\0';
|
|
1072
|
+
ble_svc_gap_device_name_set(s_device_name);
|
|
1073
|
+
JS_FreeCString(ctx, name_str);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
JS_FreeValue(ctx, name_val);
|
|
1077
|
+
|
|
1078
|
+
JSValue tx_val = JS_GetPropertyStr(ctx, opts, "includeTxPower");
|
|
1079
|
+
if (JS_ToBool(ctx, tx_val)) {
|
|
1080
|
+
fields.tx_pwr_lvl_is_present = 1;
|
|
1081
|
+
fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
|
1082
|
+
}
|
|
1083
|
+
JS_FreeValue(ctx, tx_val);
|
|
1084
|
+
|
|
1085
|
+
mfr_val = JS_GetPropertyStr(ctx, opts, "manufacturerData");
|
|
1086
|
+
if (!JS_IsUndefined(mfr_val) && !JS_IsNull(mfr_val)) {
|
|
1087
|
+
size_t mfr_len = 0;
|
|
1088
|
+
const uint8_t* mfr_buf = JS_GetUint8Array(ctx, &mfr_len, mfr_val);
|
|
1089
|
+
if (mfr_buf && mfr_len > 0) {
|
|
1090
|
+
fields.mfg_data = mfr_buf;
|
|
1091
|
+
fields.mfg_data_len = static_cast<uint8_t>(mfr_len);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
iv_val = JS_GetPropertyStr(ctx, opts, "interval");
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (s_device_name[0] == '\0') {
|
|
1099
|
+
mik__ble_set_default_name();
|
|
1100
|
+
ble_svc_gap_device_name_set(s_device_name);
|
|
1101
|
+
}
|
|
1102
|
+
fields.name = reinterpret_cast<const uint8_t*>(s_device_name);
|
|
1103
|
+
fields.name_len = static_cast<uint8_t>(strlen(s_device_name));
|
|
1104
|
+
fields.name_is_complete = 1;
|
|
1105
|
+
|
|
1106
|
+
int rc = ble_gap_adv_set_fields(&fields);
|
|
1107
|
+
JS_FreeValue(ctx, mfr_val);
|
|
1108
|
+
if (rc != 0) {
|
|
1109
|
+
JS_FreeValue(ctx, iv_val);
|
|
1110
|
+
return mik__result_err_named(ctx, "AdvertiseStartFailed",
|
|
1111
|
+
"ble_gap_adv_set_fields failed (rc=%d)", rc);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
struct ble_gap_adv_params adv_params = {};
|
|
1115
|
+
adv_params.conn_mode = connectable ? BLE_GAP_CONN_MODE_UND : BLE_GAP_CONN_MODE_NON;
|
|
1116
|
+
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
|
1117
|
+
|
|
1118
|
+
if (JS_IsObject(iv_val)) {
|
|
1119
|
+
min_val = JS_GetPropertyStr(ctx, iv_val, "min");
|
|
1120
|
+
max_val = JS_GetPropertyStr(ctx, iv_val, "max");
|
|
1121
|
+
double min_ms = 0;
|
|
1122
|
+
double max_ms = 0;
|
|
1123
|
+
if (JS_ToFloat64(ctx, &min_ms, min_val) == 0 &&
|
|
1124
|
+
JS_ToFloat64(ctx, &max_ms, max_val) == 0) {
|
|
1125
|
+
adv_params.itvl_min = static_cast<uint16_t>(min_ms / 0.625);
|
|
1126
|
+
adv_params.itvl_max = static_cast<uint16_t>(max_ms / 0.625);
|
|
1127
|
+
}
|
|
1128
|
+
JS_FreeValue(ctx, min_val);
|
|
1129
|
+
JS_FreeValue(ctx, max_val);
|
|
1130
|
+
}
|
|
1131
|
+
JS_FreeValue(ctx, iv_val);
|
|
1132
|
+
|
|
1133
|
+
rc = ble_gap_adv_start(s_own_addr_type, nullptr, BLE_HS_FOREVER, &adv_params,
|
|
1134
|
+
mik__ble_gap_event_cb, nullptr);
|
|
1135
|
+
if (rc != 0) {
|
|
1136
|
+
return mik__result_err_named(ctx, "AdvertiseStartFailed",
|
|
1137
|
+
"ble_gap_adv_start failed (rc=%d)", rc);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
s_advertising = true;
|
|
1141
|
+
return mik__result_ok_void(ctx);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
static JSValue mik__ble_stop_advertising(JSContext* ctx, JSValue this_val, int argc,
|
|
1145
|
+
JSValue* argv) {
|
|
1146
|
+
(void)this_val;
|
|
1147
|
+
(void)argc;
|
|
1148
|
+
(void)argv;
|
|
1149
|
+
|
|
1150
|
+
if (!s_advertising) {
|
|
1151
|
+
return mik__result_ok_void(ctx);
|
|
1152
|
+
}
|
|
1153
|
+
int rc = ble_gap_adv_stop();
|
|
1154
|
+
if (rc != 0 && rc != BLE_HS_EALREADY) {
|
|
1155
|
+
return mik__result_err_named(ctx, "AdvertiseStopFailed",
|
|
1156
|
+
"ble_gap_adv_stop failed (rc=%d)", rc);
|
|
1157
|
+
}
|
|
1158
|
+
s_advertising = false;
|
|
1159
|
+
return mik__result_ok_void(ctx);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
static JSValue mik__ble_stop(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
1163
|
+
(void)this_val;
|
|
1164
|
+
(void)argc;
|
|
1165
|
+
(void)argv;
|
|
1166
|
+
|
|
1167
|
+
esp_err_t err = mik__ble_teardown(ctx);
|
|
1168
|
+
if (err != ESP_OK) {
|
|
1169
|
+
return mik__result_err_named(ctx, "StackShutdown",
|
|
1170
|
+
"BLE stack shutdown failed (err=0x%x)", err);
|
|
1171
|
+
}
|
|
1172
|
+
return mik__result_ok_void(ctx);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/* Linear scan for a characteristic by NimBLE attribute handle. Used by
|
|
1176
|
+
* the loop consumer when dispatching write events to the JS handler. */
|
|
1177
|
+
static MIKBleChar* mik__ble_find_char_by_handle(uint16_t attr_handle) {
|
|
1178
|
+
if (!s_gatt_table) return nullptr;
|
|
1179
|
+
for (auto& svc : s_gatt_table->services) {
|
|
1180
|
+
for (auto& chr : svc.chars) {
|
|
1181
|
+
if (chr.val_handle == attr_handle) return &chr;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return nullptr;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/* Linear scan over the registered GATT table. The JS wrapper normalizes
|
|
1188
|
+
* UUIDs to lowercase before calling setValue, and the native side stores
|
|
1189
|
+
* the normalized strings from the original advertise() call, so direct
|
|
1190
|
+
* string comparison is sufficient. Returns nullptr if the characteristic
|
|
1191
|
+
* is not registered. */
|
|
1192
|
+
static MIKBleChar* mik__ble_find_char(const char* svc_uuid_str,
|
|
1193
|
+
const char* chr_uuid_str) {
|
|
1194
|
+
if (!s_gatt_table || !svc_uuid_str || !chr_uuid_str) return nullptr;
|
|
1195
|
+
for (auto& svc : s_gatt_table->services) {
|
|
1196
|
+
if (svc.uuid_str != svc_uuid_str) continue;
|
|
1197
|
+
for (auto& chr : svc.chars) {
|
|
1198
|
+
if (chr.uuid_str == chr_uuid_str) return &chr;
|
|
1199
|
+
}
|
|
1200
|
+
return nullptr; /* service matched but characteristic didn't */
|
|
1201
|
+
}
|
|
1202
|
+
return nullptr;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
static std::vector<JSValue>* mik__ble_listeners_for(const char* event_name) {
|
|
1206
|
+
if (strcmp(event_name, "connect") == 0) return &s_on_connect;
|
|
1207
|
+
if (strcmp(event_name, "disconnect") == 0) return &s_on_disconnect;
|
|
1208
|
+
if (strcmp(event_name, "mtu") == 0) return &s_on_mtu;
|
|
1209
|
+
return nullptr;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
static JSValue mik__ble_on(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
1213
|
+
(void)this_val;
|
|
1214
|
+
if (argc < 2) return JS_UNDEFINED;
|
|
1215
|
+
|
|
1216
|
+
const char* event_name = JS_ToCString(ctx, argv[0]);
|
|
1217
|
+
if (!event_name) return JS_EXCEPTION;
|
|
1218
|
+
|
|
1219
|
+
JSValue func = argv[1];
|
|
1220
|
+
if (!JS_IsFunction(ctx, func)) {
|
|
1221
|
+
JS_FreeCString(ctx, event_name);
|
|
1222
|
+
return JS_ThrowTypeError(ctx, "expected argument 2 to be a function");
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
auto* listeners = mik__ble_listeners_for(event_name);
|
|
1226
|
+
if (listeners) {
|
|
1227
|
+
listeners->push_back(JS_DupValue(ctx, func));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
JS_FreeCString(ctx, event_name);
|
|
1231
|
+
return JS_UNDEFINED;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
static JSValue mik__ble_off(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
1235
|
+
(void)this_val;
|
|
1236
|
+
if (argc < 2) return JS_UNDEFINED;
|
|
1237
|
+
|
|
1238
|
+
const char* event_name = JS_ToCString(ctx, argv[0]);
|
|
1239
|
+
if (!event_name) return JS_EXCEPTION;
|
|
1240
|
+
|
|
1241
|
+
JSValue func = argv[1];
|
|
1242
|
+
auto* listeners = mik__ble_listeners_for(event_name);
|
|
1243
|
+
JS_FreeCString(ctx, event_name);
|
|
1244
|
+
|
|
1245
|
+
if (listeners) {
|
|
1246
|
+
for (auto it = listeners->begin(); it != listeners->end(); ++it) {
|
|
1247
|
+
if (JS_IsSameValue(ctx, *it, func)) {
|
|
1248
|
+
JS_FreeValue(ctx, *it);
|
|
1249
|
+
listeners->erase(it);
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return JS_UNDEFINED;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/* Builds a structured AdvertisingPayloadTooLarge-style error object for
|
|
1259
|
+
* cases where the notify payload exceeds the negotiated MTU. We can't use
|
|
1260
|
+
* the generic mik__result_err helper because ValueTooLarge carries three
|
|
1261
|
+
* structured fields (uuid, bytes, max) instead of a single message string. */
|
|
1262
|
+
static JSValue mik__ble_make_value_too_large(JSContext* ctx, const char* uuid_str,
|
|
1263
|
+
size_t bytes, size_t max) {
|
|
1264
|
+
JSValue error = JS_NewObject(ctx);
|
|
1265
|
+
JS_DefinePropertyValueStr(ctx, error, "name", JS_NewString(ctx, "ValueTooLarge"),
|
|
1266
|
+
JS_PROP_C_W_E);
|
|
1267
|
+
JS_DefinePropertyValueStr(ctx, error, "uuid", JS_NewString(ctx, uuid_str), JS_PROP_C_W_E);
|
|
1268
|
+
JS_DefinePropertyValueStr(ctx, error, "bytes", JS_NewInt32(ctx, (int32_t)bytes),
|
|
1269
|
+
JS_PROP_C_W_E);
|
|
1270
|
+
JS_DefinePropertyValueStr(ctx, error, "max", JS_NewInt32(ctx, (int32_t)max), JS_PROP_C_W_E);
|
|
1271
|
+
|
|
1272
|
+
JSValue obj = JS_NewObject(ctx);
|
|
1273
|
+
JS_DefinePropertyValueStr(ctx, obj, "ok", JS_FALSE, JS_PROP_C_W_E);
|
|
1274
|
+
JS_DefinePropertyValueStr(ctx, obj, "error", error, JS_PROP_C_W_E);
|
|
1275
|
+
return obj;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
static JSValue mik__ble_notify(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
1279
|
+
(void)this_val;
|
|
1280
|
+
if (argc < 3) {
|
|
1281
|
+
return mik__result_err_named(ctx, "NoSuchCharacteristic",
|
|
1282
|
+
"notify requires service UUID, characteristic UUID, and value");
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const char* svc_uuid = JS_ToCString(ctx, argv[0]);
|
|
1286
|
+
if (!svc_uuid) return JS_EXCEPTION;
|
|
1287
|
+
const char* chr_uuid = JS_ToCString(ctx, argv[1]);
|
|
1288
|
+
if (!chr_uuid) {
|
|
1289
|
+
JS_FreeCString(ctx, svc_uuid);
|
|
1290
|
+
return JS_EXCEPTION;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
MIKBleChar* chr = mik__ble_find_char(svc_uuid, chr_uuid);
|
|
1294
|
+
JS_FreeCString(ctx, svc_uuid);
|
|
1295
|
+
|
|
1296
|
+
if (!chr) {
|
|
1297
|
+
JS_FreeCString(ctx, chr_uuid);
|
|
1298
|
+
return mik__result_err_named(ctx, "NoSuchCharacteristic",
|
|
1299
|
+
"characteristic not found");
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (!(chr->properties & MIK_BLE_PROP_NOTIFY)) {
|
|
1303
|
+
JS_FreeCString(ctx, chr_uuid);
|
|
1304
|
+
return mik__result_err_named(ctx, "InvalidProperties",
|
|
1305
|
+
"characteristic does not have notify property");
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
size_t value_len = 0;
|
|
1309
|
+
const uint8_t* value_buf = JS_GetUint8Array(ctx, &value_len, argv[2]);
|
|
1310
|
+
if (!value_buf) {
|
|
1311
|
+
JS_FreeCString(ctx, chr_uuid);
|
|
1312
|
+
return mik__result_err_named(ctx, "SetFailed",
|
|
1313
|
+
"invalid value argument, expected Uint8Array");
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (!s_gatt_table || !s_gatt_table->value_mutex) {
|
|
1317
|
+
JS_FreeCString(ctx, chr_uuid);
|
|
1318
|
+
return mik__result_err_named(ctx, "NoSuchCharacteristic",
|
|
1319
|
+
"GATT table not initialized");
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/* Snapshot the set of subscribed connections and compute the minimum
|
|
1323
|
+
* MTU across them. Hold the critical section briefly to copy state. */
|
|
1324
|
+
struct SubSnapshot {
|
|
1325
|
+
uint16_t handle;
|
|
1326
|
+
uint16_t mtu;
|
|
1327
|
+
};
|
|
1328
|
+
SubSnapshot subs[MIK_BLE_MAX_CONNECTIONS];
|
|
1329
|
+
int sub_count = 0;
|
|
1330
|
+
uint16_t min_mtu = 0xFFFF;
|
|
1331
|
+
uint32_t bit = 1u << chr->global_idx;
|
|
1332
|
+
|
|
1333
|
+
portENTER_CRITICAL(&s_connections_mux);
|
|
1334
|
+
for (int i = 0; i < MIK_BLE_MAX_CONNECTIONS; i++) {
|
|
1335
|
+
if (!s_connections[i].active) continue;
|
|
1336
|
+
if (!(s_connections[i].notify_subs & bit)) continue;
|
|
1337
|
+
subs[sub_count].handle = s_connections[i].handle;
|
|
1338
|
+
subs[sub_count].mtu = s_connections[i].mtu;
|
|
1339
|
+
if (s_connections[i].mtu < min_mtu) min_mtu = s_connections[i].mtu;
|
|
1340
|
+
sub_count++;
|
|
1341
|
+
}
|
|
1342
|
+
portEXIT_CRITICAL(&s_connections_mux);
|
|
1343
|
+
|
|
1344
|
+
/* Always update the cached value, even if nobody is subscribed — that
|
|
1345
|
+
* way subsequent GATT reads see the current bytes, matching setValue
|
|
1346
|
+
* semantics. notify = setValue + push-to-subscribers. */
|
|
1347
|
+
xSemaphoreTake(s_gatt_table->value_mutex, portMAX_DELAY);
|
|
1348
|
+
chr->value.assign(value_buf, value_buf + value_len);
|
|
1349
|
+
xSemaphoreGive(s_gatt_table->value_mutex);
|
|
1350
|
+
|
|
1351
|
+
if (sub_count == 0) {
|
|
1352
|
+
/* Silent no-op per plan: notify with no subscribers is success. */
|
|
1353
|
+
JS_FreeCString(ctx, chr_uuid);
|
|
1354
|
+
return mik__result_ok_void(ctx);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/* Validate payload against min(subscriber MTU) - 3 (ATT header). */
|
|
1358
|
+
size_t max_bytes = min_mtu > 3 ? (size_t)(min_mtu - 3) : 0;
|
|
1359
|
+
if (value_len > max_bytes) {
|
|
1360
|
+
JSValue err = mik__ble_make_value_too_large(ctx, chr_uuid, value_len, max_bytes);
|
|
1361
|
+
JS_FreeCString(ctx, chr_uuid);
|
|
1362
|
+
return err;
|
|
1363
|
+
}
|
|
1364
|
+
JS_FreeCString(ctx, chr_uuid);
|
|
1365
|
+
|
|
1366
|
+
/* Blast notify to each subscriber. Per-subscriber failures are
|
|
1367
|
+
* logged but do not fail the overall call — notify is best-effort. */
|
|
1368
|
+
for (int i = 0; i < sub_count; i++) {
|
|
1369
|
+
struct os_mbuf* om = ble_hs_mbuf_from_flat(value_buf, value_len);
|
|
1370
|
+
if (!om) {
|
|
1371
|
+
ESP_LOGW(MIK_BLE_TAG, "notify mbuf alloc failed for conn %u", subs[i].handle);
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
int rc = ble_gatts_notify_custom(subs[i].handle, chr->val_handle, om);
|
|
1375
|
+
if (rc != 0) {
|
|
1376
|
+
ESP_LOGW(MIK_BLE_TAG, "notify to conn %u failed: %d", subs[i].handle, rc);
|
|
1377
|
+
/* NimBLE frees the mbuf on both success and failure paths for
|
|
1378
|
+
* this API, so no cleanup needed here. */
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
return mik__result_ok_void(ctx);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
static JSValue mik__ble_set_value(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
1386
|
+
(void)this_val;
|
|
1387
|
+
if (argc < 3) {
|
|
1388
|
+
return mik__result_err_named(ctx, "NoSuchCharacteristic",
|
|
1389
|
+
"setValue requires service UUID, characteristic UUID, and value");
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const char* svc_uuid = JS_ToCString(ctx, argv[0]);
|
|
1393
|
+
if (!svc_uuid) return JS_EXCEPTION;
|
|
1394
|
+
const char* chr_uuid = JS_ToCString(ctx, argv[1]);
|
|
1395
|
+
if (!chr_uuid) {
|
|
1396
|
+
JS_FreeCString(ctx, svc_uuid);
|
|
1397
|
+
return JS_EXCEPTION;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
MIKBleChar* chr = mik__ble_find_char(svc_uuid, chr_uuid);
|
|
1401
|
+
JS_FreeCString(ctx, svc_uuid);
|
|
1402
|
+
JS_FreeCString(ctx, chr_uuid);
|
|
1403
|
+
|
|
1404
|
+
if (!chr) {
|
|
1405
|
+
return mik__result_err_named(ctx, "NoSuchCharacteristic",
|
|
1406
|
+
"characteristic not found");
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
size_t value_len = 0;
|
|
1410
|
+
const uint8_t* value_buf = JS_GetUint8Array(ctx, &value_len, argv[2]);
|
|
1411
|
+
if (!value_buf) {
|
|
1412
|
+
return mik__result_err_named(ctx, "SetFailed",
|
|
1413
|
+
"invalid value argument, expected Uint8Array");
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (!s_gatt_table || !s_gatt_table->value_mutex) {
|
|
1417
|
+
return mik__result_err_named(ctx, "NoSuchCharacteristic",
|
|
1418
|
+
"GATT table not initialized");
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
xSemaphoreTake(s_gatt_table->value_mutex, portMAX_DELAY);
|
|
1422
|
+
chr->value.assign(value_buf, value_buf + value_len);
|
|
1423
|
+
xSemaphoreGive(s_gatt_table->value_mutex);
|
|
1424
|
+
|
|
1425
|
+
return mik__result_ok_void(ctx);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/* ── JS class ──────────────────────────────────────────────────────── */
|
|
1429
|
+
|
|
1430
|
+
static JSClassID mik_ble_class_id;
|
|
1431
|
+
|
|
1432
|
+
static JSClassDef mik_ble_classdef = {
|
|
1433
|
+
.class_name = "Ble",
|
|
1434
|
+
.finalizer = nullptr,
|
|
1435
|
+
.gc_mark = nullptr,
|
|
1436
|
+
.call = nullptr,
|
|
1437
|
+
.exotic = nullptr,
|
|
1438
|
+
};
|
|
1439
|
+
|
|
1440
|
+
static JSValue mik__ble_constructor(JSContext* ctx, JSValue new_target, int argc, JSValue* argv) {
|
|
1441
|
+
(void)new_target;
|
|
1442
|
+
(void)argc;
|
|
1443
|
+
(void)argv;
|
|
1444
|
+
return JS_NewObjectClass(ctx, mik_ble_class_id);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
static const JSCFunctionListEntry mik__ble_proto_funcs[] = {
|
|
1448
|
+
MIK_CFUNC_DEF("getName", 0, mik__ble_get_name),
|
|
1449
|
+
MIK_CFUNC_DEF("setName", 1, mik__ble_set_name),
|
|
1450
|
+
MIK_CFUNC_DEF("getAddress", 0, mik__ble_get_address),
|
|
1451
|
+
MIK_CFUNC_DEF("getTxPower", 0, mik__ble_get_tx_power),
|
|
1452
|
+
MIK_CFUNC_DEF("setTxPower", 1, mik__ble_set_tx_power),
|
|
1453
|
+
MIK_CFUNC_DEF("advertise", 1, mik__ble_advertise),
|
|
1454
|
+
MIK_CFUNC_DEF("stopAdvertising", 0, mik__ble_stop_advertising),
|
|
1455
|
+
MIK_CFUNC_DEF("stop", 0, mik__ble_stop),
|
|
1456
|
+
MIK_CFUNC_DEF("setValue", 3, mik__ble_set_value),
|
|
1457
|
+
MIK_CFUNC_DEF("notify", 3, mik__ble_notify),
|
|
1458
|
+
MIK_CFUNC_DEF("on", 2, mik__ble_on),
|
|
1459
|
+
MIK_CFUNC_DEF("off", 2, mik__ble_off),
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
/* ── Module init ───────────────────────────────────────────────────── */
|
|
1463
|
+
|
|
1464
|
+
static int mik__ble_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
1465
|
+
JSRuntime* rt = JS_GetRuntime(ctx);
|
|
1466
|
+
|
|
1467
|
+
JS_NewClassID(rt, &mik_ble_class_id);
|
|
1468
|
+
JS_NewClass(rt, mik_ble_class_id, &mik_ble_classdef);
|
|
1469
|
+
|
|
1470
|
+
JSValue proto = JS_NewObject(ctx);
|
|
1471
|
+
JS_SetPropertyFunctionList(ctx, proto, mik__ble_proto_funcs, countof(mik__ble_proto_funcs));
|
|
1472
|
+
JS_SetClassProto(ctx, mik_ble_class_id, proto);
|
|
1473
|
+
|
|
1474
|
+
JSValue ctor =
|
|
1475
|
+
JS_NewCFunction2(ctx, mik__ble_constructor, "Ble", 0, JS_CFUNC_constructor, 0);
|
|
1476
|
+
JS_SetModuleExport(ctx, m, "Ble", ctor);
|
|
1477
|
+
return 0;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
static JSModuleDef* mik__ble_init(JSContext* ctx) {
|
|
1481
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
1482
|
+
CHECK_NOT_NULL(mik_rt);
|
|
1483
|
+
mik__ble_slot = MIK_AllocModuleSlot(mik_rt);
|
|
1484
|
+
|
|
1485
|
+
auto* state = new MIKBleState();
|
|
1486
|
+
mik__ble_st(mik_rt) = state;
|
|
1487
|
+
|
|
1488
|
+
/* Create the event queue lazily on first module import. Shared between
|
|
1489
|
+
* the NimBLE host task (producers) and the JS loop thread (consumer). */
|
|
1490
|
+
if (!s_ble_event_queue) {
|
|
1491
|
+
s_ble_event_queue = xQueueCreate(MIK_BLE_EVENT_QUEUE_DEPTH, sizeof(MIKBleEvent));
|
|
1492
|
+
CHECK_NOT_NULL(s_ble_event_queue);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
JSModuleDef* m = JS_NewCModule(ctx, "native:ble", mik__ble_module_init);
|
|
1496
|
+
if (!m) return nullptr;
|
|
1497
|
+
JS_AddModuleExport(ctx, m, "Ble");
|
|
1498
|
+
return m;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/* ── Event loop + destroy ──────────────────────────────────────────── */
|
|
1502
|
+
|
|
1503
|
+
/* Build a {id, address, mtu} connection info object for connect and
|
|
1504
|
+
* disconnect events. peer_addr is in NimBLE internal little-endian order;
|
|
1505
|
+
* reverse for display. */
|
|
1506
|
+
static JSValue mik__ble_build_conn_info(JSContext* ctx, uint16_t conn_handle,
|
|
1507
|
+
const uint8_t* peer_addr, uint16_t mtu) {
|
|
1508
|
+
JSValue info = JS_NewObject(ctx);
|
|
1509
|
+
JS_SetPropertyStr(ctx, info, "id", JS_NewInt32(ctx, conn_handle));
|
|
1510
|
+
char addr_str[18];
|
|
1511
|
+
snprintf(addr_str, sizeof(addr_str), "%02x:%02x:%02x:%02x:%02x:%02x", peer_addr[5],
|
|
1512
|
+
peer_addr[4], peer_addr[3], peer_addr[2], peer_addr[1], peer_addr[0]);
|
|
1513
|
+
JS_SetPropertyStr(ctx, info, "address", JS_NewString(ctx, addr_str));
|
|
1514
|
+
JS_SetPropertyStr(ctx, info, "mtu", JS_NewInt32(ctx, mtu));
|
|
1515
|
+
return info;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
static JSValue mik__ble_build_mtu_info(JSContext* ctx, uint16_t conn_handle, uint16_t mtu) {
|
|
1519
|
+
JSValue info = JS_NewObject(ctx);
|
|
1520
|
+
JS_SetPropertyStr(ctx, info, "id", JS_NewInt32(ctx, conn_handle));
|
|
1521
|
+
JS_SetPropertyStr(ctx, info, "mtu", JS_NewInt32(ctx, mtu));
|
|
1522
|
+
return info;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
void mik__ble_consume(JSContext* ctx) {
|
|
1526
|
+
if (!s_ble_event_queue) return;
|
|
1527
|
+
|
|
1528
|
+
MIKBleEvent evt;
|
|
1529
|
+
while (xQueueReceive(s_ble_event_queue, &evt, 0) == pdTRUE) {
|
|
1530
|
+
switch (evt.type) {
|
|
1531
|
+
case MIK_BLE_EVT_CONNECT: {
|
|
1532
|
+
JSValue info =
|
|
1533
|
+
mik__ble_build_conn_info(ctx, evt.conn_handle, evt.peer_addr, evt.mtu);
|
|
1534
|
+
for (auto& listener : s_on_connect) {
|
|
1535
|
+
mik_call_handler(ctx, listener, 1, &info);
|
|
1536
|
+
}
|
|
1537
|
+
JS_FreeValue(ctx, info);
|
|
1538
|
+
break;
|
|
1539
|
+
}
|
|
1540
|
+
case MIK_BLE_EVT_DISCONNECT: {
|
|
1541
|
+
JSValue info =
|
|
1542
|
+
mik__ble_build_conn_info(ctx, evt.conn_handle, evt.peer_addr, evt.mtu);
|
|
1543
|
+
for (auto& listener : s_on_disconnect) {
|
|
1544
|
+
mik_call_handler(ctx, listener, 1, &info);
|
|
1545
|
+
}
|
|
1546
|
+
JS_FreeValue(ctx, info);
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1549
|
+
case MIK_BLE_EVT_MTU: {
|
|
1550
|
+
JSValue info = mik__ble_build_mtu_info(ctx, evt.conn_handle, evt.mtu);
|
|
1551
|
+
for (auto& listener : s_on_mtu) {
|
|
1552
|
+
mik_call_handler(ctx, listener, 1, &info);
|
|
1553
|
+
}
|
|
1554
|
+
JS_FreeValue(ctx, info);
|
|
1555
|
+
break;
|
|
1556
|
+
}
|
|
1557
|
+
case MIK_BLE_EVT_WRITE: {
|
|
1558
|
+
MIKBleChar* chr = mik__ble_find_char_by_handle(evt.attr_handle);
|
|
1559
|
+
if (chr && !JS_IsUndefined(chr->on_write)) {
|
|
1560
|
+
JSValue buf =
|
|
1561
|
+
JS_NewUint8ArrayCopy(ctx, evt.write_data, evt.write_data_len);
|
|
1562
|
+
mik_call_handler(ctx, chr->on_write, 1, &buf);
|
|
1563
|
+
JS_FreeValue(ctx, buf);
|
|
1564
|
+
}
|
|
1565
|
+
mik__ble_pool_free(evt.write_data);
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
void mik__ble_destroy(JSContext* ctx) {
|
|
1573
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
1574
|
+
CHECK_NOT_NULL(mik_rt);
|
|
1575
|
+
if (!mik__ble_st(mik_rt)) return;
|
|
1576
|
+
|
|
1577
|
+
mik__ble_teardown(ctx);
|
|
1578
|
+
|
|
1579
|
+
if (s_ble_event_queue) {
|
|
1580
|
+
vQueueDelete(s_ble_event_queue);
|
|
1581
|
+
s_ble_event_queue = nullptr;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
delete mik__ble_st(mik_rt);
|
|
1585
|
+
mik__ble_st(mik_rt) = nullptr;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
MIK_REGISTER_MODULE(ble, "native:ble", mik__ble_init, mik__ble_consume, mik__ble_destroy)
|