@mikrojs/firmware 0.11.0 → 0.12.0-next.9.g2e06437
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/components/mikrojs/CMakeLists.txt +4 -3
- package/components/mikrojs/include/mik_http_server_internal.h +90 -0
- package/components/mikrojs/mik_http_server.cpp +790 -0
- package/components/mikrojs/mik_main.cpp +10 -4
- package/components/mikrojs/platform_esp32.cpp +6 -1
- package/components/mikrojs/test/CMakeLists.txt +1 -0
- package/components/mikrojs/test/http_server_test.cpp +216 -0
- package/package.json +3 -3
- package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32/mikrojs.bin +0 -0
- package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c3/mikrojs.bin +0 -0
- package/prebuilds/esp32c5/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c5/mikrojs.bin +0 -0
- package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c6/mikrojs.bin +0 -0
- package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32s3/mikrojs.bin +0 -0
|
@@ -43,7 +43,7 @@ endif()
|
|
|
43
43
|
|
|
44
44
|
idf_component_register(
|
|
45
45
|
INCLUDE_DIRS "include" "${MIK_INCLUDE_DIR}"
|
|
46
|
-
REQUIRES bt littlefs esp_adc esp_app_format esp_driver_gpio esp_driver_i2c esp_driver_ledc esp_driver_rmt esp_driver_spi esp_psram esp_timer esp_http_client esp-tls esp_netif esp_event esp_wifi nvs_flash esp_driver_uart esp_driver_usb_serial_jtag spi_flash
|
|
46
|
+
REQUIRES bt littlefs esp_adc esp_app_format esp_driver_gpio esp_driver_i2c esp_driver_ledc esp_driver_rmt esp_driver_spi esp_psram esp_timer esp_http_client esp_http_server esp-tls esp_netif esp_event esp_wifi nvs_flash esp_driver_uart esp_driver_usb_serial_jtag spi_flash
|
|
47
47
|
SRCS
|
|
48
48
|
# QuickJS engine
|
|
49
49
|
${QUICKJS_SOURCES}
|
|
@@ -82,6 +82,7 @@ idf_component_register(
|
|
|
82
82
|
"mik_config.cpp"
|
|
83
83
|
"mik_deploy.cpp"
|
|
84
84
|
"mik_http.cpp"
|
|
85
|
+
"mik_http_server.cpp"
|
|
85
86
|
"mik_i2c.cpp"
|
|
86
87
|
"mik_logfile.cpp"
|
|
87
88
|
"mik_neopixel.cpp"
|
|
@@ -132,8 +133,8 @@ endif()
|
|
|
132
133
|
include("${MIK_BYTECODE_CMAKE}")
|
|
133
134
|
|
|
134
135
|
# Force linker to include self-registering native modules
|
|
135
|
-
set(_MIK_MODULES cbor pin i2c spi http wifi rtc nvs_kv sntp sleep neopixel pwm uart)
|
|
136
|
-
set(_MIK_BYTECODE_MODULES cbor env result schema fs http/helpers http/request i2c kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi)
|
|
136
|
+
set(_MIK_MODULES cbor pin i2c spi http http_server wifi rtc nvs_kv sntp sleep neopixel pwm uart)
|
|
137
|
+
set(_MIK_BYTECODE_MODULES cbor env result schema fs http/helpers http/request http/server i2c kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi)
|
|
137
138
|
if(CONFIG_BT_ENABLED)
|
|
138
139
|
list(APPEND _MIK_MODULES ble)
|
|
139
140
|
list(APPEND _MIK_BYTECODE_MODULES ble)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/* Internal HTTP-server module layout shared with on-device tests.
|
|
2
|
+
* Included by mik_http_server.cpp and test/http_server_test.cpp so struct
|
|
3
|
+
* changes can't drift silently between the implementation and its mirror. */
|
|
4
|
+
#pragma once
|
|
5
|
+
|
|
6
|
+
#include <cstdint>
|
|
7
|
+
#include <cstdlib>
|
|
8
|
+
|
|
9
|
+
#include "esp_http_server.h"
|
|
10
|
+
#include "freertos/FreeRTOS.h"
|
|
11
|
+
#include "freertos/queue.h"
|
|
12
|
+
#include "freertos/semphr.h"
|
|
13
|
+
#include "utils.h"
|
|
14
|
+
|
|
15
|
+
struct MIKHsHeader {
|
|
16
|
+
char* key;
|
|
17
|
+
char* value;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
enum MIKHsCmd : uint8_t {
|
|
21
|
+
MIK_HS_RESPOND, // one-shot: status + headers + full body
|
|
22
|
+
MIK_HS_START, // begin chunked: status + headers
|
|
23
|
+
MIK_HS_CHUNK, // one body chunk
|
|
24
|
+
MIK_HS_END, // finish chunked
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/* Per-request command channel, shared between the httpd task and the JS task.
|
|
28
|
+
* Allocated by the httpd task; freed by it once the response is finished. */
|
|
29
|
+
struct MIKHsExchange {
|
|
30
|
+
SemaphoreHandle_t cmd_ready; // given by a respond* call, taken by the httpd task
|
|
31
|
+
httpd_req_t* req; // live request; valid while the httpd task is parked
|
|
32
|
+
MIKHsCmd cmd;
|
|
33
|
+
int status;
|
|
34
|
+
char status_line[40]; // must outlive the (possibly deferred) header flush
|
|
35
|
+
MIKHsHeader* headers;
|
|
36
|
+
size_t header_count;
|
|
37
|
+
uint8_t* body; // RESPOND: owned full body
|
|
38
|
+
size_t body_len;
|
|
39
|
+
uint8_t* chunk; // CHUNK: owned chunk buffer
|
|
40
|
+
size_t chunk_len;
|
|
41
|
+
bool aborted; // server stopping: handler closes the response and returns
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/* Acknowledgement posted by the httpd task after processing START/CHUNK, so the
|
|
45
|
+
* JS side can resolve the matching respondStart/respondChunk promise. */
|
|
46
|
+
struct MIKHsAck {
|
|
47
|
+
MIKHsExchange* ex;
|
|
48
|
+
bool ok; // false if the chunk send failed (client gone)
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/* A live request tracked on the JS side. `method`/`uri`/`body` are freed once
|
|
52
|
+
* the request is delivered to JS; `exchange` lives until the response finishes.
|
|
53
|
+
* `chunk_promise` holds the in-flight respondStart/respondChunk promise. */
|
|
54
|
+
struct MIKHsReq {
|
|
55
|
+
uint32_t id;
|
|
56
|
+
MIKHsExchange* exchange;
|
|
57
|
+
char* method;
|
|
58
|
+
char* uri;
|
|
59
|
+
uint8_t* body;
|
|
60
|
+
size_t body_len;
|
|
61
|
+
size_t content_length;
|
|
62
|
+
bool body_too_large;
|
|
63
|
+
bool delivered;
|
|
64
|
+
MIKPromise chunk_promise;
|
|
65
|
+
bool chunk_promise_active;
|
|
66
|
+
MIKHsReq* next;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/* Message posted from the httpd task to the JS task on request arrival. */
|
|
70
|
+
struct MIKHsMsg {
|
|
71
|
+
MIKHsExchange* exchange;
|
|
72
|
+
char* method;
|
|
73
|
+
char* uri;
|
|
74
|
+
uint8_t* body;
|
|
75
|
+
size_t body_len;
|
|
76
|
+
size_t content_length;
|
|
77
|
+
bool body_too_large;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
struct MIKHttpServerState {
|
|
81
|
+
httpd_handle_t server;
|
|
82
|
+
QueueHandle_t request_queue; // httpd -> JS: request arrivals
|
|
83
|
+
QueueHandle_t ack_queue; // httpd -> JS: START/CHUNK acks (backpressure)
|
|
84
|
+
size_t max_body_size;
|
|
85
|
+
volatile bool stopping;
|
|
86
|
+
uint32_t next_id;
|
|
87
|
+
MIKHsReq* reqs; // linked list of live requests
|
|
88
|
+
MIKPromise next_promise;
|
|
89
|
+
bool next_promise_active;
|
|
90
|
+
};
|
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
#include <cstring>
|
|
2
|
+
|
|
3
|
+
#include "esp_http_server.h"
|
|
4
|
+
#include "esp_log.h"
|
|
5
|
+
#include "freertos/FreeRTOS.h"
|
|
6
|
+
#include "freertos/queue.h"
|
|
7
|
+
#include "freertos/semphr.h"
|
|
8
|
+
#include "mik_http_server_internal.h"
|
|
9
|
+
#include "private.h"
|
|
10
|
+
#include "utils.h"
|
|
11
|
+
|
|
12
|
+
/* native:http_server — an HTTP server backed by ESP-IDF's esp_http_server.
|
|
13
|
+
*
|
|
14
|
+
* esp_http_server runs request handlers on its own (single) task; QuickJS is
|
|
15
|
+
* single-threaded, so the JS handler must run on the event-loop task. This is
|
|
16
|
+
* mik_http.cpp reversed: the httpd task parses the request, posts it to a
|
|
17
|
+
* queue, and BLOCKS on a per-request command channel until the JS side drives
|
|
18
|
+
* the response. Because the httpd task is single-threaded and blocks per
|
|
19
|
+
* request, at most one request is in flight at a time — strictly serial.
|
|
20
|
+
*
|
|
21
|
+
* Response protocol (JS -> httpd, via the exchange's cmd_ready semaphore):
|
|
22
|
+
* - RESPOND: one-shot. status + headers + full body, sent with Content-Length.
|
|
23
|
+
* - START / CHUNK / END: a chunked (streamed) response. START and CHUNK are
|
|
24
|
+
* acked back through ack_queue so the JS side gets backpressure (it awaits
|
|
25
|
+
* each ack before sending the next command); END needs no ack.
|
|
26
|
+
* The cmd_ready semaphore is binary, so the JS side must wait for the previous
|
|
27
|
+
* command's ack before posting the next — which the await-per-command flow on
|
|
28
|
+
* the JS side guarantees.
|
|
29
|
+
*
|
|
30
|
+
* Header limitation: esp_http_server cannot enumerate a request's headers (the
|
|
31
|
+
* only API fetches a header by name), so the JS side exposes lazy by-name
|
|
32
|
+
* lookups via getHeader(), reading the parked request directly. The httpd task
|
|
33
|
+
* is blocked waiting for the response, so there is no concurrent writer. */
|
|
34
|
+
|
|
35
|
+
#define MIK_HS_TAG "native:http_server"
|
|
36
|
+
#define MIK_HS_DEFAULT_MAX_BODY 16384
|
|
37
|
+
/* How often the parked handler wakes to check for server shutdown. */
|
|
38
|
+
#define MIK_HS_ABORT_POLL_MS 250
|
|
39
|
+
|
|
40
|
+
/* Dynamic module data slot, allocated on first import. The module's data
|
|
41
|
+
* structures live in mik_http_server_internal.h so on-device tests can drive
|
|
42
|
+
* the queues and inspect the exchange directly. */
|
|
43
|
+
int mik__http_server_slot = -1;
|
|
44
|
+
|
|
45
|
+
static inline MIKHttpServerState*& mik__hs_st(MIKRuntime* rt) {
|
|
46
|
+
return reinterpret_cast<MIKHttpServerState*&>(rt->module_data[mik__http_server_slot]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ── Small helpers ─────────────────────────────────────────────────── */
|
|
50
|
+
|
|
51
|
+
static const char* mik__hs_reason(int status) {
|
|
52
|
+
switch (status) {
|
|
53
|
+
case 200: return "OK";
|
|
54
|
+
case 201: return "Created";
|
|
55
|
+
case 202: return "Accepted";
|
|
56
|
+
case 204: return "No Content";
|
|
57
|
+
case 301: return "Moved Permanently";
|
|
58
|
+
case 302: return "Found";
|
|
59
|
+
case 303: return "See Other";
|
|
60
|
+
case 304: return "Not Modified";
|
|
61
|
+
case 307: return "Temporary Redirect";
|
|
62
|
+
case 308: return "Permanent Redirect";
|
|
63
|
+
case 400: return "Bad Request";
|
|
64
|
+
case 401: return "Unauthorized";
|
|
65
|
+
case 403: return "Forbidden";
|
|
66
|
+
case 404: return "Not Found";
|
|
67
|
+
case 405: return "Method Not Allowed";
|
|
68
|
+
case 409: return "Conflict";
|
|
69
|
+
case 413: return "Payload Too Large";
|
|
70
|
+
case 415: return "Unsupported Media Type";
|
|
71
|
+
case 422: return "Unprocessable Entity";
|
|
72
|
+
case 429: return "Too Many Requests";
|
|
73
|
+
case 500: return "Internal Server Error";
|
|
74
|
+
case 501: return "Not Implemented";
|
|
75
|
+
case 503: return "Service Unavailable";
|
|
76
|
+
default: return "Status";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static const char* mik__hs_method_str(int m) {
|
|
81
|
+
switch (m) {
|
|
82
|
+
case HTTP_GET: return "GET";
|
|
83
|
+
case HTTP_POST: return "POST";
|
|
84
|
+
case HTTP_PUT: return "PUT";
|
|
85
|
+
case HTTP_PATCH: return "PATCH";
|
|
86
|
+
case HTTP_DELETE: return "DELETE";
|
|
87
|
+
case HTTP_HEAD: return "HEAD";
|
|
88
|
+
case HTTP_OPTIONS: return "OPTIONS";
|
|
89
|
+
default: return "GET";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static void mik__hs_free_headers(MIKHsHeader* h, size_t n) {
|
|
94
|
+
for (size_t i = 0; i < n; i++) {
|
|
95
|
+
free(h[i].key);
|
|
96
|
+
free(h[i].value);
|
|
97
|
+
}
|
|
98
|
+
free(h);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static void mik__hs_free_exchange(MIKHsExchange* ex) {
|
|
102
|
+
if (!ex) return;
|
|
103
|
+
if (ex->cmd_ready) vSemaphoreDelete(ex->cmd_ready);
|
|
104
|
+
mik__hs_free_headers(ex->headers, ex->header_count);
|
|
105
|
+
free(ex->body);
|
|
106
|
+
free(ex->chunk);
|
|
107
|
+
delete ex;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static void mik__hs_post_ack(QueueHandle_t q, MIKHsExchange* ex, bool ok) {
|
|
111
|
+
MIKHsAck a = {ex, ok};
|
|
112
|
+
xQueueSend(q, &a, portMAX_DELAY);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static void mik__hs_set_head(httpd_req_t* req, MIKHsExchange* ex) {
|
|
116
|
+
snprintf(ex->status_line, sizeof(ex->status_line), "%d %s", ex->status,
|
|
117
|
+
mik__hs_reason(ex->status));
|
|
118
|
+
httpd_resp_set_status(req, ex->status_line);
|
|
119
|
+
for (size_t i = 0; i < ex->header_count; i++) {
|
|
120
|
+
httpd_resp_set_hdr(req, ex->headers[i].key, ex->headers[i].value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ── httpd task: request handler ───────────────────────────────────── */
|
|
125
|
+
|
|
126
|
+
static esp_err_t mik__hs_handler(httpd_req_t* req) {
|
|
127
|
+
auto* state = static_cast<MIKHttpServerState*>(req->user_ctx);
|
|
128
|
+
|
|
129
|
+
/* Read the request body up to the cap, before handing the request to JS. */
|
|
130
|
+
uint8_t* body = nullptr;
|
|
131
|
+
size_t body_len = 0;
|
|
132
|
+
bool too_large = false;
|
|
133
|
+
size_t clen = req->content_len;
|
|
134
|
+
if (clen > 0) {
|
|
135
|
+
if (clen > state->max_body_size) {
|
|
136
|
+
too_large = true;
|
|
137
|
+
} else {
|
|
138
|
+
body = static_cast<uint8_t*>(malloc(clen));
|
|
139
|
+
if (!body) {
|
|
140
|
+
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "out of memory");
|
|
141
|
+
return ESP_OK;
|
|
142
|
+
}
|
|
143
|
+
size_t off = 0;
|
|
144
|
+
while (off < clen) {
|
|
145
|
+
int r = httpd_req_recv(req, reinterpret_cast<char*>(body) + off, clen - off);
|
|
146
|
+
if (r <= 0) {
|
|
147
|
+
free(body);
|
|
148
|
+
if (r == HTTPD_SOCK_ERR_TIMEOUT) {
|
|
149
|
+
httpd_resp_send_err(req, HTTPD_408_REQ_TIMEOUT, nullptr);
|
|
150
|
+
}
|
|
151
|
+
return ESP_OK;
|
|
152
|
+
}
|
|
153
|
+
off += static_cast<size_t>(r);
|
|
154
|
+
}
|
|
155
|
+
body_len = off;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
auto* ex = new MIKHsExchange();
|
|
160
|
+
ex->cmd_ready = xSemaphoreCreateBinary();
|
|
161
|
+
if (!ex->cmd_ready) {
|
|
162
|
+
delete ex;
|
|
163
|
+
free(body);
|
|
164
|
+
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "out of memory");
|
|
165
|
+
return ESP_OK;
|
|
166
|
+
}
|
|
167
|
+
ex->req = req;
|
|
168
|
+
|
|
169
|
+
MIKHsMsg msg = {};
|
|
170
|
+
msg.exchange = ex;
|
|
171
|
+
msg.method = strdup(mik__hs_method_str(req->method));
|
|
172
|
+
msg.uri = strdup(req->uri);
|
|
173
|
+
msg.body = body;
|
|
174
|
+
msg.body_len = body_len;
|
|
175
|
+
msg.content_length = clen;
|
|
176
|
+
msg.body_too_large = too_large;
|
|
177
|
+
|
|
178
|
+
if (xQueueSend(state->request_queue, &msg, portMAX_DELAY) != pdTRUE) {
|
|
179
|
+
free(msg.method);
|
|
180
|
+
free(msg.uri);
|
|
181
|
+
free(body);
|
|
182
|
+
mik__hs_free_exchange(ex);
|
|
183
|
+
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, nullptr);
|
|
184
|
+
return ESP_OK;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Drive the response from JS commands. httpd_stop() can't complete until
|
|
188
|
+
* this handler returns, so stop() sets `stopping` before calling it. */
|
|
189
|
+
bool response_started = false;
|
|
190
|
+
bool aborted = false;
|
|
191
|
+
bool done = false;
|
|
192
|
+
while (!done) {
|
|
193
|
+
bool got = false;
|
|
194
|
+
while (!got) {
|
|
195
|
+
if (xSemaphoreTake(ex->cmd_ready, pdMS_TO_TICKS(MIK_HS_ABORT_POLL_MS)) == pdTRUE) {
|
|
196
|
+
got = true;
|
|
197
|
+
} else if (state->stopping) {
|
|
198
|
+
aborted = true;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (aborted) break;
|
|
203
|
+
|
|
204
|
+
switch (ex->cmd) {
|
|
205
|
+
case MIK_HS_RESPOND:
|
|
206
|
+
mik__hs_set_head(req, ex);
|
|
207
|
+
httpd_resp_send(req, reinterpret_cast<const char*>(ex->body),
|
|
208
|
+
ex->body ? ex->body_len : 0);
|
|
209
|
+
response_started = true;
|
|
210
|
+
done = true; // JS already removed the req
|
|
211
|
+
break;
|
|
212
|
+
case MIK_HS_START:
|
|
213
|
+
mik__hs_set_head(req, ex); // flushed on the first send_chunk
|
|
214
|
+
response_started = true;
|
|
215
|
+
mik__hs_post_ack(state->ack_queue, ex, true);
|
|
216
|
+
break;
|
|
217
|
+
case MIK_HS_CHUNK: {
|
|
218
|
+
esp_err_t e = httpd_resp_send_chunk(req, reinterpret_cast<const char*>(ex->chunk),
|
|
219
|
+
ex->chunk_len);
|
|
220
|
+
free(ex->chunk);
|
|
221
|
+
ex->chunk = nullptr;
|
|
222
|
+
ex->chunk_len = 0;
|
|
223
|
+
mik__hs_post_ack(state->ack_queue, ex, e == ESP_OK);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case MIK_HS_END:
|
|
227
|
+
httpd_resp_send_chunk(req, nullptr, 0); // terminate the chunked response
|
|
228
|
+
done = true; // JS already removed the req
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (aborted) {
|
|
234
|
+
if (response_started) {
|
|
235
|
+
httpd_resp_send_chunk(req, nullptr, 0); // close a stream already in progress
|
|
236
|
+
} else {
|
|
237
|
+
httpd_resp_set_status(req, "503 Service Unavailable");
|
|
238
|
+
httpd_resp_send(req, nullptr, 0);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
mik__hs_free_exchange(ex);
|
|
243
|
+
return ESP_OK;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* ── JS bindings ───────────────────────────────────────────────────── */
|
|
247
|
+
|
|
248
|
+
/* The request list is a plain singly-linked list with O(n) find/append. Because
|
|
249
|
+
* the httpd task is single and blocks per request, at most one request is in
|
|
250
|
+
* flight (plus, briefly, ones the serve loop has yet to drain), so n stays tiny. */
|
|
251
|
+
static MIKHsReq* mik__hs_find_req(MIKHttpServerState* state, uint32_t id) {
|
|
252
|
+
for (MIKHsReq* r = state->reqs; r; r = r->next) {
|
|
253
|
+
if (r->id == id) return r;
|
|
254
|
+
}
|
|
255
|
+
return nullptr;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
static MIKHsReq* mik__hs_find_undelivered(MIKHttpServerState* state) {
|
|
259
|
+
for (MIKHsReq* r = state->reqs; r; r = r->next) {
|
|
260
|
+
if (!r->delivered) return r;
|
|
261
|
+
}
|
|
262
|
+
return nullptr;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
static void mik__hs_remove_req(MIKHttpServerState* state, MIKHsReq* target) {
|
|
266
|
+
MIKHsReq** pp = &state->reqs;
|
|
267
|
+
while (*pp) {
|
|
268
|
+
if (*pp == target) {
|
|
269
|
+
*pp = target->next;
|
|
270
|
+
delete target;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
pp = &(*pp)->next;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
static JSValue mik__hs_build_descriptor(JSContext* ctx, MIKHsReq* r) {
|
|
278
|
+
JSValue obj = JS_NewObject(ctx);
|
|
279
|
+
JS_SetPropertyStr(ctx, obj, "id", JS_NewUint32(ctx, r->id));
|
|
280
|
+
JS_SetPropertyStr(ctx, obj, "method", JS_NewString(ctx, r->method ? r->method : "GET"));
|
|
281
|
+
JS_SetPropertyStr(ctx, obj, "url", JS_NewString(ctx, r->uri ? r->uri : "/"));
|
|
282
|
+
JS_SetPropertyStr(ctx, obj, "body",
|
|
283
|
+
r->body ? JS_NewUint8ArrayCopy(ctx, r->body, r->body_len)
|
|
284
|
+
: JS_NewUint8ArrayCopy(ctx, nullptr, 0));
|
|
285
|
+
JS_SetPropertyStr(ctx, obj, "bodyTooLarge", JS_NewBool(ctx, r->body_too_large));
|
|
286
|
+
JS_SetPropertyStr(ctx, obj, "contentLength",
|
|
287
|
+
JS_NewInt64(ctx, static_cast<int64_t>(r->content_length)));
|
|
288
|
+
return obj;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Mark a request as delivered to JS and release its request-side buffers (the
|
|
292
|
+
* descriptor already copied them). `exchange` stays until the response finishes. */
|
|
293
|
+
static void mik__hs_mark_delivered(MIKHsReq* r) {
|
|
294
|
+
r->delivered = true;
|
|
295
|
+
free(r->method);
|
|
296
|
+
r->method = nullptr;
|
|
297
|
+
free(r->uri);
|
|
298
|
+
r->uri = nullptr;
|
|
299
|
+
free(r->body);
|
|
300
|
+
r->body = nullptr;
|
|
301
|
+
r->body_len = 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
static JSValue mik__hs_start(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
305
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
306
|
+
CHECK_NOT_NULL(rt);
|
|
307
|
+
MIKHttpServerState* state = mik__hs_st(rt);
|
|
308
|
+
CHECK_NOT_NULL(state);
|
|
309
|
+
|
|
310
|
+
if (state->server) return mik__result_err_tag(ctx, "AlreadyListening");
|
|
311
|
+
|
|
312
|
+
uint32_t port = 80;
|
|
313
|
+
if (argc > 0) JS_ToUint32(ctx, &port, argv[0]);
|
|
314
|
+
uint32_t max_body = MIK_HS_DEFAULT_MAX_BODY;
|
|
315
|
+
if (argc > 1 && JS_IsNumber(argv[1])) JS_ToUint32(ctx, &max_body, argv[1]);
|
|
316
|
+
state->max_body_size = max_body;
|
|
317
|
+
state->stopping = false;
|
|
318
|
+
|
|
319
|
+
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
|
320
|
+
config.server_port = static_cast<uint16_t>(port);
|
|
321
|
+
config.uri_match_fn = httpd_uri_match_wildcard;
|
|
322
|
+
|
|
323
|
+
httpd_handle_t server = nullptr;
|
|
324
|
+
esp_err_t err = httpd_start(&server, &config);
|
|
325
|
+
if (err != ESP_OK) {
|
|
326
|
+
const char* tag = (err == ESP_ERR_HTTPD_ALLOC_MEM) ? "OutOfMemory" : "StartFailed";
|
|
327
|
+
return mik__result_err_named(ctx, tag, "failed to start HTTP server: %s",
|
|
328
|
+
esp_err_to_name(err));
|
|
329
|
+
}
|
|
330
|
+
state->server = server;
|
|
331
|
+
|
|
332
|
+
/* Register one wildcard handler per method; all routing is done in JS. */
|
|
333
|
+
static const int methods[] = {HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH,
|
|
334
|
+
HTTP_DELETE, HTTP_HEAD, HTTP_OPTIONS};
|
|
335
|
+
for (int m : methods) {
|
|
336
|
+
httpd_uri_t uri = {};
|
|
337
|
+
uri.uri = "/*";
|
|
338
|
+
uri.method = static_cast<httpd_method_t>(m);
|
|
339
|
+
uri.handler = mik__hs_handler;
|
|
340
|
+
uri.user_ctx = state;
|
|
341
|
+
httpd_register_uri_handler(server, &uri);
|
|
342
|
+
}
|
|
343
|
+
return mik__result_ok_void(ctx);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
static JSValue mik__hs_stop(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
347
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
348
|
+
CHECK_NOT_NULL(rt);
|
|
349
|
+
MIKHttpServerState* state = mik__hs_st(rt);
|
|
350
|
+
if (!state || !state->server) return JS_UNDEFINED;
|
|
351
|
+
|
|
352
|
+
/* Set `stopping` first so any parked handler aborts; httpd_stop() then
|
|
353
|
+
* blocks until the (single) httpd task returns from the handler. */
|
|
354
|
+
state->stopping = true;
|
|
355
|
+
httpd_stop(state->server);
|
|
356
|
+
state->server = nullptr;
|
|
357
|
+
|
|
358
|
+
/* Handlers have all returned and freed their exchanges. Free only the
|
|
359
|
+
* request-side data; never touch exchange pointers (already freed). Resolve
|
|
360
|
+
* any in-flight chunk promise with false so a streaming loop ends. */
|
|
361
|
+
MIKHsMsg msg;
|
|
362
|
+
while (xQueueReceive(state->request_queue, &msg, 0) == pdTRUE) {
|
|
363
|
+
free(msg.method);
|
|
364
|
+
free(msg.uri);
|
|
365
|
+
free(msg.body);
|
|
366
|
+
}
|
|
367
|
+
MIKHsAck ack;
|
|
368
|
+
while (xQueueReceive(state->ack_queue, &ack, 0) == pdTRUE) { /* discard */ }
|
|
369
|
+
|
|
370
|
+
MIKHsReq* r = state->reqs;
|
|
371
|
+
while (r) {
|
|
372
|
+
MIKHsReq* next = r->next;
|
|
373
|
+
if (!r->delivered) {
|
|
374
|
+
free(r->method);
|
|
375
|
+
free(r->uri);
|
|
376
|
+
free(r->body);
|
|
377
|
+
}
|
|
378
|
+
if (r->chunk_promise_active) {
|
|
379
|
+
JSValue v = JS_FALSE;
|
|
380
|
+
MIK_ResolvePromise(ctx, &r->chunk_promise, 1, &v);
|
|
381
|
+
r->chunk_promise_active = false;
|
|
382
|
+
}
|
|
383
|
+
delete r;
|
|
384
|
+
r = next;
|
|
385
|
+
}
|
|
386
|
+
state->reqs = nullptr;
|
|
387
|
+
|
|
388
|
+
/* Wake a pending nextRequest() with a close sentinel so the serve loop ends. */
|
|
389
|
+
if (state->next_promise_active) {
|
|
390
|
+
JSValue closed = JS_NewObject(ctx);
|
|
391
|
+
JS_SetPropertyStr(ctx, closed, "closed", JS_TRUE);
|
|
392
|
+
MIK_ResolvePromise(ctx, &state->next_promise, 1, &closed);
|
|
393
|
+
state->next_promise_active = false;
|
|
394
|
+
}
|
|
395
|
+
return JS_UNDEFINED;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
static JSValue mik__hs_next_request(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
399
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
400
|
+
CHECK_NOT_NULL(rt);
|
|
401
|
+
MIKHttpServerState* state = mik__hs_st(rt);
|
|
402
|
+
if (!state || !state->server) {
|
|
403
|
+
JSValue closed = JS_NewObject(ctx);
|
|
404
|
+
JS_SetPropertyStr(ctx, closed, "closed", JS_TRUE);
|
|
405
|
+
return MIK_NewResolvedPromise(ctx, 1, &closed);
|
|
406
|
+
}
|
|
407
|
+
if (state->next_promise_active) {
|
|
408
|
+
return JS_ThrowTypeError(ctx, "nextRequest already pending");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
MIKHsReq* r = mik__hs_find_undelivered(state);
|
|
412
|
+
if (r) {
|
|
413
|
+
JSValue desc = mik__hs_build_descriptor(ctx, r);
|
|
414
|
+
mik__hs_mark_delivered(r);
|
|
415
|
+
return MIK_NewResolvedPromise(ctx, 1, &desc);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
JSValue promise = MIK_InitPromise(ctx, &state->next_promise);
|
|
419
|
+
state->next_promise_active = true;
|
|
420
|
+
return promise;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* Parse a [[key, value], ...] array into a heap MIKHsHeader array. On any
|
|
424
|
+
* allocation failure, frees what it built and yields an empty set — a response
|
|
425
|
+
* with fewer headers is preferable to failing the whole request. */
|
|
426
|
+
static void mik__hs_parse_headers(JSContext* ctx, JSValue arr, MIKHsHeader** out,
|
|
427
|
+
size_t* out_count) {
|
|
428
|
+
*out = nullptr;
|
|
429
|
+
*out_count = 0;
|
|
430
|
+
JSValue len_val = JS_GetPropertyStr(ctx, arr, "length");
|
|
431
|
+
int64_t len = 0;
|
|
432
|
+
JS_ToInt64(ctx, &len, len_val);
|
|
433
|
+
JS_FreeValue(ctx, len_val);
|
|
434
|
+
if (len <= 0) return;
|
|
435
|
+
|
|
436
|
+
auto* headers = static_cast<MIKHsHeader*>(calloc(len, sizeof(MIKHsHeader)));
|
|
437
|
+
if (!headers) return;
|
|
438
|
+
size_t count = 0;
|
|
439
|
+
for (int64_t i = 0; i < len; i++) {
|
|
440
|
+
JSValue pair = JS_GetPropertyUint32(ctx, arr, i);
|
|
441
|
+
if (JS_IsArray(pair)) {
|
|
442
|
+
JSValue k = JS_GetPropertyUint32(ctx, pair, 0);
|
|
443
|
+
JSValue v = JS_GetPropertyUint32(ctx, pair, 1);
|
|
444
|
+
const char* ks = JS_ToCString(ctx, k);
|
|
445
|
+
const char* vs = JS_ToCString(ctx, v);
|
|
446
|
+
if (ks && vs) {
|
|
447
|
+
char* kd = strdup(ks);
|
|
448
|
+
char* vd = strdup(vs);
|
|
449
|
+
if (kd && vd) {
|
|
450
|
+
headers[count].key = kd;
|
|
451
|
+
headers[count].value = vd;
|
|
452
|
+
count++;
|
|
453
|
+
} else {
|
|
454
|
+
free(kd);
|
|
455
|
+
free(vd);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (ks) JS_FreeCString(ctx, ks);
|
|
459
|
+
if (vs) JS_FreeCString(ctx, vs);
|
|
460
|
+
JS_FreeValue(ctx, k);
|
|
461
|
+
JS_FreeValue(ctx, v);
|
|
462
|
+
}
|
|
463
|
+
JS_FreeValue(ctx, pair);
|
|
464
|
+
}
|
|
465
|
+
*out = headers;
|
|
466
|
+
*out_count = count;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* respond(id, status, headers, body) — one-shot response. No-op if the request
|
|
470
|
+
* is unknown (already responded). */
|
|
471
|
+
static JSValue mik__hs_respond(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
472
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
473
|
+
CHECK_NOT_NULL(rt);
|
|
474
|
+
MIKHttpServerState* state = mik__hs_st(rt);
|
|
475
|
+
if (!state) return JS_UNDEFINED;
|
|
476
|
+
|
|
477
|
+
uint32_t id;
|
|
478
|
+
if (JS_ToUint32(ctx, &id, argv[0])) return JS_EXCEPTION;
|
|
479
|
+
MIKHsReq* r = mik__hs_find_req(state, id);
|
|
480
|
+
if (!r || !r->delivered || !r->exchange) return JS_UNDEFINED;
|
|
481
|
+
MIKHsExchange* ex = r->exchange;
|
|
482
|
+
|
|
483
|
+
ex->cmd = MIK_HS_RESPOND;
|
|
484
|
+
int32_t status = 200;
|
|
485
|
+
JS_ToInt32(ctx, &status, argv[1]);
|
|
486
|
+
ex->status = status;
|
|
487
|
+
if (argc > 2 && JS_IsArray(argv[2])) {
|
|
488
|
+
mik__hs_parse_headers(ctx, argv[2], &ex->headers, &ex->header_count);
|
|
489
|
+
}
|
|
490
|
+
if (argc > 3 && !JS_IsUndefined(argv[3]) && !JS_IsNull(argv[3])) {
|
|
491
|
+
size_t blen;
|
|
492
|
+
const uint8_t* bptr = JS_GetUint8Array(ctx, &blen, argv[3]);
|
|
493
|
+
if (bptr && blen > 0) {
|
|
494
|
+
ex->body = static_cast<uint8_t*>(malloc(blen));
|
|
495
|
+
if (ex->body) {
|
|
496
|
+
memcpy(ex->body, bptr, blen);
|
|
497
|
+
ex->body_len = blen;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* Hand ownership to the httpd task and drop our bookkeeping. Do not touch
|
|
503
|
+
* `ex` after giving cmd_ready — the httpd task may free it immediately. */
|
|
504
|
+
r->exchange = nullptr;
|
|
505
|
+
xSemaphoreGive(ex->cmd_ready);
|
|
506
|
+
mik__hs_remove_req(state, r);
|
|
507
|
+
return JS_UNDEFINED;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/* respondStart(id, status, headers) — begin a chunked response. Returns a
|
|
511
|
+
* promise that resolves once status + headers are queued. */
|
|
512
|
+
static JSValue mik__hs_respond_start(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
513
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
514
|
+
CHECK_NOT_NULL(rt);
|
|
515
|
+
MIKHttpServerState* state = mik__hs_st(rt);
|
|
516
|
+
JSValue undef = JS_UNDEFINED;
|
|
517
|
+
if (!state) return MIK_NewResolvedPromise(ctx, 1, &undef);
|
|
518
|
+
|
|
519
|
+
uint32_t id;
|
|
520
|
+
if (JS_ToUint32(ctx, &id, argv[0])) return JS_EXCEPTION;
|
|
521
|
+
MIKHsReq* r = mik__hs_find_req(state, id);
|
|
522
|
+
if (!r || !r->delivered || !r->exchange || r->chunk_promise_active) {
|
|
523
|
+
return MIK_NewResolvedPromise(ctx, 1, &undef);
|
|
524
|
+
}
|
|
525
|
+
MIKHsExchange* ex = r->exchange;
|
|
526
|
+
|
|
527
|
+
ex->cmd = MIK_HS_START;
|
|
528
|
+
int32_t status = 200;
|
|
529
|
+
JS_ToInt32(ctx, &status, argv[1]);
|
|
530
|
+
ex->status = status;
|
|
531
|
+
if (argc > 2 && JS_IsArray(argv[2])) {
|
|
532
|
+
mik__hs_parse_headers(ctx, argv[2], &ex->headers, &ex->header_count);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
JSValue promise = MIK_InitPromise(ctx, &r->chunk_promise);
|
|
536
|
+
r->chunk_promise_active = true;
|
|
537
|
+
xSemaphoreGive(ex->cmd_ready);
|
|
538
|
+
return promise;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/* respondChunk(id, data) — send one body chunk. Returns a promise resolving to
|
|
542
|
+
* false if the client has gone (caller should stop sending). */
|
|
543
|
+
static JSValue mik__hs_respond_chunk(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
544
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
545
|
+
CHECK_NOT_NULL(rt);
|
|
546
|
+
MIKHttpServerState* state = mik__hs_st(rt);
|
|
547
|
+
JSValue f = JS_FALSE;
|
|
548
|
+
if (!state) return MIK_NewResolvedPromise(ctx, 1, &f);
|
|
549
|
+
|
|
550
|
+
uint32_t id;
|
|
551
|
+
if (JS_ToUint32(ctx, &id, argv[0])) return JS_EXCEPTION;
|
|
552
|
+
MIKHsReq* r = mik__hs_find_req(state, id);
|
|
553
|
+
if (!r || !r->delivered || !r->exchange || r->chunk_promise_active) {
|
|
554
|
+
return MIK_NewResolvedPromise(ctx, 1, &f);
|
|
555
|
+
}
|
|
556
|
+
MIKHsExchange* ex = r->exchange;
|
|
557
|
+
|
|
558
|
+
ex->cmd = MIK_HS_CHUNK;
|
|
559
|
+
if (argc > 1 && !JS_IsUndefined(argv[1]) && !JS_IsNull(argv[1])) {
|
|
560
|
+
size_t len;
|
|
561
|
+
const uint8_t* p = JS_GetUint8Array(ctx, &len, argv[1]);
|
|
562
|
+
if (p && len > 0) {
|
|
563
|
+
ex->chunk = static_cast<uint8_t*>(malloc(len));
|
|
564
|
+
if (ex->chunk) {
|
|
565
|
+
memcpy(ex->chunk, p, len);
|
|
566
|
+
ex->chunk_len = len;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
JSValue promise = MIK_InitPromise(ctx, &r->chunk_promise);
|
|
572
|
+
r->chunk_promise_active = true;
|
|
573
|
+
xSemaphoreGive(ex->cmd_ready);
|
|
574
|
+
return promise;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* respondEnd(id) — finish a chunked response. */
|
|
578
|
+
static JSValue mik__hs_respond_end(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
579
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
580
|
+
CHECK_NOT_NULL(rt);
|
|
581
|
+
MIKHttpServerState* state = mik__hs_st(rt);
|
|
582
|
+
if (!state) return JS_UNDEFINED;
|
|
583
|
+
|
|
584
|
+
uint32_t id;
|
|
585
|
+
if (JS_ToUint32(ctx, &id, argv[0])) return JS_EXCEPTION;
|
|
586
|
+
MIKHsReq* r = mik__hs_find_req(state, id);
|
|
587
|
+
if (!r || !r->delivered || !r->exchange) return JS_UNDEFINED;
|
|
588
|
+
MIKHsExchange* ex = r->exchange;
|
|
589
|
+
|
|
590
|
+
if (r->chunk_promise_active) {
|
|
591
|
+
MIK_FreePromise(ctx, &r->chunk_promise);
|
|
592
|
+
r->chunk_promise_active = false;
|
|
593
|
+
}
|
|
594
|
+
ex->cmd = MIK_HS_END;
|
|
595
|
+
r->exchange = nullptr;
|
|
596
|
+
xSemaphoreGive(ex->cmd_ready);
|
|
597
|
+
mik__hs_remove_req(state, r);
|
|
598
|
+
return JS_UNDEFINED;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/* getHeader(id, name) — fetch a single request header by name, or undefined.
|
|
602
|
+
* Reads the parked request directly; the httpd task is blocked in the handler
|
|
603
|
+
* waiting for the response, so there is no concurrent writer. */
|
|
604
|
+
static JSValue mik__hs_get_header(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
605
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
606
|
+
CHECK_NOT_NULL(rt);
|
|
607
|
+
MIKHttpServerState* state = mik__hs_st(rt);
|
|
608
|
+
if (!state) return JS_UNDEFINED;
|
|
609
|
+
|
|
610
|
+
uint32_t id;
|
|
611
|
+
if (JS_ToUint32(ctx, &id, argv[0])) return JS_EXCEPTION;
|
|
612
|
+
const char* name = JS_ToCString(ctx, argv[1]);
|
|
613
|
+
if (!name) return JS_EXCEPTION;
|
|
614
|
+
|
|
615
|
+
JSValue result = JS_UNDEFINED;
|
|
616
|
+
MIKHsReq* r = mik__hs_find_req(state, id);
|
|
617
|
+
if (r && r->delivered && r->exchange && r->exchange->req) {
|
|
618
|
+
httpd_req_t* req = r->exchange->req;
|
|
619
|
+
size_t len = httpd_req_get_hdr_value_len(req, name);
|
|
620
|
+
if (len > 0) {
|
|
621
|
+
char* buf = static_cast<char*>(malloc(len + 1));
|
|
622
|
+
if (buf) {
|
|
623
|
+
if (httpd_req_get_hdr_value_str(req, name, buf, len + 1) == ESP_OK) {
|
|
624
|
+
result = JS_NewStringLen(ctx, buf, len);
|
|
625
|
+
}
|
|
626
|
+
free(buf);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
JS_FreeCString(ctx, name);
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* ── Module init ───────────────────────────────────────────────────── */
|
|
635
|
+
|
|
636
|
+
static void mik__hs_ensure_initialized(JSContext* ctx) {
|
|
637
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
638
|
+
CHECK_NOT_NULL(rt);
|
|
639
|
+
if (mik__hs_st(rt)) return;
|
|
640
|
+
|
|
641
|
+
auto* state = new MIKHttpServerState();
|
|
642
|
+
state->server = nullptr;
|
|
643
|
+
state->request_queue = xQueueCreate(8, sizeof(MIKHsMsg));
|
|
644
|
+
state->ack_queue = xQueueCreate(8, sizeof(MIKHsAck));
|
|
645
|
+
CHECK_NOT_NULL(state->request_queue);
|
|
646
|
+
CHECK_NOT_NULL(state->ack_queue);
|
|
647
|
+
state->max_body_size = MIK_HS_DEFAULT_MAX_BODY;
|
|
648
|
+
state->stopping = false;
|
|
649
|
+
state->next_id = 1;
|
|
650
|
+
state->reqs = nullptr;
|
|
651
|
+
state->next_promise_active = false;
|
|
652
|
+
mik__hs_st(rt) = state;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
static int mik__hs_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
656
|
+
mik__hs_ensure_initialized(ctx);
|
|
657
|
+
JS_SetModuleExport(ctx, m, "start", JS_NewCFunction(ctx, mik__hs_start, "start", 2));
|
|
658
|
+
JS_SetModuleExport(ctx, m, "stop", JS_NewCFunction(ctx, mik__hs_stop, "stop", 0));
|
|
659
|
+
JS_SetModuleExport(ctx, m, "nextRequest",
|
|
660
|
+
JS_NewCFunction(ctx, mik__hs_next_request, "nextRequest", 0));
|
|
661
|
+
JS_SetModuleExport(ctx, m, "respond", JS_NewCFunction(ctx, mik__hs_respond, "respond", 4));
|
|
662
|
+
JS_SetModuleExport(ctx, m, "respondStart",
|
|
663
|
+
JS_NewCFunction(ctx, mik__hs_respond_start, "respondStart", 3));
|
|
664
|
+
JS_SetModuleExport(ctx, m, "respondChunk",
|
|
665
|
+
JS_NewCFunction(ctx, mik__hs_respond_chunk, "respondChunk", 2));
|
|
666
|
+
JS_SetModuleExport(ctx, m, "respondEnd",
|
|
667
|
+
JS_NewCFunction(ctx, mik__hs_respond_end, "respondEnd", 1));
|
|
668
|
+
JS_SetModuleExport(ctx, m, "getHeader",
|
|
669
|
+
JS_NewCFunction(ctx, mik__hs_get_header, "getHeader", 2));
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
static JSModuleDef* mik__http_server_init(JSContext* ctx) {
|
|
674
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
675
|
+
mik__http_server_slot = MIK_AllocModuleSlot(rt);
|
|
676
|
+
|
|
677
|
+
JSModuleDef* m = JS_NewCModule(ctx, "native:http_server", mik__hs_module_init);
|
|
678
|
+
if (!m) return nullptr;
|
|
679
|
+
JS_AddModuleExport(ctx, m, "start");
|
|
680
|
+
JS_AddModuleExport(ctx, m, "stop");
|
|
681
|
+
JS_AddModuleExport(ctx, m, "nextRequest");
|
|
682
|
+
JS_AddModuleExport(ctx, m, "respond");
|
|
683
|
+
JS_AddModuleExport(ctx, m, "respondStart");
|
|
684
|
+
JS_AddModuleExport(ctx, m, "respondChunk");
|
|
685
|
+
JS_AddModuleExport(ctx, m, "respondEnd");
|
|
686
|
+
JS_AddModuleExport(ctx, m, "getHeader");
|
|
687
|
+
return m;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* ── Event loop consumption ────────────────────────────────────────── */
|
|
691
|
+
|
|
692
|
+
void mik__http_server_consume(JSContext* ctx) {
|
|
693
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
694
|
+
CHECK_NOT_NULL(rt);
|
|
695
|
+
MIKHttpServerState* state = mik__hs_st(rt);
|
|
696
|
+
if (!state) return;
|
|
697
|
+
|
|
698
|
+
/* Resolve START/CHUNK acks (backpressure) before delivering new requests. */
|
|
699
|
+
MIKHsAck ack;
|
|
700
|
+
while (xQueueReceive(state->ack_queue, &ack, 0) == pdTRUE) {
|
|
701
|
+
for (MIKHsReq* r = state->reqs; r; r = r->next) {
|
|
702
|
+
if (r->exchange == ack.ex && r->chunk_promise_active) {
|
|
703
|
+
JSValue v = JS_NewBool(ctx, ack.ok);
|
|
704
|
+
MIK_ResolvePromise(ctx, &r->chunk_promise, 1, &v);
|
|
705
|
+
r->chunk_promise_active = false;
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
MIKHsMsg msg;
|
|
712
|
+
while (xQueueReceive(state->request_queue, &msg, 0) == pdTRUE) {
|
|
713
|
+
auto* r = new MIKHsReq();
|
|
714
|
+
r->id = state->next_id++;
|
|
715
|
+
r->exchange = msg.exchange;
|
|
716
|
+
r->method = msg.method;
|
|
717
|
+
r->uri = msg.uri;
|
|
718
|
+
r->body = msg.body;
|
|
719
|
+
r->body_len = msg.body_len;
|
|
720
|
+
r->content_length = msg.content_length;
|
|
721
|
+
r->body_too_large = msg.body_too_large;
|
|
722
|
+
r->delivered = false;
|
|
723
|
+
r->chunk_promise_active = false;
|
|
724
|
+
r->next = nullptr;
|
|
725
|
+
|
|
726
|
+
if (!state->reqs) {
|
|
727
|
+
state->reqs = r;
|
|
728
|
+
} else {
|
|
729
|
+
MIKHsReq* t = state->reqs;
|
|
730
|
+
while (t->next) t = t->next;
|
|
731
|
+
t->next = r;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (state->next_promise_active) {
|
|
735
|
+
JSValue desc = mik__hs_build_descriptor(ctx, r);
|
|
736
|
+
mik__hs_mark_delivered(r);
|
|
737
|
+
MIK_ResolvePromise(ctx, &state->next_promise, 1, &desc);
|
|
738
|
+
state->next_promise_active = false;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
void mik__http_server_destroy(JSContext* ctx) {
|
|
744
|
+
MIKRuntime* rt = MIK_GetRuntime(ctx);
|
|
745
|
+
CHECK_NOT_NULL(rt);
|
|
746
|
+
MIKHttpServerState* state = mik__hs_st(rt);
|
|
747
|
+
if (!state) return;
|
|
748
|
+
|
|
749
|
+
if (state->server) {
|
|
750
|
+
state->stopping = true;
|
|
751
|
+
httpd_stop(state->server);
|
|
752
|
+
state->server = nullptr;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
MIKHsMsg msg;
|
|
756
|
+
while (xQueueReceive(state->request_queue, &msg, 0) == pdTRUE) {
|
|
757
|
+
free(msg.method);
|
|
758
|
+
free(msg.uri);
|
|
759
|
+
free(msg.body);
|
|
760
|
+
}
|
|
761
|
+
MIKHsAck ack;
|
|
762
|
+
while (xQueueReceive(state->ack_queue, &ack, 0) == pdTRUE) { /* discard */ }
|
|
763
|
+
|
|
764
|
+
MIKHsReq* r = state->reqs;
|
|
765
|
+
while (r) {
|
|
766
|
+
MIKHsReq* next = r->next;
|
|
767
|
+
if (!r->delivered) {
|
|
768
|
+
free(r->method);
|
|
769
|
+
free(r->uri);
|
|
770
|
+
free(r->body);
|
|
771
|
+
}
|
|
772
|
+
if (r->chunk_promise_active) MIK_FreePromise(ctx, &r->chunk_promise);
|
|
773
|
+
delete r;
|
|
774
|
+
r = next;
|
|
775
|
+
}
|
|
776
|
+
state->reqs = nullptr;
|
|
777
|
+
|
|
778
|
+
if (state->next_promise_active) {
|
|
779
|
+
MIK_FreePromise(ctx, &state->next_promise);
|
|
780
|
+
state->next_promise_active = false;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
vQueueDelete(state->request_queue);
|
|
784
|
+
vQueueDelete(state->ack_queue);
|
|
785
|
+
delete state;
|
|
786
|
+
mik__hs_st(rt) = nullptr;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
MIK_REGISTER_MODULE(http_server, "native:http_server", mik__http_server_init,
|
|
790
|
+
mik__http_server_consume, mik__http_server_destroy)
|
|
@@ -347,15 +347,21 @@ void MIK_Main(void) {
|
|
|
347
347
|
* (USB-Serial/JTAG). `printf` goes to newlib stdio, which
|
|
348
348
|
* ESP-IDF routes to UART0 and is invisible over USB. */
|
|
349
349
|
const char* core_word = chip_info.cores == 1 ? "core" : "cores";
|
|
350
|
+
const MIKPlatform* plat = MIK_GetPlatform();
|
|
351
|
+
const char* dev_id = plat->get_device_id ? plat->get_device_id() : nullptr;
|
|
350
352
|
char banner[160];
|
|
351
353
|
int off = 0;
|
|
352
354
|
#ifdef MIK_FW_VERSION
|
|
353
|
-
off += snprintf(banner + off, sizeof(banner) - off, "Mikro.js v%s on %s,
|
|
354
|
-
|
|
355
|
+
off += snprintf(banner + off, sizeof(banner) - off, "Mikro.js v%s on %s", MIK_FW_VERSION,
|
|
356
|
+
CONFIG_IDF_TARGET);
|
|
355
357
|
#else
|
|
356
|
-
off += snprintf(banner + off, sizeof(banner) - off, "Mikro.js on %s,
|
|
357
|
-
CONFIG_IDF_TARGET, chip_info.cores, core_word);
|
|
358
|
+
off += snprintf(banner + off, sizeof(banner) - off, "Mikro.js on %s", CONFIG_IDF_TARGET);
|
|
358
359
|
#endif
|
|
360
|
+
/* Stable per-board device id (chip MAC as Crockford Base32). */
|
|
361
|
+
if (dev_id && dev_id[0]) {
|
|
362
|
+
off += snprintf(banner + off, sizeof(banner) - off, " (%s)", dev_id);
|
|
363
|
+
}
|
|
364
|
+
off += snprintf(banner + off, sizeof(banner) - off, ", %d %s", chip_info.cores, core_word);
|
|
359
365
|
if (has_wifi || has_bt || has_ble) {
|
|
360
366
|
off += snprintf(banner + off, sizeof(banner) - off, ", ");
|
|
361
367
|
bool first = true;
|
|
@@ -148,7 +148,12 @@ static const char* esp32_get_device_id(void) {
|
|
|
148
148
|
static char id[11] = {0}; /* 6 bytes (48 bits) -> 10 x 5-bit chars + NUL */
|
|
149
149
|
if (id[0] == '\0') {
|
|
150
150
|
uint8_t m[6];
|
|
151
|
-
esp_efuse_mac_get_default(
|
|
151
|
+
/* Use the 48-bit base MAC, NOT esp_efuse_mac_get_default(): on 802.15.4
|
|
152
|
+
* chips (C6/H2) the factory MAC is an 8-byte EUI-64, so that call writes
|
|
153
|
+
* 8 bytes (overflowing this 6-byte buffer) and a 6-byte read yields the
|
|
154
|
+
* OUI+FF:FE prefix rather than a unique chip id. esp_read_mac(ESP_MAC_BASE)
|
|
155
|
+
* returns the proper 48-bit base MAC on every target. */
|
|
156
|
+
esp_read_mac(m, ESP_MAC_BASE);
|
|
152
157
|
/* Pack 6 bytes into a 48-bit value, then extract 5 bits at a time
|
|
153
158
|
* from the most significant end. */
|
|
154
159
|
uint64_t v = ((uint64_t)m[0] << 40) | ((uint64_t)m[1] << 32) |
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#include <cstring>
|
|
2
|
+
|
|
3
|
+
#include "freertos/FreeRTOS.h"
|
|
4
|
+
#include "freertos/queue.h"
|
|
5
|
+
#include "freertos/semphr.h"
|
|
6
|
+
#include "mik_http_server_internal.h"
|
|
7
|
+
#include "mikrojs.h"
|
|
8
|
+
#include "private.h"
|
|
9
|
+
#include "quickjs.h"
|
|
10
|
+
#include "unity.h"
|
|
11
|
+
#include "utils.h"
|
|
12
|
+
|
|
13
|
+
/* Access the http_server module slot and consume function from
|
|
14
|
+
* mik_http_server.cpp. These tests drive the module's queues directly — no
|
|
15
|
+
* real esp_http_server / network — the same approach as http_test.cpp. */
|
|
16
|
+
extern int mik__http_server_slot;
|
|
17
|
+
extern void mik__http_server_consume(JSContext* ctx);
|
|
18
|
+
|
|
19
|
+
static inline MIKHttpServerState*& mik__hs(MIKRuntime* r) {
|
|
20
|
+
return reinterpret_cast<MIKHttpServerState*&>(r->module_data[mik__http_server_slot]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ── Test scaffolding (same teardown-before-assert convention as http_test) ── */
|
|
24
|
+
|
|
25
|
+
static MIKRuntime* rt;
|
|
26
|
+
static JSContext* ctx;
|
|
27
|
+
|
|
28
|
+
static void setup() {
|
|
29
|
+
rt = MIK_NewRuntime();
|
|
30
|
+
ctx = MIK_GetJSContext(rt);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static void teardown() { MIK_FreeRuntime(rt); }
|
|
34
|
+
|
|
35
|
+
static JSValue eval_module(const char* code) {
|
|
36
|
+
JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/test", code, strlen(code));
|
|
37
|
+
if (!JS_IsException(ret)) {
|
|
38
|
+
JS_FreeValue(ctx, ret);
|
|
39
|
+
mik__execute_jobs(ctx);
|
|
40
|
+
}
|
|
41
|
+
return ret;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static void ensure_initialized() {
|
|
45
|
+
const char* code = "import { nextRequest } from 'native:http_server';";
|
|
46
|
+
JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/test", code, strlen(code));
|
|
47
|
+
if (!JS_IsException(ret)) {
|
|
48
|
+
JS_FreeValue(ctx, ret);
|
|
49
|
+
mik__execute_jobs(ctx);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static void read_global_string(const char* key, char* out, size_t out_len) {
|
|
54
|
+
JSValue global = JS_GetGlobalObject(ctx);
|
|
55
|
+
JSValue v = JS_GetPropertyStr(ctx, global, key);
|
|
56
|
+
const char* s = JS_ToCString(ctx, v);
|
|
57
|
+
if (s) {
|
|
58
|
+
strncpy(out, s, out_len - 1);
|
|
59
|
+
out[out_len - 1] = '\0';
|
|
60
|
+
JS_FreeCString(ctx, s);
|
|
61
|
+
} else {
|
|
62
|
+
out[0] = '\0';
|
|
63
|
+
}
|
|
64
|
+
JS_FreeValue(ctx, v);
|
|
65
|
+
JS_FreeValue(ctx, global);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static bool read_global_bool(const char* key) {
|
|
69
|
+
JSValue global = JS_GetGlobalObject(ctx);
|
|
70
|
+
JSValue v = JS_GetPropertyStr(ctx, global, key);
|
|
71
|
+
bool b = JS_ToBool(ctx, v);
|
|
72
|
+
JS_FreeValue(ctx, v);
|
|
73
|
+
JS_FreeValue(ctx, global);
|
|
74
|
+
return b;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Push a request the way the httpd handler would: allocate an exchange with a
|
|
78
|
+
* command semaphore, hand method/uri ownership to the module via the queue. The
|
|
79
|
+
* exchange is returned so the test can inspect what respond() wrote and free it
|
|
80
|
+
* (no real httpd task exists to consume cmd_ready and free it). */
|
|
81
|
+
static MIKHsExchange* push_request(const char* method, const char* uri) {
|
|
82
|
+
auto* ex = new MIKHsExchange(); // value-initialized: all fields zero/null
|
|
83
|
+
ex->cmd_ready = xSemaphoreCreateBinary();
|
|
84
|
+
ex->req = nullptr;
|
|
85
|
+
|
|
86
|
+
MIKHsMsg msg = {};
|
|
87
|
+
msg.exchange = ex;
|
|
88
|
+
msg.method = strdup(method);
|
|
89
|
+
msg.uri = strdup(uri);
|
|
90
|
+
xQueueSend(mik__hs(rt)->request_queue, &msg, 0);
|
|
91
|
+
return ex;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static void free_exchange(MIKHsExchange* ex) {
|
|
95
|
+
if (ex->cmd_ready) {
|
|
96
|
+
xSemaphoreTake(ex->cmd_ready, 0); // drain the give from respond()
|
|
97
|
+
vSemaphoreDelete(ex->cmd_ready);
|
|
98
|
+
}
|
|
99
|
+
for (size_t i = 0; i < ex->header_count; i++) {
|
|
100
|
+
free(ex->headers[i].key);
|
|
101
|
+
free(ex->headers[i].value);
|
|
102
|
+
}
|
|
103
|
+
free(ex->headers);
|
|
104
|
+
free(ex->body);
|
|
105
|
+
free(ex->chunk);
|
|
106
|
+
delete ex;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ── Tests ─────────────────────────────────────────────────────────── */
|
|
110
|
+
|
|
111
|
+
TEST_CASE("native:http_server exports the expected functions", "[httpd]") {
|
|
112
|
+
setup();
|
|
113
|
+
JSValue ret = eval_module(R"(
|
|
114
|
+
import { start, stop, nextRequest, respond,
|
|
115
|
+
respondStart, respondChunk, respondEnd, getHeader } from "native:http_server";
|
|
116
|
+
globalThis.__t = [start, stop, nextRequest, respond,
|
|
117
|
+
respondStart, respondChunk, respondEnd, getHeader]
|
|
118
|
+
.every((f) => typeof f === "function") ? "ok" : "bad";
|
|
119
|
+
)");
|
|
120
|
+
bool evalOk = !JS_IsException(ret);
|
|
121
|
+
char t[8];
|
|
122
|
+
read_global_string("__t", t, sizeof(t));
|
|
123
|
+
teardown();
|
|
124
|
+
|
|
125
|
+
TEST_ASSERT_TRUE_MESSAGE(evalOk, "module eval should not throw");
|
|
126
|
+
TEST_ASSERT_EQUAL_STRING("ok", t);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
TEST_CASE("http_server consume with no messages is a no-op", "[httpd]") {
|
|
130
|
+
setup();
|
|
131
|
+
ensure_initialized();
|
|
132
|
+
mik__http_server_consume(ctx);
|
|
133
|
+
bool reqsEmpty = (mik__hs(rt)->reqs == nullptr);
|
|
134
|
+
teardown();
|
|
135
|
+
|
|
136
|
+
TEST_ASSERT_TRUE(reqsEmpty);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
TEST_CASE("nextRequest delivers method/url and respond fills the exchange", "[httpd]") {
|
|
140
|
+
setup();
|
|
141
|
+
ensure_initialized();
|
|
142
|
+
|
|
143
|
+
MIKHsExchange* ex = push_request("GET", "/hello?x=1");
|
|
144
|
+
|
|
145
|
+
eval_module(R"(
|
|
146
|
+
import { nextRequest, respond } from 'native:http_server';
|
|
147
|
+
globalThis.__method = '';
|
|
148
|
+
globalThis.__url = '';
|
|
149
|
+
(async () => {
|
|
150
|
+
const r = await nextRequest();
|
|
151
|
+
globalThis.__method = r.method;
|
|
152
|
+
globalThis.__url = r.url;
|
|
153
|
+
respond(r.id, 200, [['X-Test', 'yes']], new Uint8Array([65, 66, 67]));
|
|
154
|
+
})();
|
|
155
|
+
)");
|
|
156
|
+
|
|
157
|
+
mik__http_server_consume(ctx);
|
|
158
|
+
mik__execute_jobs(ctx);
|
|
159
|
+
|
|
160
|
+
char method[16], url[32];
|
|
161
|
+
read_global_string("__method", method, sizeof(method));
|
|
162
|
+
read_global_string("__url", url, sizeof(url));
|
|
163
|
+
|
|
164
|
+
int status = ex->status;
|
|
165
|
+
size_t bodyLen = ex->body_len;
|
|
166
|
+
char body[8] = {};
|
|
167
|
+
if (ex->body && bodyLen < sizeof(body)) memcpy(body, ex->body, bodyLen);
|
|
168
|
+
size_t headerCount = ex->header_count;
|
|
169
|
+
|
|
170
|
+
free_exchange(ex);
|
|
171
|
+
teardown();
|
|
172
|
+
|
|
173
|
+
TEST_ASSERT_EQUAL_STRING("GET", method);
|
|
174
|
+
TEST_ASSERT_EQUAL_STRING("/hello?x=1", url);
|
|
175
|
+
TEST_ASSERT_EQUAL_INT(200, status);
|
|
176
|
+
TEST_ASSERT_EQUAL_UINT(3, bodyLen);
|
|
177
|
+
TEST_ASSERT_EQUAL_STRING("ABC", body);
|
|
178
|
+
TEST_ASSERT_EQUAL_UINT(1, headerCount);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Regression: a response with no body (e.g. a 303 redirect) must not throw in
|
|
182
|
+
* native respond(). Before the fix, respond() called JS_GetUint8Array on an
|
|
183
|
+
* undefined body and left a pending TypeError. */
|
|
184
|
+
TEST_CASE("respond tolerates a bodyless response", "[httpd]") {
|
|
185
|
+
setup();
|
|
186
|
+
ensure_initialized();
|
|
187
|
+
|
|
188
|
+
MIKHsExchange* ex = push_request("POST", "/save");
|
|
189
|
+
|
|
190
|
+
eval_module(R"(
|
|
191
|
+
import { nextRequest, respond } from 'native:http_server';
|
|
192
|
+
globalThis.__threw = false;
|
|
193
|
+
(async () => {
|
|
194
|
+
const r = await nextRequest();
|
|
195
|
+
try {
|
|
196
|
+
respond(r.id, 303, [['location', '/']], undefined);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
globalThis.__threw = true;
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
)");
|
|
202
|
+
|
|
203
|
+
mik__http_server_consume(ctx);
|
|
204
|
+
mik__execute_jobs(ctx);
|
|
205
|
+
|
|
206
|
+
bool threw = read_global_bool("__threw");
|
|
207
|
+
int status = ex->status;
|
|
208
|
+
bool bodyNull = (ex->body == nullptr);
|
|
209
|
+
|
|
210
|
+
free_exchange(ex);
|
|
211
|
+
teardown();
|
|
212
|
+
|
|
213
|
+
TEST_ASSERT_FALSE_MESSAGE(threw, "bodyless respond must not throw");
|
|
214
|
+
TEST_ASSERT_EQUAL_INT(303, status);
|
|
215
|
+
TEST_ASSERT_TRUE_MESSAGE(bodyNull, "bodyless respond leaves the body unset");
|
|
216
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/firmware",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0-next.9.g2e06437",
|
|
4
4
|
"description": "Mikro.js ESP32 firmware: ESP-IDF component, build tools, and project template",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp-idf",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"esbuild": "^0.28.0",
|
|
54
|
-
"@mikrojs/native": "0.
|
|
55
|
-
"@mikrojs/quickjs": "0.
|
|
54
|
+
"@mikrojs/native": "0.12.0-next.9.g2e06437",
|
|
55
|
+
"@mikrojs/quickjs": "0.12.0-next.9.g2e06437"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=24.0.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|