@geastack/cli 0.1.53 → 0.1.55
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/README.md +17 -4
- package/docs/ESP32-WAVESHARE-AMOLED-QUICKSTART.md +2 -2
- package/docs/NPX-COMMANDS.md +10 -4
- package/docs/SETUP.md +99 -21
- package/docs/SPEC.md +6 -3
- package/package.json +1 -1
- package/src/boards/config.mjs +90 -8
- package/src/boards/resolve.mjs +5 -3
- package/src/boards/usb.mjs +41 -0
- package/src/chips.mjs +1 -1
- package/src/commands/apps.mjs +1 -33
- package/src/commands/board.mjs +15 -2
- package/src/commands/boards.mjs +334 -0
- package/src/commands/doctor.mjs +4 -4
- package/src/context.mjs +5 -2
- package/src/esp32/idf-version.mjs +89 -0
- package/src/gea.mjs +9 -4
- package/src/serial-devices.mjs +32 -20
- package/src/setup-wizard.mjs +140 -52
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ flowchart TD
|
|
|
60
60
|
flowchart TD
|
|
61
61
|
setup["npx gea setup"] --> mode{"Mode?"}
|
|
62
62
|
|
|
63
|
-
mode -->|"--esp-idf"| directIdf["Install or check ESP-IDF v6.0.
|
|
63
|
+
mode -->|"--esp-idf"| directIdf["Install or check ESP-IDF v6.0.2"]
|
|
64
64
|
mode -->|"--board alias"| directBoard["Run target setup for board alias"]
|
|
65
65
|
mode -->|"--target target-id"| directTarget["Run target setup directly"]
|
|
66
66
|
|
|
@@ -69,7 +69,7 @@ flowchart TD
|
|
|
69
69
|
interactive -->|"Known supported board"| knownBoard["Pick board"]
|
|
70
70
|
knownBoard --> alias["Set board alias"]
|
|
71
71
|
alias --> serial["Detect serial devices"]
|
|
72
|
-
serial --> saveSerial["
|
|
72
|
+
serial --> saveSerial["Detect the USB serial (registry + GEADEV ping)"]
|
|
73
73
|
saveSerial --> ota["Optional OTA host"]
|
|
74
74
|
ota --> reviewKnown["Review board setup"]
|
|
75
75
|
reviewKnown --> writeKnown["Write .gea/boards.json"]
|
|
@@ -83,7 +83,7 @@ flowchart TD
|
|
|
83
83
|
writeProfile --> writeCustomAlias["Write alias to .gea/boards.json"]
|
|
84
84
|
|
|
85
85
|
interactive -->|"npm dependencies only"| npmInstall["Run npm install when package.json exists"]
|
|
86
|
-
interactive -->|"ESP-IDF toolchain only"| idfOnly["Install or check ESP-IDF v6.0.
|
|
86
|
+
interactive -->|"ESP-IDF toolchain only"| idfOnly["Install or check ESP-IDF v6.0.2"]
|
|
87
87
|
|
|
88
88
|
writeKnown --> initialize["Initialize board target"]
|
|
89
89
|
writeCustomAlias --> initialize
|
|
@@ -100,6 +100,18 @@ host bindings, and the other native packages from npm. A project can use a
|
|
|
100
100
|
local CLI with `npx gea` or a global installation with `gea`; neither command
|
|
101
101
|
depends on a GeaStack source checkout.
|
|
102
102
|
|
|
103
|
+
Boards are managed without editing JSON by hand; aliases live in
|
|
104
|
+
`~/.geastack/boards.json` (this machine) and the project's `.gea/boards.json`
|
|
105
|
+
(overrides):
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
gea boards discover # which registered board is on which USB port, its app and IP
|
|
109
|
+
gea boards list
|
|
110
|
+
gea boards set amoled host 192.168.1.100
|
|
111
|
+
gea boards rename amoled desk-amoled
|
|
112
|
+
gea boards remove desk-amoled
|
|
113
|
+
```
|
|
114
|
+
|
|
103
115
|
Custom boards remain editable after setup:
|
|
104
116
|
|
|
105
117
|
```sh
|
|
@@ -169,4 +181,5 @@ First implementation is in place:
|
|
|
169
181
|
- `list` and `inspect` helpers;
|
|
170
182
|
- `create-geastack` with a bundled counter starter, a blank application, and a
|
|
171
183
|
GitHub-backed rich example flow for web, embedded, GeaOS, iOS, macOS, and Android apps,
|
|
172
|
-
all with `.gea/boards.json
|
|
184
|
+
all with `.gea/boards.json`, plus `gea boards` for machine-wide aliases in
|
|
185
|
+
`~/.geastack/boards.json`.
|
|
@@ -90,7 +90,7 @@ You do not need to run that before every command unless `gea doctor` says ESP-ID
|
|
|
90
90
|
|
|
91
91
|
If the wizard finds a connected serial device, pick it. If no board is plugged in yet, that is fine; you can provide the port later.
|
|
92
92
|
|
|
93
|
-
The wizard writes the board
|
|
93
|
+
The wizard writes the board alias into your app's `.gea/boards.json` (or `~/.geastack/boards.json` with `--global`, for every project on this machine) and initializes the selected board target.
|
|
94
94
|
|
|
95
95
|
## 4. Flash
|
|
96
96
|
|
|
@@ -134,4 +134,4 @@ Most first-run issues are one of:
|
|
|
134
134
|
- ESP-IDF is not activated in the current shell.
|
|
135
135
|
- The USB cable is power-only.
|
|
136
136
|
- The serial port needs to be passed with `--port`.
|
|
137
|
-
- The board alias does not match
|
|
137
|
+
- The board alias does not match a name in `npx gea boards list` (`~/.geastack/boards.json` plus the app's `.gea/boards.json`). `npx gea boards discover` shows which registered board each USB port is.
|
package/docs/NPX-COMMANDS.md
CHANGED
|
@@ -93,8 +93,11 @@ packages are scoped and configured for restricted npmjs publication.
|
|
|
93
93
|
- npm dependency check/install only;
|
|
94
94
|
- ESP-IDF toolchain check/install only.
|
|
95
95
|
|
|
96
|
-
Known-board setup
|
|
97
|
-
|
|
96
|
+
Known-board setup asks whether the board is plugged in, reads each USB
|
|
97
|
+
device's serial from the OS and pings it (so a board is picked by the app it
|
|
98
|
+
reports running, never by a `/dev` name), shows a review screen, writes a board alias into the active boards config
|
|
99
|
+
(`--global` for `~/.geastack/boards.json`, `--local` for the project's
|
|
100
|
+
`.gea/boards.json`; by default the project config when it exists), then
|
|
98
101
|
initializes the selected board target so the next command can be
|
|
99
102
|
`npx gea flash --board <alias> --monitor`. In generated apps, that config is:
|
|
100
103
|
|
|
@@ -138,5 +141,8 @@ ESP-IDF setup is available as:
|
|
|
138
141
|
npx gea setup --esp-idf
|
|
139
142
|
```
|
|
140
143
|
|
|
141
|
-
It installs or dry-runs ESP-IDF
|
|
142
|
-
toolchains.
|
|
144
|
+
It installs or dry-runs ESP-IDF with the ESP32, ESP32-S3, and ESP32-P4
|
|
145
|
+
toolchains. The version is resolved dynamically: `--idf-version <tag>` or
|
|
146
|
+
`GEA_ESP_IDF_VERSION` pins an exact release, otherwise GeaStack tries the
|
|
147
|
+
latest stable ESP-IDF release on GitHub and falls back to its pinned default
|
|
148
|
+
(currently v6.0.2) when that cannot be determined.
|
package/docs/SETUP.md
CHANGED
|
@@ -34,8 +34,8 @@ For ESP32 hardware:
|
|
|
34
34
|
- Node.js 20.19 or newer.
|
|
35
35
|
- npm.
|
|
36
36
|
- Python 3.
|
|
37
|
-
- ESP-IDF v6.0.
|
|
38
|
-
-
|
|
37
|
+
- ESP-IDF v6.0.2 (GeaStack's pinned default; see ESP-IDF For ESP32 Targets below).
|
|
38
|
+
- a board alias for your board (`~/.geastack/boards.json` or the project's `.gea/boards.json`, see Board Configuration).
|
|
39
39
|
|
|
40
40
|
For the Waveshare ESP32-S3 AMOLED board, use
|
|
41
41
|
[ESP32-WAVESHARE-AMOLED-QUICKSTART.md](ESP32-WAVESHARE-AMOLED-QUICKSTART.md).
|
|
@@ -69,9 +69,21 @@ npm --version
|
|
|
69
69
|
|
|
70
70
|
## ESP-IDF For ESP32 Targets
|
|
71
71
|
|
|
72
|
-
The embedded board scripts
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
The embedded board scripts target ESP-IDF v6.0.2 by default. GeaStack resolves
|
|
73
|
+
the version it installs or verifies dynamically, in this order:
|
|
74
|
+
|
|
75
|
+
1. `--idf-version <tag>` (e.g. `npx gea setup --esp-idf --idf-version v6.1.0-rc1`)
|
|
76
|
+
to pin an exact release or try a release candidate.
|
|
77
|
+
2. `GEA_ESP_IDF_VERSION` (same shape) when no `--idf-version` is given.
|
|
78
|
+
3. The latest stable ESP-IDF release on GitHub, when it can be determined
|
|
79
|
+
(release candidates and betas are ignored).
|
|
80
|
+
4. The pinned default, v6.0.2, when nothing above applies (offline, GitHub
|
|
81
|
+
unreachable, etc).
|
|
82
|
+
|
|
83
|
+
Verifying an existing install accepts any installed version whose
|
|
84
|
+
major.minor is the same as or newer than the resolved target -- an installed
|
|
85
|
+
6.0.2 is never rejected just because the resolved target moved on to, say,
|
|
86
|
+
6.1.0, unless it genuinely trails it.
|
|
75
87
|
|
|
76
88
|
Command-line install:
|
|
77
89
|
|
|
@@ -84,7 +96,7 @@ Equivalent manual install:
|
|
|
84
96
|
```sh
|
|
85
97
|
mkdir -p "$HOME/esp"
|
|
86
98
|
cd "$HOME/esp"
|
|
87
|
-
git clone -b v6.0.
|
|
99
|
+
git clone -b v6.0.2 --recursive https://github.com/espressif/esp-idf.git
|
|
88
100
|
cd esp-idf
|
|
89
101
|
./install.sh esp32,esp32s3,esp32p4
|
|
90
102
|
. ./export.sh
|
|
@@ -106,7 +118,7 @@ export GEA_EMBEDDED_IDF_EXPORT="/path/to/esp-idf/export.sh"
|
|
|
106
118
|
```
|
|
107
119
|
|
|
108
120
|
The board script also checks common locations such as `$HOME/esp/esp-idf`,
|
|
109
|
-
`$HOME/esp32/esp-idf`, and `$HOME/esp32/esp-idf-v6.0.
|
|
121
|
+
`$HOME/esp32/esp-idf`, and `$HOME/esp32/esp-idf-v6.0.2`.
|
|
110
122
|
|
|
111
123
|
Verify through GeaStack:
|
|
112
124
|
|
|
@@ -224,9 +236,44 @@ manager or from python.org.
|
|
|
224
236
|
|
|
225
237
|
## Board Configuration
|
|
226
238
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
239
|
+
A board alias names one physical unit: `gea flash --board amoled` resolves
|
|
240
|
+
`amoled` to a target, a USB serial and, optionally, an IP. Aliases are
|
|
241
|
+
machine-local configuration and never come from a package. The CLI merges two
|
|
242
|
+
files, project over home:
|
|
243
|
+
|
|
244
|
+
| file | holds | written by |
|
|
245
|
+
| --- | --- | --- |
|
|
246
|
+
| `~/.geastack/boards.json` | every board on this machine (`GEA_HOME` relocates the directory) | `gea boards add --global`, `gea boards set` |
|
|
247
|
+
| `<project>/.gea/boards.json` | aliases specific to one application; overrides a home alias of the same name | `create-geastack` (empty), `gea boards add` |
|
|
248
|
+
|
|
249
|
+
`--boards-config <file>` (or `GEA_BOARDS_CONFIG`) replaces both with exactly
|
|
250
|
+
that file. By default `gea boards add` writes to the project config when the
|
|
251
|
+
project has one and to the home config otherwise; an existing alias is always
|
|
252
|
+
edited where it lives.
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
npx gea boards discover # what is plugged in: alias, app, IP
|
|
256
|
+
npx gea boards add # register a board (guided)
|
|
257
|
+
npx gea boards list # every alias and which file it lives in
|
|
258
|
+
npx gea boards set amoled host 192.168.1.100
|
|
259
|
+
npx gea boards rename amoled desk-amoled
|
|
260
|
+
npx gea boards remove desk-amoled
|
|
261
|
+
npx gea doctor
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`gea boards discover` sends one `GEADEV PING` to each serial device without
|
|
265
|
+
resetting it; a board running gea firmware answers with its app id, its IP
|
|
266
|
+
(when it has joined WiFi) and its MAC, and the CLI pairs the reply with an
|
|
267
|
+
alias by USB serial. `--save` writes a reported IP into
|
|
268
|
+
`transports.ota.host`, which is what `gea ota`, `gea logs` and
|
|
269
|
+
`gea screenshot` use over WiFi.
|
|
270
|
+
|
|
271
|
+
### Entry shapes
|
|
272
|
+
|
|
273
|
+
Every entry names a `target` (`gea targets list`) and its `adapter`, then the
|
|
274
|
+
transports the board offers. The USB serial is the stable identity: on ESP32
|
|
275
|
+
boards it is the station MAC, and the CLI resolves it to today's `/dev` port
|
|
276
|
+
at call time, so never record a port path.
|
|
230
277
|
|
|
231
278
|
```json
|
|
232
279
|
{
|
|
@@ -234,22 +281,53 @@ Example:
|
|
|
234
281
|
"target": "esp32-s3-touch-amoled-2.06",
|
|
235
282
|
"adapter": "esp32-idf",
|
|
236
283
|
"transports": {
|
|
237
|
-
"usbSerial": {
|
|
238
|
-
|
|
239
|
-
|
|
284
|
+
"usbSerial": { "serial": "80:B5:4E:DA:73:88" },
|
|
285
|
+
"ota": { "host": "192.168.1.100" }
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
"rotary": {
|
|
289
|
+
"target": "esp32-s3-elecrow-rotary-2.1",
|
|
290
|
+
"adapter": "esp32-idf",
|
|
291
|
+
"transports": {
|
|
292
|
+
"usbSerial": { "serial": "14:C1:9F:26:65:08", "restartAfterFlash": "manual" }
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
"tufty": {
|
|
296
|
+
"target": "rp2350-tufty-2350",
|
|
297
|
+
"adapter": "rp2350-pico",
|
|
298
|
+
"transports": { "usbSerial": { "serial": "fa59949adbb4802f" } }
|
|
299
|
+
},
|
|
300
|
+
"linux": {
|
|
301
|
+
"target": "geaos",
|
|
302
|
+
"adapter": "geaos-linux",
|
|
303
|
+
"transports": {
|
|
304
|
+
"telnet": { "host": "192.168.7.2", "port": 2323 },
|
|
305
|
+
"fastboot": { "serial": "geaos001" }
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
"lokmat": {
|
|
309
|
+
"target": "lokmat-applp2max",
|
|
310
|
+
"adapter": "geaos-arm64",
|
|
311
|
+
"transports": {
|
|
312
|
+
"usbSerial": { "serial": "geaos01" },
|
|
313
|
+
"fastboot": { "serial": "0123456789ABCDEF" },
|
|
314
|
+
"mtk": { "workdir": "~/lokmat-root" }
|
|
240
315
|
}
|
|
316
|
+
},
|
|
317
|
+
"my-board": {
|
|
318
|
+
"target": "my-board",
|
|
319
|
+
"targetDefinition": "targets/my-board.json",
|
|
320
|
+
"adapter": "esp32-idf",
|
|
321
|
+
"transports": { "usbSerial": { "serial": "YOUR_BOARD_USB_SERIAL" } }
|
|
241
322
|
}
|
|
242
323
|
}
|
|
243
324
|
```
|
|
244
325
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
npx gea list targets
|
|
251
|
-
npx gea doctor
|
|
252
|
-
```
|
|
326
|
+
- `restartAfterFlash: "manual"` marks a board whose USB-Serial-JTAG port
|
|
327
|
+
re-enters ROM download mode after a flash; the CLI stops and asks for a
|
|
328
|
+
power cycle instead of pulsing the reset lines.
|
|
329
|
+
- `targetDefinition` points at a custom target composed by `gea setup` /
|
|
330
|
+
`gea chips`, relative to the file the alias lives in.
|
|
253
331
|
|
|
254
332
|
## Common Verification Flow
|
|
255
333
|
|
package/docs/SPEC.md
CHANGED
|
@@ -83,9 +83,12 @@ gea flash --app bouncing-balls-jsx --board amoled --monitor
|
|
|
83
83
|
gea flash --app css-3d-cube --target android
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
The CLI
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
The CLI resolves board aliases from two machine-local files merged project over
|
|
87
|
+
home: `~/.geastack/boards.json` (every board on the machine, `GEA_HOME`
|
|
88
|
+
relocates it) and the project's `.gea/boards.json`. `--boards-config` replaces
|
|
89
|
+
both. No package ships aliases. `gea boards` manages them: `list`, `show`,
|
|
90
|
+
`add`, `set <alias> <key> <value>`, `remove`, `rename`, and `discover`, which
|
|
91
|
+
identifies connected boards over USB (`GEADEV PING` reports app, IP and MAC).
|
|
89
92
|
|
|
90
93
|
### `gea chips`
|
|
91
94
|
|
package/package.json
CHANGED
package/src/boards/config.mjs
CHANGED
|
@@ -1,19 +1,70 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
|
|
3
4
|
import { ExitCode, fail } from '../errors.mjs'
|
|
4
|
-
import { exists, readJson } from '../fs-utils.mjs'
|
|
5
|
+
import { exists, readJson, writeJson } from '../fs-utils.mjs'
|
|
5
6
|
|
|
6
|
-
// Board aliases
|
|
7
|
-
//
|
|
7
|
+
// Board aliases are machine-local configuration: which physical board answers
|
|
8
|
+
// to `--board amoled`, its USB serial, its IP. They live in two tiers that
|
|
9
|
+
// are merged, project over home:
|
|
10
|
+
//
|
|
11
|
+
// ~/.geastack/boards.json every board on this machine (GEA_HOME overrides
|
|
12
|
+
// the directory)
|
|
13
|
+
// <project>/.gea/boards.json aliases specific to one project, overriding a
|
|
14
|
+
// home alias of the same name
|
|
15
|
+
//
|
|
16
|
+
// An explicit --boards-config (or GEA_BOARDS_CONFIG) replaces both tiers: a
|
|
17
|
+
// caller naming a file wants exactly that file. Nothing is ever read from an
|
|
18
|
+
// installed package -- a board catalog shipped in @geastack/targets was a
|
|
19
|
+
// development convenience that described one developer's bench, and every
|
|
20
|
+
// npm install would have to overwrite it.
|
|
21
|
+
|
|
22
|
+
export function homeBoardsConfigPath(env = process.env) {
|
|
23
|
+
const home = env.GEA_HOME || path.join(env.HOME || env.USERPROFILE || os.homedir(), '.geastack')
|
|
24
|
+
return path.join(home, 'boards.json')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Every file that contributes aliases, lowest precedence first. The list is
|
|
28
|
+
// the same whether or not the files exist so writers can target a tier that
|
|
29
|
+
// has not been created yet.
|
|
30
|
+
export function boardConfigTiers(ctx) {
|
|
31
|
+
if (ctx.boardsConfig) return [{ scope: 'explicit', file: ctx.boardsConfig }]
|
|
32
|
+
const tiers = []
|
|
33
|
+
if (ctx.homeBoardsConfig) tiers.push({ scope: 'home', file: ctx.homeBoardsConfig })
|
|
34
|
+
if (ctx.projectBoardsConfig) tiers.push({ scope: 'project', file: ctx.projectBoardsConfig })
|
|
35
|
+
return tiers
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The single path older callers print or check: the explicit file, else the
|
|
39
|
+
// highest-precedence tier that exists, else where `gea boards add` would
|
|
40
|
+
// write (project when the project already has a config, else home).
|
|
8
41
|
export function boardConfigPath(ctx) {
|
|
42
|
+
const tiers = boardConfigTiers(ctx)
|
|
43
|
+
const existing = [...tiers].reverse().find((tier) => exists(tier.file))
|
|
44
|
+
return existing ? existing.file : boardConfigWritePath(ctx)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Where a write goes when the caller does not say: `--global` / `--local`
|
|
48
|
+
// pick a tier, an alias that already exists is edited in place, a new alias
|
|
49
|
+
// joins the project config if the project has one and the home config
|
|
50
|
+
// otherwise.
|
|
51
|
+
export function boardConfigWritePath(ctx, { scope = '', alias = '' } = {}) {
|
|
52
|
+
const tiers = boardConfigTiers(ctx)
|
|
9
53
|
if (ctx.boardsConfig) return ctx.boardsConfig
|
|
10
|
-
if (
|
|
11
|
-
|
|
54
|
+
if (scope === 'global' || scope === 'home') return ctx.homeBoardsConfig
|
|
55
|
+
if (scope === 'project') return ctx.projectBoardsConfig
|
|
56
|
+
if (scope) fail(`Unknown board config scope '${scope}'. Expected --global or --local.`, ExitCode.usage)
|
|
57
|
+
if (alias) {
|
|
58
|
+
const origin = boardConfigOrigins(ctx).get(alias)
|
|
59
|
+
if (origin) return origin
|
|
60
|
+
}
|
|
61
|
+
const project = tiers.find((tier) => tier.scope === 'project')
|
|
62
|
+
if (project && exists(project.file)) return project.file
|
|
63
|
+
return ctx.homeBoardsConfig || project?.file || path.join(ctx.cwd || process.cwd(), '.gea', 'boards.json')
|
|
12
64
|
}
|
|
13
65
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!file || !exists(file)) return {}
|
|
66
|
+
function readTier(file) {
|
|
67
|
+
if (!exists(file)) return {}
|
|
17
68
|
try {
|
|
18
69
|
return normalizeBoardConfig(readJson(file))
|
|
19
70
|
} catch (error) {
|
|
@@ -21,6 +72,37 @@ export function loadBoardConfig(ctx) {
|
|
|
21
72
|
}
|
|
22
73
|
}
|
|
23
74
|
|
|
75
|
+
// Merged aliases plus, for each, the file it came from: a `targetDefinition`
|
|
76
|
+
// is relative to its own file, and `gea boards list` says which tier an alias
|
|
77
|
+
// lives in.
|
|
78
|
+
export function loadBoardConfigWithOrigins(ctx) {
|
|
79
|
+
const boards = {}
|
|
80
|
+
const origins = new Map()
|
|
81
|
+
for (const tier of boardConfigTiers(ctx)) {
|
|
82
|
+
for (const [alias, board] of Object.entries(readTier(tier.file))) {
|
|
83
|
+
boards[alias] = board
|
|
84
|
+
origins.set(alias, tier.file)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { boards, origins }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function loadBoardConfig(ctx) {
|
|
91
|
+
return loadBoardConfigWithOrigins(ctx).boards
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function boardConfigOrigins(ctx) {
|
|
95
|
+
return loadBoardConfigWithOrigins(ctx).origins
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function readBoardConfigFile(file) {
|
|
99
|
+
return readTier(file)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function writeBoardConfigFile(file, boards) {
|
|
103
|
+
writeJson(file, boards)
|
|
104
|
+
}
|
|
105
|
+
|
|
24
106
|
export function normalizeBoardConfig(raw) {
|
|
25
107
|
if (!raw || typeof raw !== 'object') return {}
|
|
26
108
|
if (raw.boards && typeof raw.boards === 'object') return raw.boards
|
package/src/boards/resolve.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
|
-
import { loadBoardConfig, normalizeBoardConfig } from './config.mjs'
|
|
4
|
+
import { boardConfigOrigins, boardConfigPath, loadBoardConfig, normalizeBoardConfig } from './config.mjs'
|
|
5
5
|
import { loadTargets } from './targets.mjs'
|
|
6
6
|
import { resolveUsbSerialPort } from './usb.mjs'
|
|
7
7
|
|
|
@@ -30,14 +30,16 @@ export function resolveBoardSelection({
|
|
|
30
30
|
needs = {},
|
|
31
31
|
targets = ctx ? loadTargets(ctx) : {},
|
|
32
32
|
config = ctx ? loadBoardConfig(ctx) : {},
|
|
33
|
-
|
|
33
|
+
// A targetDefinition is relative to the file its alias came from, which
|
|
34
|
+
// differs between the home and project tiers.
|
|
35
|
+
configDir = ctx ? path.dirname((boardName && boardConfigOrigins(ctx).get(boardName)) || boardConfigPath(ctx)) : process.cwd(),
|
|
34
36
|
usbSerialResolver = resolveUsbSerialPort,
|
|
35
37
|
deferUsbPort = false
|
|
36
38
|
} = {}) {
|
|
37
39
|
const boards = normalizeBoardConfig(config)
|
|
38
40
|
const board = boardName ? boards[boardName] : null
|
|
39
41
|
if (boardName && !board) {
|
|
40
|
-
throw new Error(`Unknown board '${boardName}'.
|
|
42
|
+
throw new Error(`Unknown board '${boardName}'. Run gea boards list, or gea boards add to register it.`)
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
let target = board?.target || targetName || ''
|
package/src/boards/usb.mjs
CHANGED
|
@@ -113,6 +113,47 @@ function macUsbCalloutPortsForSerial(serial, ioreg = runIoreg) {
|
|
|
113
113
|
return [...matches].sort()
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// Every USB callout port the registry knows, with the serial and product
|
|
117
|
+
// name inherited from the enclosing USB device: the inverse of
|
|
118
|
+
// macUsbCalloutPortsForSerial, for `gea boards discover` and the setup
|
|
119
|
+
// wizard. The /dev name carries no identity (the usbmodem number changes on
|
|
120
|
+
// every enumeration and identical boards share it), so this is the only
|
|
121
|
+
// place a port's serial can come from on macOS.
|
|
122
|
+
export function listMacUsbCalloutPorts(ioreg = runIoreg) {
|
|
123
|
+
let output = ''
|
|
124
|
+
try {
|
|
125
|
+
output = ioreg(['-p', 'IOService', '-l', '-w0'], { maxBuffer: 64 * 1024 * 1024 })
|
|
126
|
+
} catch {
|
|
127
|
+
return []
|
|
128
|
+
}
|
|
129
|
+
const stack = []
|
|
130
|
+
const ports = new Map()
|
|
131
|
+
for (const line of output.split(/\r?\n/)) {
|
|
132
|
+
const node = line.match(/^([\s|]*)[+\\-]*o\s+/)
|
|
133
|
+
if (node) {
|
|
134
|
+
const depth = (node[1].match(/\|/g) || []).length
|
|
135
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) stack.pop()
|
|
136
|
+
stack.push({ depth, serial: '', product: '' })
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
if (stack.length === 0) continue
|
|
140
|
+
const current = stack[stack.length - 1]
|
|
141
|
+
const serialValue = ioregStringProperty(line, 'kUSBSerialNumberString') || ioregStringProperty(line, 'USB Serial Number')
|
|
142
|
+
if (serialValue) current.serial = serialValue
|
|
143
|
+
const productValue = ioregStringProperty(line, 'kUSBProductString') || ioregStringProperty(line, 'USB Product Name')
|
|
144
|
+
if (productValue) current.product = productValue
|
|
145
|
+
const callout = ioregStringProperty(line, 'IOCalloutDevice')
|
|
146
|
+
if (!callout || !path.basename(callout).startsWith('cu.')) continue
|
|
147
|
+
// A callout with no USB serial above it is not a USB device at all
|
|
148
|
+
// (Bluetooth SPP, the debug console); probing those is a wasted timeout.
|
|
149
|
+
const serial = [...stack].reverse().find((entry) => entry.serial)?.serial || ''
|
|
150
|
+
if (!serial) continue
|
|
151
|
+
const label = [...stack].reverse().find((entry) => entry.product)?.product || ''
|
|
152
|
+
ports.set(callout, { path: callout, serial, label })
|
|
153
|
+
}
|
|
154
|
+
return [...ports.values()].sort((a, b) => a.path.localeCompare(b.path))
|
|
155
|
+
}
|
|
156
|
+
|
|
116
157
|
function runIoreg(args, options = {}) {
|
|
117
158
|
return execFileSync('ioreg', args, { encoding: 'utf8', ...options })
|
|
118
159
|
}
|
package/src/chips.mjs
CHANGED
|
@@ -142,7 +142,7 @@ export function resolveTargetDefinition(ctx, parsed) {
|
|
|
142
142
|
const configPath = option(parsed, 'boards-config')
|
|
143
143
|
? path.resolve(ctx.cwd, option(parsed, 'boards-config'))
|
|
144
144
|
: ctx.projectBoardsConfig
|
|
145
|
-
if (!configPath || !exists(configPath)) fail(
|
|
145
|
+
if (!configPath || !exists(configPath)) fail(`No board config was found at ${configPath || '.gea/boards.json'}. Run gea setup and create a custom board first.`, ExitCode.usage)
|
|
146
146
|
const boards = readJson(configPath)
|
|
147
147
|
const requested = option(parsed, 'board', '')
|
|
148
148
|
const customBoards = Object.entries(boards).filter(([, entry]) => typeof entry?.targetDefinition === 'string' && entry.targetDefinition)
|
package/src/commands/apps.mjs
CHANGED
|
@@ -2,7 +2,6 @@ import { readdirSync } from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { flag, option, optionList } from '../args.mjs'
|
|
5
|
-
import { loadBoardConfig, normalizeBoardConfig, boardConfigPath } from '../boards/config.mjs'
|
|
6
5
|
import { loadTargets } from '../boards/targets.mjs'
|
|
7
6
|
import { ExitCode, fail } from '../errors.mjs'
|
|
8
7
|
import { exists } from '../fs-utils.mjs'
|
|
@@ -161,38 +160,7 @@ async function iconSheet(ctx, parsed, options) {
|
|
|
161
160
|
return 0
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
// gea
|
|
165
|
-
|
|
166
|
-
export async function boardsCommand(ctx, parsed, rest, options) {
|
|
167
|
-
const sub = rest[0] || 'list'
|
|
168
|
-
const boards = normalizeBoardConfig(loadBoardConfig(ctx))
|
|
169
|
-
if (sub === 'list') {
|
|
170
|
-
if (flag(parsed, 'json')) options.stdout(JSON.stringify(boards, null, 2))
|
|
171
|
-
else {
|
|
172
|
-
const names = Object.keys(boards).sort()
|
|
173
|
-
if (names.length === 0) options.stdout(`No boards configured in ${boardConfigPath(ctx)}. Run gea setup, or gea boards add.`)
|
|
174
|
-
for (const name of names) {
|
|
175
|
-
const board = boards[name]
|
|
176
|
-
const bits = [board.target]
|
|
177
|
-
if (board.transports?.usbSerial?.serial) bits.push(`usb ${board.transports.usbSerial.serial}`)
|
|
178
|
-
if (board.transports?.ota?.host) bits.push(`wifi ${board.transports.ota.host}`)
|
|
179
|
-
options.stdout(`${name}\t${bits.join(' ')}`)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return 0
|
|
183
|
-
}
|
|
184
|
-
if (sub === 'show') {
|
|
185
|
-
const name = rest[1] || option(parsed, 'board', '')
|
|
186
|
-
if (!name || !boards[name]) fail(`Unknown board '${name}'. Run gea boards list.`, ExitCode.usage)
|
|
187
|
-
options.stdout(JSON.stringify({ [name]: boards[name] }, null, 2))
|
|
188
|
-
return 0
|
|
189
|
-
}
|
|
190
|
-
if (sub === 'add') {
|
|
191
|
-
const { runSetupWizard } = await import('../setup-wizard.mjs')
|
|
192
|
-
return runSetupWizard(ctx, parsed, options)
|
|
193
|
-
}
|
|
194
|
-
fail(`Unknown boards subcommand '${sub}'. Expected list, show, or add.`, ExitCode.usage)
|
|
195
|
-
}
|
|
163
|
+
// gea targets ...
|
|
196
164
|
|
|
197
165
|
export function targetsCommand(ctx, parsed, rest, options) {
|
|
198
166
|
const sub = rest[0] || 'list'
|
package/src/commands/board.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { eraseSlot, flashFirmware, flashImageSet, flashOptions, postFlashRestart
|
|
|
11
11
|
import { bleOta, otaEraseSlot, otaFlash, otaStage, waitForReboot } from '../esp32/ota.mjs'
|
|
12
12
|
import { manifestRequestsBleOta } from '../esp32/capabilities.mjs'
|
|
13
13
|
import { runGeaos } from '../geaos/adapter.mjs'
|
|
14
|
-
import { assertTargetEnabled, assertValidApp, resolveRequestedApp } from '../manifest.mjs'
|
|
14
|
+
import { assertTargetEnabled, assertValidApp, discoverApps, resolveRequestedApp, targetEnabledForApp } from '../manifest.mjs'
|
|
15
15
|
import { buildRp2350, flashRp2350, rp2350BuildDir } from '../rp2350/adapter.mjs'
|
|
16
16
|
import { runTargetHook } from '../taurus/adapter.mjs'
|
|
17
17
|
|
|
@@ -69,9 +69,22 @@ function io(parsed, options) {
|
|
|
69
69
|
|
|
70
70
|
// ---- build ------------------------------------------------------------------
|
|
71
71
|
|
|
72
|
+
// Firmware is built per app and the ESP32/RP2350 CMake refuses to configure
|
|
73
|
+
// without one (its script-mode pass would otherwise analyze a directory), so
|
|
74
|
+
// those adapters need an app even for --configure-only; the geaos and taurus
|
|
75
|
+
// adapters have app-less actions.
|
|
76
|
+
const appRequiredAdapters = new Set(['esp32-idf', 'rp2350-pico'])
|
|
77
|
+
|
|
78
|
+
function requireAppForAdapter(ctx, parsed, selection, app) {
|
|
79
|
+
if (app || !appRequiredAdapters.has(selection.adapter)) return app
|
|
80
|
+
const candidates = discoverApps(ctx).filter((candidate) => targetEnabledForApp(ctx, candidate, selection.boardName || selection.target))
|
|
81
|
+
const hint = candidates.length > 0 ? `apps targeting '${selection.boardName || selection.target}': ${candidates.map((candidate) => candidate.id).join(', ')}` : `no app in ${ctx.projectRoot} targets '${selection.boardName || selection.target}' yet`
|
|
82
|
+
fail(`Board '${selection.boardName || selection.target}' builds one app at a time: pass --app <id> or run inside the app folder (${hint}).`, ExitCode.usage)
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
export async function buildCommand(ctx, parsed, rest, options) {
|
|
73
86
|
const selection = selectBoard(ctx, parsed)
|
|
74
|
-
const app = optionalApp(ctx, parsed, rest, selection)
|
|
87
|
+
const app = requireAppForAdapter(ctx, parsed, selection, optionalApp(ctx, parsed, rest, selection))
|
|
75
88
|
const base = io(parsed, options)
|
|
76
89
|
const env = createChildEnv(ctx, base.env)
|
|
77
90
|
switch (selection.adapter) {
|