@altronix/cli 0.12.0 → 0.15.0
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/dist/about.js +1 -1
- package/dist/build.js +91 -173
- package/dist/build.ui.d.ts +3 -3
- package/dist/build.ui.js +2 -2
- package/dist/cloud.js +1 -1
- package/dist/index.js +6 -110
- package/dist/net.js +1 -1
- package/dist/select.ui.d.ts +1 -1
- package/dist/stress.js +1 -1
- package/dist/update.d.ts +41 -1
- package/dist/update.js +155 -53
- package/dist/update.ui.d.ts +27 -0
- package/dist/update.ui.js +119 -0
- package/package.json +18 -18
package/dist/about.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { About, cborDecodeStr } from '@altronix/zdk';
|
|
1
|
+
import { About, cborDecodeStr } from '@altronix/zdk/common';
|
|
2
2
|
import { Linq } from '@altronix/device';
|
|
3
3
|
import { EmptyError, finalize, firstValueFrom, map, switchMap } from 'rxjs';
|
|
4
4
|
import logger from './logger.js';
|
package/dist/build.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
import { Ajv } from 'ajv';
|
|
3
|
+
import { parse as parseJsonc } from 'jsonc-parser';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import fs from 'node:fs';
|
|
5
6
|
import cp from 'node:child_process';
|
|
6
7
|
import inquirer from '@inquirer/confirm';
|
|
7
|
-
import {
|
|
8
|
+
import { concat, concatMap, EMPTY, EmptyError, from, last, lastValueFrom, map, merge, mergeMap, Observable, of, share, tap, toArray } from 'rxjs';
|
|
8
9
|
import { PassThrough } from 'node:stream';
|
|
9
10
|
import { render } from 'ink';
|
|
10
11
|
import React from 'react';
|
|
11
|
-
import Ui
|
|
12
|
+
import Ui from './build.ui.js';
|
|
12
13
|
import { keys } from './keys.js';
|
|
13
14
|
const schemaBoard = {
|
|
14
15
|
type: 'object',
|
|
@@ -59,28 +60,13 @@ const schemaBuild = {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
};
|
|
62
|
-
const schemaSeedle = {
|
|
63
|
-
type: 'object',
|
|
64
|
-
required: [],
|
|
65
|
-
additionalProperties: {
|
|
66
|
-
type: 'object',
|
|
67
|
-
required: ['installDir', 'files'],
|
|
68
|
-
properties: {
|
|
69
|
-
namespace: { type: 'string', nullable: true },
|
|
70
|
-
prefix: { type: 'string', nullable: true },
|
|
71
|
-
installDir: { type: 'string' },
|
|
72
|
-
files: { type: 'array', items: { type: 'string' } }
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
63
|
const schema = {
|
|
77
64
|
type: 'object',
|
|
78
|
-
required: ['applications', 'bootloaders'
|
|
65
|
+
required: ['applications', 'bootloaders'],
|
|
79
66
|
additionalProperties: false,
|
|
80
67
|
properties: {
|
|
81
68
|
applications: schemaBuild,
|
|
82
|
-
bootloaders: schemaBuild
|
|
83
|
-
wasm: schemaSeedle
|
|
69
|
+
bootloaders: schemaBuild
|
|
84
70
|
}
|
|
85
71
|
};
|
|
86
72
|
const ajv = new Ajv({ allErrors: true, verbose: true });
|
|
@@ -126,17 +112,17 @@ async function resolver(cwd) {
|
|
|
126
112
|
}
|
|
127
113
|
async function parseAppVersion(v) {
|
|
128
114
|
const data = await fs.promises.readFile(v, 'ascii');
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
const
|
|
115
|
+
const major = data.matchAll(/^VERSION_MAJOR = ([0-9]+)/gm).next();
|
|
116
|
+
const minor = data.matchAll(/^VERSION_MINOR = ([0-9]+)/gm).next();
|
|
117
|
+
const patch = data.matchAll(/^PATCHLEVEL = ([0-9]+)/gm).next();
|
|
118
|
+
const tweak = data.matchAll(/^VERSION_TWEAK = ([0-9]+)/gm).next();
|
|
119
|
+
const extra = data.matchAll(/^EXTRAVERSION = ([\.a-zA-Z0-9-]+)/gm).next();
|
|
134
120
|
return {
|
|
135
|
-
major:
|
|
136
|
-
minor:
|
|
137
|
-
patch:
|
|
138
|
-
tweak:
|
|
139
|
-
extra:
|
|
121
|
+
major: (major.value && major.value[1] && parseInt(major.value[1])) || 0,
|
|
122
|
+
minor: (minor.value && minor.value[1] && parseInt(minor.value[1])) || 0,
|
|
123
|
+
patch: (patch.value && patch.value[1] && parseInt(patch.value[1])) || 0,
|
|
124
|
+
tweak: (tweak.value && tweak.value[1] && parseInt(tweak.value[1])) || 0,
|
|
125
|
+
extra: (extra.value && extra.value[1]) || ''
|
|
140
126
|
};
|
|
141
127
|
}
|
|
142
128
|
function formatVersion(ver) {
|
|
@@ -232,129 +218,6 @@ function west(args) {
|
|
|
232
218
|
err.on('data', (d) => (error += d));
|
|
233
219
|
})));
|
|
234
220
|
}
|
|
235
|
-
async function seedleOptionsNormalize(seedle, cwd, verbose) {
|
|
236
|
-
const name = seedle.__key;
|
|
237
|
-
const resolve = await resolver(cwd);
|
|
238
|
-
const types = resolve('<atx>/lib/atx/types.cddl');
|
|
239
|
-
const installDir = resolve(seedle.installDir);
|
|
240
|
-
const buildDir = path.join(installDir, name);
|
|
241
|
-
const files = [...seedle.files, types].map((file) => path.resolve(cwd, file));
|
|
242
|
-
return {
|
|
243
|
-
name: seedle.__key,
|
|
244
|
-
cwd,
|
|
245
|
-
installDir,
|
|
246
|
-
buildDir,
|
|
247
|
-
files,
|
|
248
|
-
cddl: path.join(installDir, name, `${name}.cddl`),
|
|
249
|
-
outputFile: path.join(installDir, `${name}-build.log`),
|
|
250
|
-
errorFile: path.join(installDir, `${name}-build.err`),
|
|
251
|
-
prefix: seedle.prefix || '',
|
|
252
|
-
namespace: seedle.namespace || 'altronix',
|
|
253
|
-
verbose
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
function seedle(opts) {
|
|
257
|
-
const { name, namespace, cddl, cwd, installDir } = opts;
|
|
258
|
-
const templatePath = path.resolve(cwd, '..', 'seedle-template');
|
|
259
|
-
const seedlePath = path.resolve(cwd, '..', 'seedle', 'seedle');
|
|
260
|
-
return of([
|
|
261
|
-
'generate',
|
|
262
|
-
'--force',
|
|
263
|
-
`--destination=${installDir}`,
|
|
264
|
-
`--path=${templatePath}`,
|
|
265
|
-
`--name=${name}`,
|
|
266
|
-
'--overwrite',
|
|
267
|
-
`-dnamespace=${namespace}`,
|
|
268
|
-
`-dseedle-manifest-path=${seedlePath.replace(/\\/g, '\\\\')}`,
|
|
269
|
-
`-dcddl=${cddl.replace(/\\/g, '\\\\')}`
|
|
270
|
-
]).pipe(mergeMap((seedleArgs) => new Observable((subscriber) => {
|
|
271
|
-
const wasm = cp.spawn('cargo', seedleArgs, {
|
|
272
|
-
cwd: installDir,
|
|
273
|
-
shell: true
|
|
274
|
-
});
|
|
275
|
-
const fout = fs.createWriteStream(opts.outputFile);
|
|
276
|
-
const ferr = fs.createWriteStream(opts.errorFile);
|
|
277
|
-
wasm.stdout.pipe(fout);
|
|
278
|
-
wasm.stderr.pipe(ferr);
|
|
279
|
-
wasm.on('error', (e) => {
|
|
280
|
-
subscriber.error(e);
|
|
281
|
-
fout.close();
|
|
282
|
-
ferr.close();
|
|
283
|
-
});
|
|
284
|
-
wasm.on('exit', () => {
|
|
285
|
-
fout.close();
|
|
286
|
-
ferr.close();
|
|
287
|
-
subscriber.next();
|
|
288
|
-
subscriber.complete();
|
|
289
|
-
});
|
|
290
|
-
})));
|
|
291
|
-
}
|
|
292
|
-
function cmakeItem(opts) {
|
|
293
|
-
const { name: item } = opts;
|
|
294
|
-
return { kind: 'wasm', item };
|
|
295
|
-
}
|
|
296
|
-
function cmakeConfigure(opts) {
|
|
297
|
-
const { buildDir, outputFile, errorFile } = opts;
|
|
298
|
-
const { item } = cmakeItem(opts);
|
|
299
|
-
return of([`-B${buildDir}`, `-S${buildDir}`]).pipe(mergeMap((cmakeArgs) => new Observable((subscriber) => {
|
|
300
|
-
const wasm = cp.spawn('cmake', cmakeArgs, {
|
|
301
|
-
cwd: buildDir,
|
|
302
|
-
shell: true
|
|
303
|
-
});
|
|
304
|
-
const fout = fs.createWriteStream(outputFile);
|
|
305
|
-
const ferr = fs.createWriteStream(errorFile);
|
|
306
|
-
const out = new PassThrough();
|
|
307
|
-
wasm.stdout.pipe(fout);
|
|
308
|
-
wasm.stdout.pipe(out);
|
|
309
|
-
wasm.stderr.pipe(ferr);
|
|
310
|
-
wasm.on('error', (e) => {
|
|
311
|
-
fout.close();
|
|
312
|
-
ferr.close();
|
|
313
|
-
out.destroy();
|
|
314
|
-
subscriber.error(new BuildError(item, 'wasm', e.name));
|
|
315
|
-
});
|
|
316
|
-
wasm.on('exit', () => {
|
|
317
|
-
fout.close();
|
|
318
|
-
ferr.close();
|
|
319
|
-
out.destroy();
|
|
320
|
-
subscriber.complete();
|
|
321
|
-
});
|
|
322
|
-
out.on('data', (data) => subscriber.next({ item, output: data.toString() }));
|
|
323
|
-
})));
|
|
324
|
-
}
|
|
325
|
-
function cmakeBuild(opts) {
|
|
326
|
-
const { buildDir, outputFile, errorFile } = opts;
|
|
327
|
-
const { item } = cmakeItem(opts);
|
|
328
|
-
return of([`--build`, `${buildDir}`, `--target`, ` wasm`]).pipe(mergeMap((cmakeArgs) => new Observable((subscriber) => {
|
|
329
|
-
const wasm = cp.spawn('cmake', cmakeArgs, {
|
|
330
|
-
cwd: buildDir,
|
|
331
|
-
shell: true
|
|
332
|
-
});
|
|
333
|
-
const fout = fs.createWriteStream(outputFile);
|
|
334
|
-
const ferr = fs.createWriteStream(errorFile);
|
|
335
|
-
const out = new PassThrough();
|
|
336
|
-
wasm.stdout.pipe(fout);
|
|
337
|
-
wasm.stdout.pipe(out);
|
|
338
|
-
wasm.stderr.pipe(ferr);
|
|
339
|
-
wasm.on('error', (e) => {
|
|
340
|
-
fout.close();
|
|
341
|
-
ferr.close();
|
|
342
|
-
out.destroy();
|
|
343
|
-
subscriber.error(new BuildError(item, 'wasm', e.name));
|
|
344
|
-
});
|
|
345
|
-
wasm.on('exit', () => {
|
|
346
|
-
fout.close();
|
|
347
|
-
ferr.close();
|
|
348
|
-
out.destroy();
|
|
349
|
-
subscriber.complete();
|
|
350
|
-
});
|
|
351
|
-
out.on('data', (data) => subscriber.next({ item, output: data.toString() }));
|
|
352
|
-
})));
|
|
353
|
-
}
|
|
354
|
-
function cmake(opts) {
|
|
355
|
-
const { item } = cmakeItem(opts);
|
|
356
|
-
return concat(cmakeConfigure(opts), cmakeBuild(opts), of({ item, complete: true })).pipe(catchError(of));
|
|
357
|
-
}
|
|
358
221
|
function emulateBytePages(board) {
|
|
359
222
|
return (board.startsWith('same54_xpro') ||
|
|
360
223
|
board.startsWith('netway') ||
|
|
@@ -364,11 +227,20 @@ function extraAppConfs(extraConfs, config) {
|
|
|
364
227
|
const key = process.env['ALTRONIX_RELEASE_KEY'];
|
|
365
228
|
if (!key)
|
|
366
229
|
throw new Error('missing ALTRONIX_RELEASE_KEY from environment');
|
|
230
|
+
// Custom TLVs for partition identification embedded in the MCUboot image header.
|
|
231
|
+
// - 0x8001: partition index (1 = slot1_partition, where firmware updates are written)
|
|
232
|
+
// - 0x8002: partition name string
|
|
233
|
+
// Note: INT32 values use little-endian hex (1 -> 0x01000000) per imgtool convention.
|
|
234
|
+
const imgtoolArgs = [
|
|
235
|
+
'--custom-tlv 0x8001 0x01000000',
|
|
236
|
+
'--custom-tlv 0x8002 slot1_partition'
|
|
237
|
+
].join(' ');
|
|
367
238
|
const extraConfsData = [
|
|
368
239
|
`CONFIG_MCUBOOT_SIGNATURE_KEY_FILE="${key}"`,
|
|
369
240
|
`CONFIG_BOOTLOADER_MCUBOOT=y`,
|
|
370
241
|
`CONFIG_ATX_UPDATE_ENABLE=y`,
|
|
371
|
-
`CONFIG_ATX_CONFIG="${config}"
|
|
242
|
+
`CONFIG_ATX_CONFIG="${config}"`,
|
|
243
|
+
`CONFIG_MCUBOOT_EXTRA_IMGTOOL_ARGS="${imgtoolArgs}"`
|
|
372
244
|
].join('\r\n');
|
|
373
245
|
return from(fs.promises.writeFile(extraConfs, extraConfsData));
|
|
374
246
|
}
|
|
@@ -391,9 +263,6 @@ function copy() {
|
|
|
391
263
|
return from(promise);
|
|
392
264
|
}));
|
|
393
265
|
}
|
|
394
|
-
function concatFiles(src, dest) {
|
|
395
|
-
return from(src).pipe(concatMap((src) => from(fs.promises.readFile(src, 'ascii'))), toArray(), mergeMap((arr) => from(fs.promises.writeFile(dest, arr.join('\r\n')))));
|
|
396
|
-
}
|
|
397
266
|
function mkdir() {
|
|
398
267
|
return (obs$) => obs$.pipe(mergeMap((dir) => new Observable((subscriber) => {
|
|
399
268
|
fs.promises
|
|
@@ -442,6 +311,15 @@ function clean(force) {
|
|
|
442
311
|
function tovoid() {
|
|
443
312
|
return (obs$) => obs$.pipe(map(() => void 0));
|
|
444
313
|
}
|
|
314
|
+
function filterExists(getPath) {
|
|
315
|
+
return (obs$) => obs$.pipe(mergeMap((item) => from(stat(getPath(item))).pipe(mergeMap((s) => (s ? of(item) : EMPTY)))));
|
|
316
|
+
}
|
|
317
|
+
function concatFiles(opts) {
|
|
318
|
+
const { inputs, output } = opts;
|
|
319
|
+
return from(Promise.all(inputs.map((f) => fs.promises.readFile(f)))
|
|
320
|
+
.then((buffers) => Buffer.concat(buffers))
|
|
321
|
+
.then((data) => fs.promises.writeFile(output, data)));
|
|
322
|
+
}
|
|
445
323
|
export async function build() {
|
|
446
324
|
const config = this.opts()['config'] || path.resolve('./', 'atx.json');
|
|
447
325
|
const verbose = this.optsWithGlobals()['verbose'];
|
|
@@ -450,7 +328,7 @@ export async function build() {
|
|
|
450
328
|
const mConfig = new RegExp(this.opts()['matchesConfig'] || '(.*)');
|
|
451
329
|
const cwd = path.resolve(path.dirname(config));
|
|
452
330
|
const data = await fs.promises.readFile(config, 'ascii');
|
|
453
|
-
const atx =
|
|
331
|
+
const atx = parseJsonc(data);
|
|
454
332
|
const extraAppConfFile = 'application.conf';
|
|
455
333
|
const extraBootConfFile = 'bootloader.conf';
|
|
456
334
|
if (!validate(atx))
|
|
@@ -477,32 +355,72 @@ export async function build() {
|
|
|
477
355
|
return keys(board.images).map((config) => westOptionsNormalize(board, config, app, cwd, verbose));
|
|
478
356
|
});
|
|
479
357
|
});
|
|
480
|
-
const wasm = keys(atx.wasm).map((w) => seedleOptionsNormalize(w, cwd, verbose));
|
|
481
358
|
let concurrent = this.opts()['concurrent']
|
|
482
359
|
? parseInt(this.opts()['concurrent'])
|
|
483
360
|
: Infinity;
|
|
484
361
|
let buildApps = this.opts()['application'];
|
|
485
362
|
let buildBoot = this.opts()['bootloader'];
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
buildApps = buildBoot = buildWasm = true;
|
|
363
|
+
if (!(buildApps || buildBoot)) {
|
|
364
|
+
buildApps = buildBoot = true;
|
|
489
365
|
}
|
|
490
366
|
const apps$ = buildApps ? from(await Promise.all(apps)) : EMPTY;
|
|
491
367
|
const boot$ = buildBoot ? from(await Promise.all(bootloaders)) : EMPTY;
|
|
492
|
-
const wasm$ = buildWasm ? from(await Promise.all(wasm)) : EMPTY;
|
|
493
368
|
// Handle all the preliminary stuff before building
|
|
494
|
-
const ready$ = concat(merge(apps$.pipe(mergeMap(({ installDir: s, binaryDir: b }) => from([s, b]))), boot$.pipe(mergeMap(({ installDir: s, binaryDir: b }) => from([s, b])))
|
|
495
|
-
clean(this.opts()['yes']), mkdir()), apps$.pipe(mergeMap(({ binaryDir, config }) => extraAppConfs(path.join(binaryDir, extraAppConfFile), config))), boot$.pipe(mergeMap(({ board, binaryDir }) => extraBootConfs(board, path.join(binaryDir, extraBootConfFile))))
|
|
369
|
+
const ready$ = concat(merge(apps$.pipe(mergeMap(({ installDir: s, binaryDir: b }) => from([s, b]))), boot$.pipe(mergeMap(({ installDir: s, binaryDir: b }) => from([s, b])))).pipe(toArray(), mergeMap((arr) => from([...new Set(arr)])), // dedupe
|
|
370
|
+
clean(this.opts()['yes']), mkdir()), apps$.pipe(mergeMap(({ binaryDir, config }) => extraAppConfs(path.join(binaryDir, extraAppConfFile), config))), boot$.pipe(mergeMap(({ board, binaryDir }) => extraBootConfs(board, path.join(binaryDir, extraBootConfFile))))).pipe(last(), share());
|
|
496
371
|
// Get an array of every build item.
|
|
497
|
-
const items$ = merge(apps$.pipe(map(westItem)), boot$.pipe(map(westItem))
|
|
498
|
-
// Run build commands for all bootloaders
|
|
499
|
-
const build$ = merge(apps$.pipe(map(west)), boot$.pipe(map(west))
|
|
372
|
+
const items$ = merge(apps$.pipe(map(westItem)), boot$.pipe(map(westItem))).pipe(toArray());
|
|
373
|
+
// Run build commands for all bootloaders and applications concurrently
|
|
374
|
+
const build$ = merge(apps$.pipe(map(west)), boot$.pipe(map(west))).pipe(mergeMap((obs) => obs, concurrent), share());
|
|
375
|
+
// Create update.bin by concatenating signed binaries in the build dir
|
|
376
|
+
const update$ = apps$.pipe(mergeMap(({ binaryDir }) => {
|
|
377
|
+
const zephyrDir = path.join(binaryDir, 'zephyr');
|
|
378
|
+
const required = path.join(zephyrDir, 'zephyr.signed.bin');
|
|
379
|
+
const optional = [
|
|
380
|
+
path.join(zephyrDir, 'www.signed.bin'),
|
|
381
|
+
path.join(zephyrDir, 'packedfs.signed.bin')
|
|
382
|
+
];
|
|
383
|
+
return from(optional).pipe(filterExists((f) => f), toArray(), mergeMap((extras) => concatFiles({
|
|
384
|
+
inputs: [required, ...extras],
|
|
385
|
+
output: path.join(zephyrDir, 'update.bin')
|
|
386
|
+
})));
|
|
387
|
+
}), tovoid());
|
|
500
388
|
// Install everything into installDir
|
|
501
|
-
const install$ = merge(apps$.pipe(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
389
|
+
const install$ = merge(apps$.pipe(mergeMap(({ name, config, version: ver, board, binaryDir, installDir: d }) => {
|
|
390
|
+
const prefix = `${board}-${name}-${config}-${ver}`;
|
|
391
|
+
const zephyrDir = path.join(binaryDir, 'zephyr');
|
|
392
|
+
// Required files (error if missing)
|
|
393
|
+
const required = [
|
|
394
|
+
{
|
|
395
|
+
src: path.join(zephyrDir, 'zephyr.signed.bin'),
|
|
396
|
+
dst: path.join(d, `${prefix}.signed.bin`)
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
src: path.join(zephyrDir, 'update.bin'),
|
|
400
|
+
dst: path.join(d, `${prefix}.update.bin`)
|
|
401
|
+
}
|
|
402
|
+
];
|
|
403
|
+
// Optional files (skip if missing)
|
|
404
|
+
const optional = [
|
|
405
|
+
{
|
|
406
|
+
src: path.join(zephyrDir, 'www.bin'),
|
|
407
|
+
dst: path.join(d, `${prefix}.www.bin`)
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
src: path.join(zephyrDir, 'www.signed.bin'),
|
|
411
|
+
dst: path.join(d, `${prefix}.www.signed.bin`)
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
src: path.join(zephyrDir, 'packedfs.bin'),
|
|
415
|
+
dst: path.join(d, `${prefix}.packedfs.bin`)
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
src: path.join(zephyrDir, 'packedfs.signed.bin'),
|
|
419
|
+
dst: path.join(d, `${prefix}.packedfs.signed.bin`)
|
|
420
|
+
}
|
|
421
|
+
];
|
|
422
|
+
const optional$ = from(optional).pipe(filterExists((opt) => opt.src));
|
|
423
|
+
return merge(from(required), optional$);
|
|
506
424
|
})), boot$.pipe(map(({ name, config, board, binaryDir, installDir: d }) => {
|
|
507
425
|
return {
|
|
508
426
|
src: path.join(binaryDir, 'zephyr', 'zephyr.bin'),
|
|
@@ -515,7 +433,7 @@ export async function build() {
|
|
|
515
433
|
const renderer = render(React.createElement(Ui, { items: items, "progress$": build$, onComplete: () => renderer.unmount() }));
|
|
516
434
|
await renderer.waitUntilExit();
|
|
517
435
|
renderer.cleanup();
|
|
518
|
-
return lastValueFrom(install$);
|
|
436
|
+
return lastValueFrom(concat(update$, install$));
|
|
519
437
|
}
|
|
520
438
|
catch (e) {
|
|
521
439
|
if (!(e instanceof EmptyError))
|
package/dist/build.ui.d.ts
CHANGED
|
@@ -2,12 +2,12 @@ import React from 'react';
|
|
|
2
2
|
import { Observable } from 'rxjs';
|
|
3
3
|
export declare class BuildError extends Error implements BuildItem {
|
|
4
4
|
item: string;
|
|
5
|
-
kind: 'west'
|
|
6
|
-
constructor(item: string, kind: 'west'
|
|
5
|
+
kind: 'west';
|
|
6
|
+
constructor(item: string, kind: 'west', message: string);
|
|
7
7
|
}
|
|
8
8
|
export interface BuildItem {
|
|
9
9
|
item: string;
|
|
10
|
-
kind: 'west'
|
|
10
|
+
kind: 'west';
|
|
11
11
|
}
|
|
12
12
|
export interface BuildProgress<K extends string = string> {
|
|
13
13
|
item: K;
|
package/dist/build.ui.js
CHANGED
|
@@ -36,8 +36,8 @@ export default function ({ items, progress$, onComplete }) {
|
|
|
36
36
|
React.createElement(Box, { width: col - width - 4 - 2 - 1 - 1 - 1 },
|
|
37
37
|
React.createElement(Text, { wrap: "truncate", dimColor: !progressComplete(progress[item]), color: progressColor(progress[item]) }, progress[item])))))));
|
|
38
38
|
}
|
|
39
|
-
function buildColor(
|
|
40
|
-
return
|
|
39
|
+
function buildColor(_kind) {
|
|
40
|
+
return 'blue';
|
|
41
41
|
}
|
|
42
42
|
function progressComplete(progress) {
|
|
43
43
|
return progress === 'OK!';
|
package/dist/cloud.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cborDecodeNum, NetCloud } from '@altronix/zdk';
|
|
1
|
+
import { cborDecodeNum, NetCloud } from '@altronix/zdk/common';
|
|
2
2
|
import { Linq } from '@altronix/device';
|
|
3
3
|
import { concat, EmptyError, finalize, lastValueFrom, map, reduce, switchMap } from 'rxjs';
|
|
4
4
|
import logger from './logger.js';
|
package/dist/index.js
CHANGED
|
@@ -3,16 +3,8 @@ import { program } from 'commander';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import { build } from './build.js';
|
|
6
|
-
import { getAbout, getSite, setSite } from './about.js';
|
|
7
|
-
import { reboot, save, saveAndReboot } from './exe.js';
|
|
8
|
-
import { getNet, setNet, setDhcp } from './net.js';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
|
-
import { getCloud, setCloud } from './cloud.js';
|
|
11
|
-
import { stress } from './stress.js';
|
|
12
6
|
import { update } from './update.js';
|
|
13
|
-
import {
|
|
14
|
-
import { getDemo, getHello } from './sample.js';
|
|
15
|
-
import { concurrent } from './concurrent.js';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
16
8
|
// https://stackoverflow.com/questions/8817423/why-is-dirname-not-defined-in-node-repl
|
|
17
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
10
|
// Parse package.json to get version
|
|
@@ -25,17 +17,6 @@ program
|
|
|
25
17
|
.version(ver)
|
|
26
18
|
.option('-Q, --quiet', 'no logs')
|
|
27
19
|
.option('-VV, --verbose', 'extra logging');
|
|
28
|
-
// Scan command
|
|
29
|
-
/*
|
|
30
|
-
const ignore = 'node_modules;target;build;.git';
|
|
31
|
-
program
|
|
32
|
-
.command('scan')
|
|
33
|
-
.description('scan for *.cddl files')
|
|
34
|
-
.option('-p, --path <PATH>', 'root directory to start scan', cwd)
|
|
35
|
-
.option('-m, --matches <REGEX>', 'match expression', '.*cddl$')
|
|
36
|
-
.option('-i, --ignores <REGEX>', 'ignore directories', ignore)
|
|
37
|
-
.action(seedle.scan);
|
|
38
|
-
*/
|
|
39
20
|
// Build command
|
|
40
21
|
// TODO - detect if west is available and fail early
|
|
41
22
|
program
|
|
@@ -45,101 +26,16 @@ program
|
|
|
45
26
|
.option('-C, --concurrent <NUMBER>', 'how many builds to run concurrently')
|
|
46
27
|
.option('-A, --application', 'build applications')
|
|
47
28
|
.option('-B, --bootloader', 'build bootloaders')
|
|
48
|
-
.option('-W, --wasm', 'build wasm')
|
|
49
29
|
.option('--matches-board [regex]', 'board regex match expr')
|
|
50
30
|
.option('--matches-application [regex]', 'application regex match expr')
|
|
51
31
|
.option('--matches-config [regex]', 'config regex match expr')
|
|
52
32
|
.option('-y, --yes', 'answer yes automatically')
|
|
53
33
|
.action(build);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.description('manage zephyr device settings')
|
|
57
|
-
.option('-p, --port <PORT>', 'listen port for incoming device connections')
|
|
58
|
-
.option('-v, --verbose', 'print extra debug informating during request')
|
|
59
|
-
.option('-f, --first', 'perform request on first device we see (any device)');
|
|
60
|
-
device
|
|
61
|
-
.command('get-about')
|
|
62
|
-
.description('get about data on the device')
|
|
63
|
-
.action(getAbout);
|
|
64
|
-
device
|
|
65
|
-
.command('get-site')
|
|
66
|
-
.description('get the site ID of the device')
|
|
67
|
-
.action(getSite);
|
|
68
|
-
device
|
|
69
|
-
.command('set-site')
|
|
70
|
-
.description('set the site ID of the device')
|
|
71
|
-
.argument('<site>', 'new site id')
|
|
72
|
-
.action(setSite);
|
|
73
|
-
device
|
|
74
|
-
.command('save')
|
|
75
|
-
.description('save data to persistant storage. (does not reboot)')
|
|
76
|
-
.action(save);
|
|
77
|
-
device
|
|
78
|
-
.command('reboot')
|
|
79
|
-
.description('reboot the device. (does not save)')
|
|
80
|
-
.action(reboot);
|
|
81
|
-
device
|
|
82
|
-
.command('save-reboot')
|
|
83
|
-
.description('save data to persistant storage and reboot the device')
|
|
84
|
-
.action(saveAndReboot);
|
|
85
|
-
device
|
|
86
|
-
.command('get-net')
|
|
87
|
-
.description('get network configuration from the device')
|
|
88
|
-
.action(getNet);
|
|
89
|
-
device
|
|
90
|
-
.command('set-net')
|
|
91
|
-
.description('set network interface into static ip mode')
|
|
92
|
-
.argument('<ip>', 'the new IP address')
|
|
93
|
-
.argument('<sn>', 'the new SUBNET mask')
|
|
94
|
-
.argument('<gw>', 'the new GATEWAY address')
|
|
95
|
-
.action(setNet);
|
|
96
|
-
device
|
|
97
|
-
.command('set-dhcp')
|
|
98
|
-
.description('set network interface into DHCP mode')
|
|
99
|
-
.action(setDhcp);
|
|
100
|
-
device
|
|
101
|
-
.command('get-cloud')
|
|
102
|
-
.description('get cloud endpoint on the device')
|
|
103
|
-
.action(getCloud);
|
|
104
|
-
device
|
|
105
|
-
.command('set-cloud')
|
|
106
|
-
.description('set cloud endpoint on the device')
|
|
107
|
-
.argument('<endpoint>', 'cloud service location')
|
|
108
|
-
.action(setCloud);
|
|
109
|
-
device
|
|
110
|
-
.command('stress')
|
|
111
|
-
.description('run a stress test on a device')
|
|
112
|
-
.argument('<count>', 'how many requests to make')
|
|
113
|
-
.action(stress);
|
|
114
|
-
device
|
|
34
|
+
// Update command
|
|
35
|
+
program
|
|
115
36
|
.command('update')
|
|
116
|
-
.description('
|
|
117
|
-
.argument('<
|
|
37
|
+
.description('inspect or flash firmware update')
|
|
38
|
+
.argument('<url>', 'target device URL (e.g., https://192.168.1.1:8008)')
|
|
39
|
+
.argument('<file>', 'path to update binary')
|
|
118
40
|
.action(update);
|
|
119
|
-
device
|
|
120
|
-
.command('listen')
|
|
121
|
-
.description('listen for alerts and heartbeats')
|
|
122
|
-
.argument('<duration>', 'how long to listen')
|
|
123
|
-
.action(listen);
|
|
124
|
-
device
|
|
125
|
-
.command('concurrent')
|
|
126
|
-
.description('make a number of requests concurrently')
|
|
127
|
-
.argument('<count>', 'how many requests to make concurrently')
|
|
128
|
-
.action(concurrent);
|
|
129
|
-
const sample = program
|
|
130
|
-
.command('sample')
|
|
131
|
-
.description('manage zephyr device settings')
|
|
132
|
-
.option('-p, --port <PORT>', 'listen port for incoming device connections')
|
|
133
|
-
.option('-v, --verbose', 'print extra debug informating during request')
|
|
134
|
-
.option('-f, --first', 'perform request on first device we see (any device)');
|
|
135
|
-
sample
|
|
136
|
-
.command('get-demo')
|
|
137
|
-
.description('read the demo data from the sample')
|
|
138
|
-
.action(getDemo);
|
|
139
|
-
sample
|
|
140
|
-
.command('get-hello')
|
|
141
|
-
.description('read the hello data from the sample')
|
|
142
|
-
.action(getHello);
|
|
143
|
-
// Load plugins
|
|
144
|
-
// (await plugins()).forEach(({ plugin: _, path: __, description: ___ }) => {});
|
|
145
41
|
program.parseAsync().catch((e) => console.error(e));
|
package/dist/net.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { concat, EmptyError, finalize, firstValueFrom, lastValueFrom, map, switchMap } from 'rxjs';
|
|
2
2
|
import { Linq } from '@altronix/device';
|
|
3
|
-
import { NetIp } from '@altronix/zdk';
|
|
3
|
+
import { NetIp } from '@altronix/zdk/common';
|
|
4
4
|
import logger from './logger.js';
|
|
5
5
|
import { select, first } from './select.js';
|
|
6
6
|
export async function getNet() {
|
package/dist/select.ui.d.ts
CHANGED
package/dist/stress.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { METH_CONSTANTS } from '@altronix/zdk';
|
|
1
|
+
import { METH_CONSTANTS } from '@altronix/zdk/common';
|
|
2
2
|
import { Linq } from '@altronix/device';
|
|
3
3
|
import { EmptyError, finalize, lastValueFrom, switchMap, pipe, repeat } from 'rxjs';
|
|
4
4
|
import progress from './progress.js';
|
package/dist/update.d.ts
CHANGED
|
@@ -1,2 +1,42 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
|
|
2
|
+
import { UpdateProgress } from './update.ui.js';
|
|
3
|
+
/**
|
|
4
|
+
* Start flash operation - erases the partition
|
|
5
|
+
* GET /api/flash/start?partition=<name>
|
|
6
|
+
*/
|
|
7
|
+
export declare function flashStart(baseUrl: string, partition: string): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Transfer a single chunk
|
|
10
|
+
* PUT /api/flash/transfer?partition=<name>
|
|
11
|
+
* Body: CBOR-encoded [offset, data]
|
|
12
|
+
*/
|
|
13
|
+
export declare function flashTransfer(baseUrl: string, partition: string, offset: number, data: Uint8Array): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Complete the flash operation - triggers reboot
|
|
16
|
+
* GET /api/flash/transfer_complete?partition=<name>
|
|
17
|
+
*/
|
|
18
|
+
export declare function flashTransferComplete(baseUrl: string, partition: string): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Split image into 512-byte chunks, padding last chunk with 0xFF
|
|
21
|
+
*/
|
|
22
|
+
export declare function chunkImage(image: Uint8Array): Array<{
|
|
23
|
+
offset: number;
|
|
24
|
+
data: Uint8Array;
|
|
25
|
+
}>;
|
|
26
|
+
export type OnProgress = (progress: UpdateProgress) => void;
|
|
27
|
+
export interface UploadImage {
|
|
28
|
+
partition: string;
|
|
29
|
+
image: Uint8Array;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Upload firmware to device
|
|
33
|
+
*
|
|
34
|
+
* For each partition:
|
|
35
|
+
* 1. Erase partition (GET /api/flash/start)
|
|
36
|
+
* 2. Transfer chunks (PUT /api/flash/transfer with CBOR [offset, data])
|
|
37
|
+
*
|
|
38
|
+
* After all partitions:
|
|
39
|
+
* 3. Finalize update (GET /api/flash/transfer_complete)
|
|
40
|
+
*/
|
|
41
|
+
export declare function uploadFirmware(baseUrl: string, images: UploadImage[], onProgress: OnProgress): Promise<void>;
|
|
42
|
+
export declare function update(this: Command, url: string, file: string): Promise<void>;
|
package/dist/update.js
CHANGED
|
@@ -1,59 +1,161 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import { parseMcubootImages, AltronixImage } from '@altronix/webtobin';
|
|
5
|
+
import * as CBOR from 'cbor2';
|
|
6
|
+
import { render } from 'ink';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import UpdateUi from './update.ui.js';
|
|
9
|
+
const CHUNK_SIZE = 512;
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// OTA API
|
|
12
|
+
// =============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Make an HTTP/HTTPS request to the OTA API
|
|
15
|
+
*/
|
|
16
|
+
function otaRequest(method, baseUrl, path, partition, body) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const url = new URL(path, baseUrl);
|
|
19
|
+
url.searchParams.set('partition', partition);
|
|
20
|
+
const isHttps = url.protocol === 'https:';
|
|
21
|
+
const transport = isHttps ? https : http;
|
|
22
|
+
const req = transport.request(url, {
|
|
23
|
+
method,
|
|
24
|
+
headers: body
|
|
25
|
+
? {
|
|
26
|
+
'Content-Type': 'application/cbor',
|
|
27
|
+
'Content-Length': body.length,
|
|
28
|
+
}
|
|
29
|
+
: undefined,
|
|
30
|
+
rejectUnauthorized: false,
|
|
31
|
+
}, (res) => {
|
|
32
|
+
res.resume();
|
|
33
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
34
|
+
resolve();
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
req.on('error', reject);
|
|
41
|
+
if (body)
|
|
42
|
+
req.write(body);
|
|
43
|
+
req.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Start flash operation - erases the partition
|
|
48
|
+
* GET /api/flash/start?partition=<name>
|
|
49
|
+
*/
|
|
50
|
+
export function flashStart(baseUrl, partition) {
|
|
51
|
+
return otaRequest('GET', baseUrl, '/api/flash/start', partition);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Transfer a single chunk
|
|
55
|
+
* PUT /api/flash/transfer?partition=<name>
|
|
56
|
+
* Body: CBOR-encoded [offset, data]
|
|
57
|
+
*/
|
|
58
|
+
export function flashTransfer(baseUrl, partition, offset, data) {
|
|
59
|
+
const payload = new Uint8Array(CBOR.encode([offset, data]));
|
|
60
|
+
return otaRequest('PUT', baseUrl, '/api/flash/transfer', partition, payload);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Complete the flash operation - triggers reboot
|
|
64
|
+
* GET /api/flash/transfer_complete?partition=<name>
|
|
65
|
+
*/
|
|
66
|
+
export function flashTransferComplete(baseUrl, partition) {
|
|
67
|
+
return otaRequest('GET', baseUrl, '/api/flash/transfer_complete', partition);
|
|
68
|
+
}
|
|
69
|
+
// =============================================================================
|
|
70
|
+
// Chunking
|
|
71
|
+
// =============================================================================
|
|
72
|
+
/**
|
|
73
|
+
* Split image into 512-byte chunks, padding last chunk with 0xFF
|
|
74
|
+
*/
|
|
75
|
+
export function chunkImage(image) {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
for (let offset = 0; offset < image.length; offset += CHUNK_SIZE) {
|
|
78
|
+
const slice = image.subarray(offset, offset + CHUNK_SIZE);
|
|
79
|
+
const data = new Uint8Array(CHUNK_SIZE);
|
|
80
|
+
data.fill(0xff);
|
|
81
|
+
data.set(slice);
|
|
82
|
+
chunks.push({ offset, data });
|
|
16
83
|
}
|
|
17
|
-
|
|
18
|
-
|
|
84
|
+
return chunks;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Upload firmware to device
|
|
88
|
+
*
|
|
89
|
+
* For each partition:
|
|
90
|
+
* 1. Erase partition (GET /api/flash/start)
|
|
91
|
+
* 2. Transfer chunks (PUT /api/flash/transfer with CBOR [offset, data])
|
|
92
|
+
*
|
|
93
|
+
* After all partitions:
|
|
94
|
+
* 3. Finalize update (GET /api/flash/transfer_complete)
|
|
95
|
+
*/
|
|
96
|
+
export async function uploadFirmware(baseUrl, images, onProgress) {
|
|
97
|
+
for (const { partition, image } of images) {
|
|
98
|
+
// Erase
|
|
99
|
+
onProgress({ partition, phase: 'erase' });
|
|
100
|
+
await flashStart(baseUrl, partition);
|
|
101
|
+
// Transfer chunks
|
|
102
|
+
const chunks = chunkImage(image);
|
|
103
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
104
|
+
const { offset, data } = chunks[i];
|
|
105
|
+
await flashTransfer(baseUrl, partition, offset, data);
|
|
106
|
+
onProgress({
|
|
107
|
+
partition,
|
|
108
|
+
phase: 'transfer',
|
|
109
|
+
current: offset + CHUNK_SIZE,
|
|
110
|
+
total: image.length
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Mark complete
|
|
114
|
+
onProgress({ partition, phase: 'complete' });
|
|
19
115
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
slicer(n - 1);
|
|
24
|
-
let last = new Uint8Array(size);
|
|
25
|
-
last.set(data.slice(end));
|
|
26
|
-
arr.push(last);
|
|
116
|
+
// Finalize (once, after all partitions)
|
|
117
|
+
if (images.length > 0) {
|
|
118
|
+
await flashTransferComplete(baseUrl, images[0].partition);
|
|
27
119
|
}
|
|
28
|
-
return arr;
|
|
29
|
-
}
|
|
30
|
-
function updateMap(data, idx) {
|
|
31
|
-
let update = new Update({ data, offset: idx * 512 });
|
|
32
|
-
// NOTE: Have to initialize the Uint8Array across binding this way
|
|
33
|
-
// https://gitlab.altronix.com/software-engineering/sdk/atx-zdk/-/issues/1
|
|
34
|
-
update.data = data;
|
|
35
|
-
return update;
|
|
36
120
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// CLI Command
|
|
123
|
+
// =============================================================================
|
|
124
|
+
export async function update(url, file) {
|
|
125
|
+
// Parse firmware file
|
|
126
|
+
const buffer = await fs.promises.readFile(file);
|
|
127
|
+
const bin = new Uint8Array(buffer);
|
|
128
|
+
const mcubootImages = parseMcubootImages(bin);
|
|
129
|
+
if (mcubootImages.length === 0) {
|
|
130
|
+
console.error('No valid MCUboot images found in file');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
// Extract partition data
|
|
134
|
+
const images = mcubootImages.map((img) => {
|
|
135
|
+
const altronix = AltronixImage.fromMcubootImage(img);
|
|
136
|
+
return {
|
|
137
|
+
partition: altronix.partitionName,
|
|
138
|
+
image: altronix.signedImage
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
// Build UI items
|
|
142
|
+
const items = mcubootImages.map((img) => {
|
|
143
|
+
const altronix = AltronixImage.fromMcubootImage(img);
|
|
144
|
+
const { major, minor, revision, build } = altronix.version;
|
|
145
|
+
return {
|
|
146
|
+
partition: altronix.partitionName,
|
|
147
|
+
version: `${major}.${minor}.${revision}+${build}`,
|
|
148
|
+
size: altronix.signedImage.length
|
|
149
|
+
};
|
|
58
150
|
});
|
|
151
|
+
// Render UI - it handles the upload internally
|
|
152
|
+
const renderer = render(React.createElement(UpdateUi, { baseUrl: url, items: items, images: images, onComplete: (error) => {
|
|
153
|
+
renderer.unmount();
|
|
154
|
+
if (error) {
|
|
155
|
+
console.error(`\nUpdate failed: ${error.message}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
console.log('\nUpdate complete. Device is rebooting...');
|
|
159
|
+
} }));
|
|
160
|
+
await renderer.waitUntilExit();
|
|
59
161
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { UploadImage } from './update.js';
|
|
3
|
+
/**
|
|
4
|
+
* Partition metadata for display
|
|
5
|
+
*/
|
|
6
|
+
export interface UpdateItem {
|
|
7
|
+
partition: string;
|
|
8
|
+
version: string;
|
|
9
|
+
size: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Progress event from upload
|
|
13
|
+
*/
|
|
14
|
+
export interface UpdateProgress {
|
|
15
|
+
partition: string;
|
|
16
|
+
phase: 'erase' | 'transfer' | 'complete';
|
|
17
|
+
current?: number;
|
|
18
|
+
total?: number;
|
|
19
|
+
}
|
|
20
|
+
interface UpdateUiProps {
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
items: UpdateItem[];
|
|
23
|
+
images: UploadImage[];
|
|
24
|
+
onComplete: (error?: Error) => void;
|
|
25
|
+
}
|
|
26
|
+
export default function UpdateUi({ baseUrl, items, images, onComplete }: UpdateUiProps): React.JSX.Element;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import useStdoutDimensions from './useStdoutDimensions.js';
|
|
4
|
+
import { uploadFirmware } from './update.js';
|
|
5
|
+
export default function UpdateUi({ baseUrl, items, images, onComplete }) {
|
|
6
|
+
const [col] = useStdoutDimensions();
|
|
7
|
+
const [state, setState] = useState(() => initState(items));
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
uploadFirmware(baseUrl, images, (progress) => {
|
|
10
|
+
setState((prev) => updateState(prev, progress));
|
|
11
|
+
})
|
|
12
|
+
.then(() => onComplete())
|
|
13
|
+
.catch((e) => onComplete(e));
|
|
14
|
+
}, []);
|
|
15
|
+
const maxNameLen = Math.max(...items.map((i) => i.partition.length));
|
|
16
|
+
const barWidth = Math.min(col - maxNameLen - 25, 40);
|
|
17
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
18
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
19
|
+
React.createElement(Text, { bold: true }, "OTA Update")),
|
|
20
|
+
items.map((item) => {
|
|
21
|
+
const s = state[item.partition];
|
|
22
|
+
const percent = s && s.total > 0 ? Math.floor((100 * s.current) / s.total) : 0;
|
|
23
|
+
const filled = s && s.total > 0 ? Math.floor((barWidth * s.current) / s.total) : 0;
|
|
24
|
+
const empty = barWidth - filled;
|
|
25
|
+
return (React.createElement(Box, { key: item.partition },
|
|
26
|
+
React.createElement(Box, { width: maxNameLen + 1 },
|
|
27
|
+
React.createElement(Text, { bold: true }, item.partition.padEnd(maxNameLen))),
|
|
28
|
+
React.createElement(Box, { marginLeft: 1 },
|
|
29
|
+
React.createElement(Text, null, "["),
|
|
30
|
+
React.createElement(Text, { color: "green" }, '#'.repeat(filled)),
|
|
31
|
+
React.createElement(Text, { color: "gray" }, '-'.repeat(empty)),
|
|
32
|
+
React.createElement(Text, null, "]")),
|
|
33
|
+
React.createElement(Box, { marginLeft: 1, width: 5 },
|
|
34
|
+
React.createElement(Text, null,
|
|
35
|
+
percent.toString().padStart(3),
|
|
36
|
+
"%")),
|
|
37
|
+
React.createElement(Box, { marginLeft: 1 },
|
|
38
|
+
React.createElement(Text, { color: phaseColor(s?.phase) }, phaseText(s?.phase)))));
|
|
39
|
+
}),
|
|
40
|
+
React.createElement(Box, { marginTop: 1 },
|
|
41
|
+
React.createElement(Text, { dimColor: true },
|
|
42
|
+
formatSize(items.reduce((acc, i) => acc + i.size, 0)),
|
|
43
|
+
" total"))));
|
|
44
|
+
}
|
|
45
|
+
function initState(items) {
|
|
46
|
+
return items.reduce((acc, item) => ({
|
|
47
|
+
...acc,
|
|
48
|
+
[item.partition]: {
|
|
49
|
+
phase: 'pending',
|
|
50
|
+
current: 0,
|
|
51
|
+
total: item.size
|
|
52
|
+
}
|
|
53
|
+
}), {});
|
|
54
|
+
}
|
|
55
|
+
function updateState(prev, progress) {
|
|
56
|
+
const current = prev[progress.partition];
|
|
57
|
+
if (!current)
|
|
58
|
+
return prev;
|
|
59
|
+
switch (progress.phase) {
|
|
60
|
+
case 'erase':
|
|
61
|
+
return {
|
|
62
|
+
...prev,
|
|
63
|
+
[progress.partition]: { ...current, phase: 'erase' }
|
|
64
|
+
};
|
|
65
|
+
case 'transfer':
|
|
66
|
+
return {
|
|
67
|
+
...prev,
|
|
68
|
+
[progress.partition]: {
|
|
69
|
+
...current,
|
|
70
|
+
phase: 'transfer',
|
|
71
|
+
current: progress.current ?? current.current
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
case 'complete':
|
|
75
|
+
return {
|
|
76
|
+
...prev,
|
|
77
|
+
[progress.partition]: {
|
|
78
|
+
...current,
|
|
79
|
+
phase: 'done',
|
|
80
|
+
current: current.total
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
default:
|
|
84
|
+
return prev;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function phaseColor(phase) {
|
|
88
|
+
switch (phase) {
|
|
89
|
+
case 'erase':
|
|
90
|
+
return 'yellow';
|
|
91
|
+
case 'transfer':
|
|
92
|
+
return 'cyan';
|
|
93
|
+
case 'done':
|
|
94
|
+
return 'green';
|
|
95
|
+
default:
|
|
96
|
+
return 'gray';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function phaseText(phase) {
|
|
100
|
+
switch (phase) {
|
|
101
|
+
case 'pending':
|
|
102
|
+
return 'waiting';
|
|
103
|
+
case 'erase':
|
|
104
|
+
return 'erasing';
|
|
105
|
+
case 'transfer':
|
|
106
|
+
return 'uploading';
|
|
107
|
+
case 'done':
|
|
108
|
+
return 'OK';
|
|
109
|
+
default:
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function formatSize(bytes) {
|
|
114
|
+
if (bytes < 1024)
|
|
115
|
+
return `${bytes} B`;
|
|
116
|
+
if (bytes < 1024 * 1024)
|
|
117
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
118
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@altronix/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,35 +13,35 @@
|
|
|
13
13
|
"dist"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@
|
|
17
|
-
"
|
|
16
|
+
"@altronix/webtobin": "^0.8.0",
|
|
17
|
+
"@inquirer/confirm": "^3.2.0",
|
|
18
|
+
"ajv": "^8.17.1",
|
|
19
|
+
"cbor2": "^2.2.1",
|
|
18
20
|
"commander": "^10.0.1",
|
|
19
|
-
"dotenv": "^16.
|
|
20
|
-
"ink": "^4.1
|
|
21
|
+
"dotenv": "^16.6.1",
|
|
22
|
+
"ink": "^4.4.1",
|
|
21
23
|
"jsonc-parser": "^3.3.1",
|
|
22
|
-
"react": "^18.
|
|
23
|
-
"rxjs": "^7.8.
|
|
24
|
-
"winston": "^3.
|
|
25
|
-
"@altronix/device": "0.9.0",
|
|
26
|
-
"@altronix/zdk": "0.9.0"
|
|
24
|
+
"react": "^18.3.1",
|
|
25
|
+
"rxjs": "^7.8.2",
|
|
26
|
+
"winston": "^3.19.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@sindresorhus/tsconfig": "^3.0.1",
|
|
30
|
-
"@types/react": "^18.
|
|
30
|
+
"@types/react": "^18.3.27",
|
|
31
31
|
"@typescript-eslint/eslint-plugin": "~5.44.0",
|
|
32
32
|
"@typescript-eslint/parser": "~5.44.0",
|
|
33
|
-
"ava": "^5.
|
|
34
|
-
"chalk": "^5.2
|
|
33
|
+
"ava": "^5.3.1",
|
|
34
|
+
"chalk": "^5.6.2",
|
|
35
35
|
"eslint": "~8.28.0",
|
|
36
36
|
"eslint-config-prettier": "~8.5.0",
|
|
37
37
|
"eslint-config-xo-react": "^0.27.0",
|
|
38
38
|
"eslint-plugin-jest": "~27.1.7",
|
|
39
|
-
"eslint-plugin-react": "^7.
|
|
40
|
-
"eslint-plugin-react-hooks": "^4.6.
|
|
39
|
+
"eslint-plugin-react": "^7.37.5",
|
|
40
|
+
"eslint-plugin-react-hooks": "^4.6.2",
|
|
41
41
|
"ink-testing-library": "^3.0.0",
|
|
42
|
-
"prettier": "^2.8.
|
|
43
|
-
"ts-node": "^10.9.
|
|
44
|
-
"typescript": "^5.
|
|
42
|
+
"prettier": "^2.8.8",
|
|
43
|
+
"ts-node": "^10.9.2",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
45
|
"xo": "^0.53.1"
|
|
46
46
|
},
|
|
47
47
|
"ava": {
|