@mikrojs/firmware 0.18.0 → 0.18.1
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 +12 -1
- package/components/mikrojs/include/mik_http_internal.h +47 -0
- package/components/mikrojs/include/mik_ota_native.h +19 -0
- package/components/mikrojs/mik_config.cpp +28 -5
- package/components/mikrojs/mik_http.cpp +199 -8
- package/components/mikrojs/mik_nvs_kv.cpp +28 -5
- package/components/mikrojs/mik_ota.cpp +305 -106
- package/components/mikrojs/mik_ota_client_module.cpp +743 -0
- package/components/mikrojs/mik_ota_env.cpp +370 -0
- package/components/mikrojs/test/ota_host/run.sh +7 -2
- 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
- package/sdkconfig.defaults +58 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIKOtaEnv for ESP-IDF: the platform side of the native OTA client.
|
|
3
|
+
*
|
|
4
|
+
* Every seam here is deliberately thin. The parts that are easy to get subtly
|
|
5
|
+
* wrong — how a mik.sys value is encoded, how the device-name pair is spelled —
|
|
6
|
+
* live in the portable library behind host tests (mikrojs/sys_codec.h), because
|
|
7
|
+
* getting them wrong means the C and JS implementations disagree about live
|
|
8
|
+
* device state in a way no compile catches.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
#include <esp_app_desc.h>
|
|
12
|
+
#include <esp_random.h>
|
|
13
|
+
#include <esp_system.h>
|
|
14
|
+
#include <esp_timer.h>
|
|
15
|
+
#include <nvs.h>
|
|
16
|
+
|
|
17
|
+
#include <cstdio>
|
|
18
|
+
#include <cstring>
|
|
19
|
+
|
|
20
|
+
#include "esp_log.h"
|
|
21
|
+
#include "mik_http_internal.h"
|
|
22
|
+
#include "mik_ota_native.h"
|
|
23
|
+
#include "mikrojs/mikrojs.h"
|
|
24
|
+
#include "mikrojs/private.h"
|
|
25
|
+
#include "mikrojs/platform.h"
|
|
26
|
+
#include "mikrojs/sys_codec.h"
|
|
27
|
+
|
|
28
|
+
#define MIK_OTA_ENV_TAG "native:mikro/ota_client"
|
|
29
|
+
#define MIK_OTA_SYS_NS "mik.sys"
|
|
30
|
+
|
|
31
|
+
namespace {
|
|
32
|
+
|
|
33
|
+
/* One env per runtime. The OTA client is a singleton within a runtime (one
|
|
34
|
+
* check-in loop per device), so a file-scope record is the whole lifetime story. */
|
|
35
|
+
struct EnvState {
|
|
36
|
+
MIKOtaEnv env;
|
|
37
|
+
MIKRuntime* rt;
|
|
38
|
+
int bytecode_version;
|
|
39
|
+
/* The device id string the platform owns; copied so identity() can hand back
|
|
40
|
+
* a fixed-size field without worrying about the platform's lifetime. */
|
|
41
|
+
char device_id[64];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
EnvState g_state = {};
|
|
45
|
+
|
|
46
|
+
// ── kv (mik.sys) ─────────────────────────────────────────────────────────────
|
|
47
|
+
// Values are the CBOR encoding of the value itself, exactly as
|
|
48
|
+
// native:mikro/nvs_kv writes them, so `ota.tries` means the same thing whether
|
|
49
|
+
// the C policy or the JS one reads it.
|
|
50
|
+
|
|
51
|
+
/* Absence is the only silent outcome: a missing namespace (nothing ever stored)
|
|
52
|
+
* or a missing key reads as absent. Every other failure is an error, so a read
|
|
53
|
+
* starved by heap pressure — nvs_open allocates its handle — is never mistaken
|
|
54
|
+
* for "not stored". Same rule as native:mikro/nvs_kv's get. */
|
|
55
|
+
MIKOtaKvStatus kv_read_blob(const char* key, uint8_t* out, size_t* inout_len) {
|
|
56
|
+
nvs_handle_t h;
|
|
57
|
+
esp_err_t err = nvs_open(MIK_OTA_SYS_NS, NVS_READONLY, &h);
|
|
58
|
+
if (err == ESP_ERR_NVS_NOT_FOUND) return MIK_OTA_KV_ABSENT;
|
|
59
|
+
if (err != ESP_OK) return MIK_OTA_KV_ERROR;
|
|
60
|
+
|
|
61
|
+
size_t len = 0;
|
|
62
|
+
err = nvs_get_blob(h, key, nullptr, &len);
|
|
63
|
+
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
|
64
|
+
nvs_close(h);
|
|
65
|
+
return MIK_OTA_KV_ABSENT;
|
|
66
|
+
}
|
|
67
|
+
if (err != ESP_OK) {
|
|
68
|
+
nvs_close(h);
|
|
69
|
+
return MIK_OTA_KV_ERROR;
|
|
70
|
+
}
|
|
71
|
+
if (len == 0) {
|
|
72
|
+
nvs_close(h);
|
|
73
|
+
return MIK_OTA_KV_ABSENT;
|
|
74
|
+
}
|
|
75
|
+
if (!out) {
|
|
76
|
+
*inout_len = len;
|
|
77
|
+
nvs_close(h);
|
|
78
|
+
return MIK_OTA_KV_OK;
|
|
79
|
+
}
|
|
80
|
+
if (*inout_len < len) {
|
|
81
|
+
nvs_close(h);
|
|
82
|
+
return MIK_OTA_KV_ERROR;
|
|
83
|
+
}
|
|
84
|
+
err = nvs_get_blob(h, key, out, &len);
|
|
85
|
+
nvs_close(h);
|
|
86
|
+
if (err != ESP_OK) return MIK_OTA_KV_ERROR;
|
|
87
|
+
*inout_len = len;
|
|
88
|
+
return MIK_OTA_KV_OK;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
bool kv_write_blob(const char* key, const uint8_t* data, size_t len) {
|
|
92
|
+
nvs_handle_t h;
|
|
93
|
+
if (nvs_open(MIK_OTA_SYS_NS, NVS_READWRITE, &h) != ESP_OK) return false;
|
|
94
|
+
esp_err_t err = nvs_set_blob(h, key, data, len);
|
|
95
|
+
if (err == ESP_OK) err = nvs_commit(h);
|
|
96
|
+
nvs_close(h);
|
|
97
|
+
return err == ESP_OK;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
MIKOtaKvStatus env_kv_get_blob(void*, const char* key, uint8_t* out, size_t* inout_len) {
|
|
101
|
+
return kv_read_blob(key, out, inout_len);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
bool env_kv_set_blob(void*, const char* key, const uint8_t* data, size_t len) {
|
|
105
|
+
return kv_write_blob(key, data, len);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
bool env_kv_get_str(void*, const char* key, char* out, size_t max_len) {
|
|
109
|
+
uint8_t buf[320];
|
|
110
|
+
size_t len = sizeof(buf);
|
|
111
|
+
if (kv_read_blob(key, buf, &len) != MIK_OTA_KV_OK) return false;
|
|
112
|
+
return mik__kv_decode_str(buf, len, out, max_len);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
bool env_kv_set_str(void*, const char* key, const char* value) {
|
|
116
|
+
uint8_t buf[320];
|
|
117
|
+
size_t needed = mik__kv_encode_str(value, buf, sizeof(buf));
|
|
118
|
+
if (needed > sizeof(buf)) return false;
|
|
119
|
+
return kv_write_blob(key, buf, needed);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
bool env_kv_get_i32(void*, const char* key, int32_t* out) {
|
|
123
|
+
uint8_t buf[16];
|
|
124
|
+
size_t len = sizeof(buf);
|
|
125
|
+
if (kv_read_blob(key, buf, &len) != MIK_OTA_KV_OK) return false;
|
|
126
|
+
return mik__kv_decode_i32(buf, len, out);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
bool env_kv_set_i32(void*, const char* key, int32_t value) {
|
|
130
|
+
uint8_t buf[16];
|
|
131
|
+
size_t needed = mik__kv_encode_i32(value, buf, sizeof(buf));
|
|
132
|
+
if (needed > sizeof(buf)) return false;
|
|
133
|
+
return kv_write_blob(key, buf, needed);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
bool env_kv_remove(void*, const char* key) {
|
|
137
|
+
nvs_handle_t h;
|
|
138
|
+
if (nvs_open(MIK_OTA_SYS_NS, NVS_READWRITE, &h) != ESP_OK) return false;
|
|
139
|
+
esp_err_t err = nvs_erase_key(h, key);
|
|
140
|
+
if (err == ESP_OK) err = nvs_commit(h);
|
|
141
|
+
nvs_close(h);
|
|
142
|
+
/* A key that was never there is not a failure: the client removes slots
|
|
143
|
+
* unconditionally. */
|
|
144
|
+
return err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── HTTP ─────────────────────────────────────────────────────────────────────
|
|
148
|
+
// Borrowed from native:mikro/http: its task, TLS setup, inflight ceiling and
|
|
149
|
+
// chunk budget. A second esp_http_client path would double the handshake heap
|
|
150
|
+
// spike this device has the least of.
|
|
151
|
+
|
|
152
|
+
void* env_http_request(void* opaque, const MIKOtaHttpRequest* req,
|
|
153
|
+
const MIKOtaHttpCallbacks* cbs) {
|
|
154
|
+
auto* state = static_cast<EnvState*>(opaque);
|
|
155
|
+
MIKHttpNativeRequest spec = {};
|
|
156
|
+
spec.url = req->url;
|
|
157
|
+
spec.method = req->method;
|
|
158
|
+
spec.header_keys = req->header_keys;
|
|
159
|
+
spec.header_values = req->header_values;
|
|
160
|
+
spec.header_count = req->header_count;
|
|
161
|
+
spec.body = req->body;
|
|
162
|
+
spec.body_len = req->body_len;
|
|
163
|
+
spec.timeout_ms = req->timeout_ms;
|
|
164
|
+
|
|
165
|
+
MIKHttpNativeSink sink = {};
|
|
166
|
+
sink.headers = cbs->headers;
|
|
167
|
+
sink.data = cbs->data;
|
|
168
|
+
sink.done = cbs->done;
|
|
169
|
+
sink.user_data = cbs->user_data;
|
|
170
|
+
|
|
171
|
+
uint32_t id = mik__http_start_native(state->rt, &spec, &sink);
|
|
172
|
+
if (id == 0) return nullptr;
|
|
173
|
+
/* The handle is the id, offset so it is never NULL for id 0's sake. */
|
|
174
|
+
return reinterpret_cast<void*>(static_cast<uintptr_t>(id));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
void env_http_cancel(void* opaque, void* handle) {
|
|
178
|
+
auto* state = static_cast<EnvState*>(opaque);
|
|
179
|
+
mik__http_cancel_native(state->rt, static_cast<uint32_t>(reinterpret_cast<uintptr_t>(handle)));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── identity and system ──────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
bool env_identity(void* opaque, MIKDeviceIdentity* out) {
|
|
185
|
+
auto* state = static_cast<EnvState*>(opaque);
|
|
186
|
+
if (!out) return false;
|
|
187
|
+
*out = {};
|
|
188
|
+
snprintf(out->device_id, sizeof(out->device_id), "%s", state->device_id);
|
|
189
|
+
#ifdef MIK_FW_VERSION
|
|
190
|
+
snprintf(out->firmware_version, sizeof(out->firmware_version), "%s", MIK_FW_VERSION);
|
|
191
|
+
#else
|
|
192
|
+
snprintf(out->firmware_version, sizeof(out->firmware_version), "0.0.0-dev");
|
|
193
|
+
#endif
|
|
194
|
+
const esp_app_desc_t* desc = esp_app_get_description();
|
|
195
|
+
for (int i = 0; i < 32; i++) {
|
|
196
|
+
snprintf(out->firmware_hash + i * 2, 3, "%02x", desc->app_elf_sha256[i]);
|
|
197
|
+
}
|
|
198
|
+
out->bytecode_version = state->bytecode_version;
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
bool env_storage_free(void*, size_t* out) {
|
|
203
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
204
|
+
size_t total = 0;
|
|
205
|
+
size_t used = 0;
|
|
206
|
+
if (!platform->get_fs_info || !platform->get_fs_info("user", &total, &used)) return false;
|
|
207
|
+
if (out) *out = total > used ? total - used : 0;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
bool env_get_device_name(void*, int* out_rev, char* out_name, size_t name_len) {
|
|
212
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
213
|
+
const char* stored = platform->get_device_name ? platform->get_device_name() : nullptr;
|
|
214
|
+
if (!stored) return false;
|
|
215
|
+
/* A pair that will not parse reads as never named, matching the JS reader. */
|
|
216
|
+
return mik__device_name_parse(stored, out_rev, out_name, name_len);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
void env_set_device_name(void*, int rev, const char* name) {
|
|
220
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
221
|
+
if (!platform->set_device_name) return;
|
|
222
|
+
char text[160];
|
|
223
|
+
size_t needed = mik__device_name_format(rev, name, text, sizeof(text));
|
|
224
|
+
if (needed >= sizeof(text)) return; /* an absurd name: leave the pair alone */
|
|
225
|
+
platform->set_device_name(text);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
void env_restart(void*) {
|
|
229
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
230
|
+
if (platform->restart) platform->restart();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
int64_t env_monotonic_ms(void*) { return esp_timer_get_time() / 1000; }
|
|
234
|
+
|
|
235
|
+
double env_random_fraction(void*) {
|
|
236
|
+
/* esp_random is uniform over the full 32-bit range; scale to [0, 1). */
|
|
237
|
+
return static_cast<double>(esp_random()) / 4294967296.0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
void env_log(void*, int level, const char* fmt, ...) {
|
|
241
|
+
va_list args;
|
|
242
|
+
va_start(args, fmt);
|
|
243
|
+
char buf[320];
|
|
244
|
+
vsnprintf(buf, sizeof(buf), fmt, args);
|
|
245
|
+
va_end(args);
|
|
246
|
+
switch (level) {
|
|
247
|
+
case MIK_LOG_ERROR:
|
|
248
|
+
ESP_LOGE(MIK_OTA_ENV_TAG, "%s", buf);
|
|
249
|
+
break;
|
|
250
|
+
case MIK_LOG_WARN:
|
|
251
|
+
ESP_LOGW(MIK_OTA_ENV_TAG, "%s", buf);
|
|
252
|
+
break;
|
|
253
|
+
case MIK_LOG_DEBUG:
|
|
254
|
+
ESP_LOGD(MIK_OTA_ENV_TAG, "%s", buf);
|
|
255
|
+
break;
|
|
256
|
+
default:
|
|
257
|
+
ESP_LOGI(MIK_OTA_ENV_TAG, "%s", buf);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Join a JS-level app path onto the runtime's fs base, the way the fs layer
|
|
263
|
+
* does for `readFile('/app/...')`. */
|
|
264
|
+
void resolve_app_path(EnvState* state, const char* leaf, char* out, size_t out_len) {
|
|
265
|
+
const char* root = state && state->rt
|
|
266
|
+
? (state->rt->fs_root ? state->rt->fs_root : state->rt->fs_base_path)
|
|
267
|
+
: nullptr;
|
|
268
|
+
snprintf(out, out_len, "%s%s", root ? root : "", leaf);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
char* env_read_manifest(void* opaque) {
|
|
272
|
+
auto* state = static_cast<EnvState*>(opaque);
|
|
273
|
+
char path[128];
|
|
274
|
+
resolve_app_path(state, "/app/mikro.app.json", path, sizeof(path));
|
|
275
|
+
FILE* f = fopen(path, "r");
|
|
276
|
+
if (!f) return nullptr;
|
|
277
|
+
fseek(f, 0, SEEK_END);
|
|
278
|
+
long size = ftell(f);
|
|
279
|
+
/* A manifest is a few hundred bytes of metadata plus whatever configDefaults
|
|
280
|
+
* carries. The bound is generous but present: this is read into RAM. */
|
|
281
|
+
if (size <= 0 || size > 8192) {
|
|
282
|
+
fclose(f);
|
|
283
|
+
return nullptr;
|
|
284
|
+
}
|
|
285
|
+
rewind(f);
|
|
286
|
+
char* text = static_cast<char*>(malloc(static_cast<size_t>(size) + 1));
|
|
287
|
+
if (!text) {
|
|
288
|
+
fclose(f);
|
|
289
|
+
return nullptr;
|
|
290
|
+
}
|
|
291
|
+
size_t read = fread(text, 1, static_cast<size_t>(size), f);
|
|
292
|
+
fclose(f);
|
|
293
|
+
text[read] = '\0';
|
|
294
|
+
return text;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
bool env_read_app_version(void* opaque, char* out, size_t out_len) {
|
|
298
|
+
auto* state = static_cast<EnvState*>(opaque);
|
|
299
|
+
/* The live app version, the same file `ota.ts` reads as /app/package.json.
|
|
300
|
+
* That is a JS-level path: the fs layer joins it onto the runtime's base
|
|
301
|
+
* path, so reaching it from C means joining it here too. Read with stdio
|
|
302
|
+
* rather than through the JS fs layer, because the client runs from C and
|
|
303
|
+
* must not need a JSContext for a report field. */
|
|
304
|
+
char path[128];
|
|
305
|
+
resolve_app_path(state, "/app/package.json", path, sizeof(path));
|
|
306
|
+
FILE* f = fopen(path, "r");
|
|
307
|
+
if (!f) return false;
|
|
308
|
+
/* First 511 bytes only: enough for the small package.json a packed app
|
|
309
|
+
* carries. A "version" key past that, or a nested one before the top-level
|
|
310
|
+
* key, misreports; keep version near the top of package.json. */
|
|
311
|
+
char buf[512];
|
|
312
|
+
size_t read = fread(buf, 1, sizeof(buf) - 1, f);
|
|
313
|
+
fclose(f);
|
|
314
|
+
buf[read] = '\0';
|
|
315
|
+
|
|
316
|
+
/* Minimal scan for the top-level "version" string. A full JSON parse would
|
|
317
|
+
* mean pulling one in for a single field. */
|
|
318
|
+
const char* key = strstr(buf, "\"version\"");
|
|
319
|
+
if (!key) return false;
|
|
320
|
+
const char* colon = strchr(key, ':');
|
|
321
|
+
if (!colon) return false;
|
|
322
|
+
const char* quote = strchr(colon, '"');
|
|
323
|
+
if (!quote) return false;
|
|
324
|
+
const char* end = strchr(quote + 1, '"');
|
|
325
|
+
if (!end) return false;
|
|
326
|
+
size_t len = static_cast<size_t>(end - quote - 1);
|
|
327
|
+
if (len == 0 || len + 1 > out_len) return false;
|
|
328
|
+
memcpy(out, quote + 1, len);
|
|
329
|
+
out[len] = '\0';
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
} // namespace
|
|
334
|
+
|
|
335
|
+
const MIKOtaEnv* mik__ota_env_for(MIKRuntime* rt, int bytecode_version) {
|
|
336
|
+
g_state.rt = rt;
|
|
337
|
+
g_state.bytecode_version = bytecode_version;
|
|
338
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
339
|
+
const char* id = platform->get_device_id ? platform->get_device_id() : nullptr;
|
|
340
|
+
snprintf(g_state.device_id, sizeof(g_state.device_id), "%s", id ? id : "");
|
|
341
|
+
|
|
342
|
+
MIKOtaEnv& env = g_state.env;
|
|
343
|
+
env = {};
|
|
344
|
+
env.opaque = &g_state;
|
|
345
|
+
|
|
346
|
+
env.http_request = env_http_request;
|
|
347
|
+
env.http_cancel = env_http_cancel;
|
|
348
|
+
|
|
349
|
+
env.kv_get_blob = env_kv_get_blob;
|
|
350
|
+
env.kv_set_blob = env_kv_set_blob;
|
|
351
|
+
env.kv_get_str = env_kv_get_str;
|
|
352
|
+
env.kv_set_str = env_kv_set_str;
|
|
353
|
+
env.kv_get_i32 = env_kv_get_i32;
|
|
354
|
+
env.kv_set_i32 = env_kv_set_i32;
|
|
355
|
+
env.kv_remove = env_kv_remove;
|
|
356
|
+
|
|
357
|
+
mik__ota_fill_install_ops(&env);
|
|
358
|
+
|
|
359
|
+
env.identity = env_identity;
|
|
360
|
+
env.storage_free = env_storage_free;
|
|
361
|
+
env.get_device_name = env_get_device_name;
|
|
362
|
+
env.set_device_name = env_set_device_name;
|
|
363
|
+
env.restart = env_restart;
|
|
364
|
+
env.monotonic_ms = env_monotonic_ms;
|
|
365
|
+
env.random_fraction = env_random_fraction;
|
|
366
|
+
env.log = env_log;
|
|
367
|
+
env.read_app_version = env_read_app_version;
|
|
368
|
+
env.read_manifest = env_read_manifest;
|
|
369
|
+
return &env;
|
|
370
|
+
}
|
|
@@ -93,9 +93,14 @@ echo "== corrupt/truncated rejection =="
|
|
|
93
93
|
head -c "$(( $(wc -c < "$BUILD/small.tgz") - 16 ))" "$BUILD/small.tgz" > "$BUILD/trunc.tgz"
|
|
94
94
|
"$TEST" expect-fail "$BUILD/trunc.tgz" "$BUILD/out_trunc" >/dev/null && echo " truncated rejected ✓" \
|
|
95
95
|
|| { echo "FAIL: truncated build was accepted"; exit 1; }
|
|
96
|
-
# Corrupt: flip a byte in the deflate body.
|
|
96
|
+
# Corrupt: flip a byte in the deflate body. XOR rather than overwrite: the
|
|
97
|
+
# fixture's compressed bytes shift with the host's gzip, and overwriting with a
|
|
98
|
+
# constant is a no-op whenever the byte already holds that value, which turns
|
|
99
|
+
# this case into "valid build rejected as accepted" on some runner images.
|
|
97
100
|
cp "$BUILD/large.tgz" "$BUILD/corrupt.tgz"
|
|
98
|
-
|
|
101
|
+
orig=$(dd if="$BUILD/corrupt.tgz" bs=1 skip=64 count=1 2>/dev/null | od -An -tu1 | tr -d ' ')
|
|
102
|
+
printf "\\$(printf '%03o' $((orig ^ 0x5a)))" \
|
|
103
|
+
| dd of="$BUILD/corrupt.tgz" bs=1 seek=64 count=1 conv=notrunc 2>/dev/null
|
|
99
104
|
"$TEST" expect-fail "$BUILD/corrupt.tgz" "$BUILD/out_corrupt" >/dev/null && echo " corrupt rejected ✓" \
|
|
100
105
|
|| { echo "FAIL: corrupt build was accepted"; exit 1; }
|
|
101
106
|
# Not gzip at all.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/firmware",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Mikro.js ESP32 firmware: ESP-IDF component, build tools, and project template",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp-idf",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"esbuild": "^0.28.0",
|
|
50
|
-
"@mikrojs/native": "0.18.
|
|
51
|
-
"@mikrojs/quickjs": "0.18.
|
|
50
|
+
"@mikrojs/native": "0.18.1",
|
|
51
|
+
"@mikrojs/quickjs": "0.18.1"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24.0.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/sdkconfig.defaults
CHANGED
|
@@ -32,6 +32,22 @@ CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH=y
|
|
|
32
32
|
CONFIG_ESP_WIFI_IRAM_OPT=n
|
|
33
33
|
CONFIG_ESP_WIFI_RX_IRAM_OPT=n
|
|
34
34
|
|
|
35
|
+
# Trim WiFi driver buffers toward Espressif's low-RAM rank. The IDF defaults
|
|
36
|
+
# left too little heap for a TLS handshake next to a JS runtime on no-PSRAM
|
|
37
|
+
# chips. STATIC_RX is a real ~1.6 KB/buffer allocation made at wifi init; the
|
|
38
|
+
# DYNAMIC_* counts are ceilings, so they raise the guaranteed-free floor
|
|
39
|
+
# rather than idle heap. Paired with the lwIP window below, which the WiFi
|
|
40
|
+
# performance guide says must track the DYNAMIC_* counts.
|
|
41
|
+
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=6
|
|
42
|
+
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=10
|
|
43
|
+
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=10
|
|
44
|
+
# IDF default, and within the guide's min(2 x STATIC_RX, DYNAMIC_RX) bound.
|
|
45
|
+
# Lower values throttle downlink aggregation for a few pointers of RAM; to
|
|
46
|
+
# actually reclaim the reorder path, disable AMPDU RX instead.
|
|
47
|
+
CONFIG_ESP_WIFI_RX_BA_WIN=6
|
|
48
|
+
# Management short buffers. 6 is the Kconfig minimum and enough for a station.
|
|
49
|
+
CONFIG_ESP_WIFI_MGMT_SBUF_NUM=6
|
|
50
|
+
|
|
35
51
|
# SPI master ISR doesn't need to run during flash writes
|
|
36
52
|
CONFIG_SPI_MASTER_ISR_IN_IRAM=n
|
|
37
53
|
|
|
@@ -106,6 +122,31 @@ CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=0
|
|
|
106
122
|
# native module won't even compile. Cost is roughly 5–10 KB flash.
|
|
107
123
|
CONFIG_LWIP_IPV6=y
|
|
108
124
|
|
|
125
|
+
# TIME_WAIT is 2xMSL. At the lwIP default (60s MSL) every closed TLS
|
|
126
|
+
# connection leaves heap residue for two minutes; on a client checking in
|
|
127
|
+
# on an interval, that residue fragments the heap until TLS handshakes
|
|
128
|
+
# fail (measured on esp32c6: largest free block ratchets down each round).
|
|
129
|
+
# 6s is safe for an outbound-only client: stale-segment reuse needs the
|
|
130
|
+
# same ephemeral port against the same server inside the window, and a
|
|
131
|
+
# stale segment that did land fails the TLS record MAC anyway. Note this is
|
|
132
|
+
# a global lwIP setting: in SoftAP mode the device is the side that lands in
|
|
133
|
+
# TIME_WAIT, where a short MSL is a weaker guarantee.
|
|
134
|
+
CONFIG_LWIP_TCP_MSL=6000
|
|
135
|
+
# FIN_WAIT_2 is the same lingering-socket story when the peer half-closes;
|
|
136
|
+
# the IDF lwIP guide names both timers for minimum-RAM builds.
|
|
137
|
+
CONFIG_LWIP_TCP_FIN_WAIT_TIMEOUT=6000
|
|
138
|
+
|
|
139
|
+
# Per-connection TCP buffering, and the paired half of the WiFi DYNAMIC_*
|
|
140
|
+
# counts above. 2880 is 2xMSS: it halves lwIP's per-connection peak, at the
|
|
141
|
+
# cost of roughly doubling OTA image download time on a high-latency path.
|
|
142
|
+
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=2880
|
|
143
|
+
CONFIG_LWIP_TCP_WND_DEFAULT=2880
|
|
144
|
+
# Out-of-order segments are held as heap pbufs per pcb until the gap fills,
|
|
145
|
+
# which is exactly when a handshake wants a large contiguous block.
|
|
146
|
+
CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=2
|
|
147
|
+
# Bounds how many pbufs can queue up waiting on the tcpip task.
|
|
148
|
+
CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=12
|
|
149
|
+
|
|
109
150
|
# --- MbedTLS: TLS 1.2 only, AES-GCM/CCM, ECDHE ---
|
|
110
151
|
# 1.3's handshake working state plus its Kconfig-forced
|
|
111
152
|
# KEEP_PEER_CERTIFICATE add several KB of peak heap per connection, so disable for now
|
|
@@ -125,6 +166,10 @@ CONFIG_MBEDTLS_FS_IO=n
|
|
|
125
166
|
# Allocate TLS record buffers on demand (vs. a fixed 16 KB IN +
|
|
126
167
|
# 4 KB OUT block alive per connection). Incompatible with DTLS.
|
|
127
168
|
CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
|
|
169
|
+
# Every handshake TX step allocates OUT_CONTENT_LEN + record overhead, and it
|
|
170
|
+
# does so while the RX certificate buffer is still live. Caps the outbound
|
|
171
|
+
# record size, so large request bodies split across more records.
|
|
172
|
+
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=2048
|
|
128
173
|
|
|
129
174
|
# Drop the peer cert chain after handshake validates it (~4 KB per
|
|
130
175
|
# connection). Note: TLS 1.3 force-selects this back on via Kconfig.
|
|
@@ -140,5 +185,18 @@ CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=n
|
|
|
140
185
|
|
|
141
186
|
# Use smaller cert bundle (common CAs only)
|
|
142
187
|
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y
|
|
188
|
+
# Deliberate deviation from IDF 6.0.2, which defaults this to n to save ~700 B
|
|
189
|
+
# of heap. We need it: with n, esp_crt_bundle looks up only the top served
|
|
190
|
+
# cert's issuer, so any chain that serves a cross-signed root fails outright.
|
|
191
|
+
# That is ~2 in 5 real hosts, mostly Google Trust Services roots cross-signed
|
|
192
|
+
# by GlobalSign: openai, anthropic, cloudflare, npm, pypi, discord, googleapis.
|
|
193
|
+
# Device-confirmed against registry.npmjs.org (MBEDTLS_ERR_X509_FATAL_ERROR,
|
|
194
|
+
# -0x3000). Neither cross-signing root ships in the CMN or the full bundle, so
|
|
195
|
+
# bundle selection is not an escape either.
|
|
196
|
+
#
|
|
197
|
+
# The cost on 6.0.2 is a leak: ~215 B of issuer-name copies per successful
|
|
198
|
+
# handshake, ~10 KB/day at a 30-minute check-in cadence. Fixed upstream by
|
|
199
|
+
# 0d8ff68f8a47 and a1f1d9072900, landing in an upcoming release. Until our
|
|
200
|
+
# IDF floor gets there, patch IDF rather than turning this off.
|
|
143
201
|
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y
|
|
144
202
|
|