@geastack/cli 0.1.54 → 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 +3 -3
- package/docs/NPX-COMMANDS.md +8 -4
- package/docs/SETUP.md +18 -6
- package/package.json +1 -1
- package/src/commands/board.mjs +15 -2
- package/src/esp32/idf-version.mjs +89 -0
- package/src/gea.mjs +4 -2
- package/src/setup-wizard.mjs +134 -49
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
|
package/docs/NPX-COMMANDS.md
CHANGED
|
@@ -93,8 +93,9 @@ 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
|
|
98
99
|
(`--global` for `~/.geastack/boards.json`, `--local` for the project's
|
|
99
100
|
`.gea/boards.json`; by default the project config when it exists), then
|
|
100
101
|
initializes the selected board target so the next command can be
|
|
@@ -140,5 +141,8 @@ ESP-IDF setup is available as:
|
|
|
140
141
|
npx gea setup --esp-idf
|
|
141
142
|
```
|
|
142
143
|
|
|
143
|
-
It installs or dry-runs ESP-IDF
|
|
144
|
-
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,7 +34,7 @@ For ESP32 hardware:
|
|
|
34
34
|
- Node.js 20.19 or newer.
|
|
35
35
|
- npm.
|
|
36
36
|
- Python 3.
|
|
37
|
-
- ESP-IDF v6.0.
|
|
37
|
+
- ESP-IDF v6.0.2 (GeaStack's pinned default; see ESP-IDF For ESP32 Targets below).
|
|
38
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
|
|
@@ -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
|
|
package/package.json
CHANGED
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) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// ESP-IDF target-version resolution.
|
|
2
|
+
//
|
|
3
|
+
// One pinned default in code, an env var / CLI option that overrides it
|
|
4
|
+
// outright (pin an older release or try a release candidate), and a
|
|
5
|
+
// best-effort GitHub "latest release" lookup used only when nothing
|
|
6
|
+
// overrides the pin. The network call is injected (`fetchImpl`/`fetchLatest`)
|
|
7
|
+
// so real usage hits GitHub while tests supply a fake and never touch the
|
|
8
|
+
// network.
|
|
9
|
+
|
|
10
|
+
const STABLE_TAG = /^v(\d+)\.(\d+)\.(\d+)$/
|
|
11
|
+
const VERSION_IN_TEXT = /v?(\d+)\.(\d+)\.(\d+)/i
|
|
12
|
+
|
|
13
|
+
// The version GeaStack targets when nothing overrides it and the latest
|
|
14
|
+
// release cannot be determined (offline, GitHub unreachable, rate limited).
|
|
15
|
+
// Bump this alongside board-script updates.
|
|
16
|
+
export const DEFAULT_ESP_IDF_VERSION = 'v6.0.2'
|
|
17
|
+
|
|
18
|
+
const GITHUB_LATEST_RELEASE_URL = 'https://api.github.com/repos/espressif/esp-idf/releases/latest'
|
|
19
|
+
|
|
20
|
+
// Parses a strict `vX.Y.Z` release tag. Returns null for anything else,
|
|
21
|
+
// including release candidates and betas (`v6.1.0-rc1`), so callers can
|
|
22
|
+
// filter those out of the "latest" lookup.
|
|
23
|
+
export function parseStableIdfTag(value) {
|
|
24
|
+
const match = STABLE_TAG.exec(String(value || '').trim())
|
|
25
|
+
if (!match) return null
|
|
26
|
+
return { tag: match[0], major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Pulls a `major.minor(.patch)` out of free-form text such as
|
|
30
|
+
// `idf.py --version`'s `ESP-IDF v6.0.2-dirty` output.
|
|
31
|
+
export function extractIdfVersionFromText(text) {
|
|
32
|
+
const match = VERSION_IN_TEXT.exec(String(text || ''))
|
|
33
|
+
if (!match) return null
|
|
34
|
+
return { majorMinor: `${match[1]}.${match[2]}`, full: `${match[1]}.${match[2]}.${match[3]}` }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Best-effort lookup of the latest stable ESP-IDF release tag from GitHub.
|
|
38
|
+
// Never throws: returns '' when the request fails, times out (~4s default),
|
|
39
|
+
// the machine is offline, or the latest release is not a stable vX.Y.Z tag.
|
|
40
|
+
// `fetchImpl` defaults to the global fetch; tests inject a fake instead of
|
|
41
|
+
// touching the network.
|
|
42
|
+
export async function fetchLatestEspIdfVersion({ fetchImpl = globalThis.fetch, timeoutMs = 4000, log = () => {} } = {}) {
|
|
43
|
+
if (typeof fetchImpl !== 'function') return ''
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetchImpl(GITHUB_LATEST_RELEASE_URL, {
|
|
48
|
+
signal: controller.signal,
|
|
49
|
+
headers: { Accept: 'application/vnd.github+json' }
|
|
50
|
+
})
|
|
51
|
+
if (!response?.ok) return ''
|
|
52
|
+
const body = await response.json()
|
|
53
|
+
return parseStableIdfTag(body?.tag_name)?.tag || ''
|
|
54
|
+
} catch (error) {
|
|
55
|
+
log(`Could not determine the latest ESP-IDF release (${error.message}); using ${DEFAULT_ESP_IDF_VERSION}.`)
|
|
56
|
+
return ''
|
|
57
|
+
} finally {
|
|
58
|
+
clearTimeout(timer)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Resolves the ESP-IDF version tag to install/verify against, in order:
|
|
63
|
+
// 1. `override` (--idf-version) or GEA_ESP_IDF_VERSION -- explicit pin or
|
|
64
|
+
// a release candidate to test; used as-is (not required to be stable).
|
|
65
|
+
// 2. the latest stable GitHub release, when `fetchLatest` can determine one.
|
|
66
|
+
// 3. DEFAULT_ESP_IDF_VERSION.
|
|
67
|
+
export async function resolveEspIdfVersion({ override, env = process.env, fetchLatest = fetchLatestEspIdfVersion, log = () => {} } = {}) {
|
|
68
|
+
const requested = override || env.GEA_ESP_IDF_VERSION
|
|
69
|
+
if (requested) return requested.startsWith('v') ? requested : `v${requested}`
|
|
70
|
+
const latest = await fetchLatest({ log })
|
|
71
|
+
return latest || DEFAULT_ESP_IDF_VERSION
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// True when an installed ESP-IDF's version.cmake-derived version satisfies
|
|
75
|
+
// the resolved target: same-or-newer major.minor. This is a floor, not an
|
|
76
|
+
// exact-match: an installed 6.0.2 must not be rejected just because the
|
|
77
|
+
// resolved target moved on to, say, 6.1.0 -- reinstalling isn't required
|
|
78
|
+
// unless the installed major.minor genuinely trails the target's.
|
|
79
|
+
// An unparsable target is treated as "anything installed is acceptable".
|
|
80
|
+
export function idfVersionMeetsTarget(installedVersion, targetTag) {
|
|
81
|
+
const target = extractIdfVersionFromText(targetTag)
|
|
82
|
+
if (!target) return true
|
|
83
|
+
const [targetMajor, targetMinor] = target.majorMinor.split('.').map(Number)
|
|
84
|
+
const parts = String(installedVersion?.majorMinor || '').split('.').map(Number)
|
|
85
|
+
if (parts.length < 2 || parts.some((part) => Number.isNaN(part))) return false
|
|
86
|
+
const [installedMajor, installedMinor] = parts
|
|
87
|
+
if (installedMajor !== targetMajor) return installedMajor > targetMajor
|
|
88
|
+
return installedMinor >= targetMinor
|
|
89
|
+
}
|
package/src/gea.mjs
CHANGED
|
@@ -43,8 +43,10 @@ export async function runGea(argv, io = {}) {
|
|
|
43
43
|
|
|
44
44
|
const ctx = createContext(parsed, env, cwd)
|
|
45
45
|
const rest = parsed.positionals.slice(1)
|
|
46
|
-
// probeSerialDevice lets tests answer `gea boards discover` without a port
|
|
47
|
-
|
|
46
|
+
// probeSerialDevice lets tests answer `gea boards discover` without a port;
|
|
47
|
+
// fetchEspIdfLatest likewise lets tests answer the setup wizard's ESP-IDF
|
|
48
|
+
// "latest release" lookup without touching the network.
|
|
49
|
+
const options = { stdout, stderr, env, stdin, output, prompt, probeSerialDevice: io.probeSerialDevice, fetchEspIdfLatest: io.fetchEspIdfLatest }
|
|
48
50
|
|
|
49
51
|
// A platform name in --target (web, macos, ...) is not a board.
|
|
50
52
|
const target = option(parsed, 'target', '')
|
package/src/setup-wizard.mjs
CHANGED
|
@@ -3,30 +3,34 @@ import os from 'node:os'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
5
|
import { knownBoards } from './board-catalog.mjs'
|
|
6
|
-
import { boardConfigWritePath } from './boards/config.mjs'
|
|
6
|
+
import { boardConfigWritePath, loadBoardConfig } from './boards/config.mjs'
|
|
7
|
+
import { discoverBoards, probeSerialDevice } from './commands/boards.mjs'
|
|
8
|
+
import { discoverApps, resolveRequestedApp, targetEnabledForApp } from './manifest.mjs'
|
|
7
9
|
import { configureChipSelection, loadChipCatalog, validateGpioAssignments } from './chips.mjs'
|
|
8
10
|
import { flag, option } from './args.mjs'
|
|
9
11
|
import { ExitCode, fail } from './errors.mjs'
|
|
12
|
+
import { espIdfVersion as readInstalledEspIdfVersion, findEspIdf } from './esp32/idf-env.mjs'
|
|
13
|
+
import { extractIdfVersionFromText, fetchLatestEspIdfVersion, idfVersionMeetsTarget, resolveEspIdfVersion } from './esp32/idf-version.mjs'
|
|
10
14
|
import { exists, readJson, writeJson } from './fs-utils.mjs'
|
|
11
15
|
import { ask, choose, confirm, createPrompt } from './prompts.mjs'
|
|
12
16
|
import { runExternal } from './run.mjs'
|
|
13
|
-
import { detectSerialDevices
|
|
17
|
+
import { detectSerialDevices } from './serial-devices.mjs'
|
|
14
18
|
import { commandVersion } from './toolchain.mjs'
|
|
15
19
|
|
|
16
|
-
const espIdfVersion = 'v6.0.1'
|
|
17
20
|
const espIdfInstallTargets = 'esp32,esp32s3,esp32p4'
|
|
18
21
|
|
|
19
22
|
export async function runSetupWizard(ctx, parsed, io) {
|
|
20
23
|
const stdout = io.stdout || console.log
|
|
21
24
|
const prompt = createPrompt(io)
|
|
22
25
|
try {
|
|
26
|
+
const targetIdfVersion = await resolveEspIdfVersionForWizard(parsed, io)
|
|
23
27
|
renderHeader(io, 'GeaStack setup', [
|
|
24
28
|
`Project: ${ctx.projectRoot}`,
|
|
25
29
|
`Boards: ${boardConfigPath(ctx, parsed)}`
|
|
26
30
|
])
|
|
27
31
|
if (option(parsed, 'esp-idf') === true) {
|
|
28
32
|
renderStep(io, 'Toolchain', ['Checking ESP-IDF for ESP32 builds.'])
|
|
29
|
-
await maybeSetupEspIdf(ctx, parsed, io, prompt, { force: true })
|
|
33
|
+
await maybeSetupEspIdf(ctx, parsed, io, prompt, { force: true, targetVersion: targetIdfVersion })
|
|
30
34
|
return 0
|
|
31
35
|
}
|
|
32
36
|
const mode = await choose(prompt, {
|
|
@@ -50,7 +54,7 @@ export async function runSetupWizard(ctx, parsed, io) {
|
|
|
50
54
|
{
|
|
51
55
|
value: 'esp-idf',
|
|
52
56
|
label: 'Only install/check ESP-IDF toolchain',
|
|
53
|
-
description: `Installs or verifies ESP-IDF ${
|
|
57
|
+
description: `Installs or verifies ESP-IDF ${targetIdfVersion}.`
|
|
54
58
|
}
|
|
55
59
|
],
|
|
56
60
|
defaultValue: 'known'
|
|
@@ -64,7 +68,7 @@ export async function runSetupWizard(ctx, parsed, io) {
|
|
|
64
68
|
|
|
65
69
|
if (mode === 'esp-idf') {
|
|
66
70
|
renderStep(io, 'Toolchain', ['Checking ESP-IDF for ESP32 builds.'])
|
|
67
|
-
await maybeSetupEspIdf(ctx, parsed, io, prompt, { force: true })
|
|
71
|
+
await maybeSetupEspIdf(ctx, parsed, io, prompt, { force: true, targetVersion: targetIdfVersion })
|
|
68
72
|
return 0
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -77,11 +81,11 @@ export async function runSetupWizard(ctx, parsed, io) {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
renderStep(io, 'Toolchain', ['Checking ESP-IDF before board initialization.'])
|
|
80
|
-
await maybeSetupEspIdf(ctx, parsed, io, prompt)
|
|
84
|
+
await maybeSetupEspIdf(ctx, parsed, io, prompt, { targetVersion: targetIdfVersion })
|
|
81
85
|
if (option(parsed, 'install') === true) {
|
|
82
86
|
await maybeInstallNpmDependencies(ctx, parsed, io, prompt, { force: true })
|
|
83
87
|
}
|
|
84
|
-
await maybeInitializeBoardTarget(ctx, parsed, io, boardSetup)
|
|
88
|
+
await maybeInitializeBoardTarget(ctx, parsed, io, prompt, boardSetup)
|
|
85
89
|
if (boardSetup?.flashReady) {
|
|
86
90
|
stdout(`Ready: npx gea flash --board ${boardSetup.alias} --monitor`)
|
|
87
91
|
} else {
|
|
@@ -114,8 +118,8 @@ async function setupKnownBoard(ctx, parsed, io, prompt) {
|
|
|
114
118
|
validate: validateAlias
|
|
115
119
|
})
|
|
116
120
|
renderStep(io, 'Connection', ['Use a detected serial device, enter one manually, or skip it for now.'])
|
|
117
|
-
const serial = await selectUsbSerial(prompt, io, {
|
|
118
|
-
message: 'USB serial
|
|
121
|
+
const serial = await selectUsbSerial(prompt, io, ctx, {
|
|
122
|
+
message: 'The USB serial identifies this board whatever port it lands on.'
|
|
119
123
|
})
|
|
120
124
|
const otaHost = await ask(prompt, {
|
|
121
125
|
message: 'OTA host/IP (optional)',
|
|
@@ -231,8 +235,8 @@ async function setupCustomBoard(ctx, parsed, io, prompt) {
|
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
renderStep(io, 'Connection', ['The first flash uses USB. BLE OTA can take over after the initial firmware is running.'])
|
|
234
|
-
const usbSerial = await selectUsbSerial(prompt, io, {
|
|
235
|
-
message: 'USB serial
|
|
238
|
+
const usbSerial = await selectUsbSerial(prompt, io, ctx, {
|
|
239
|
+
message: 'The USB serial identifies this board whatever port it lands on.'
|
|
236
240
|
})
|
|
237
241
|
|
|
238
242
|
const configPath = boardConfigPath(ctx, parsed)
|
|
@@ -368,32 +372,78 @@ async function maybeInstallNpmDependencies(ctx, parsed, io, prompt, { force = fa
|
|
|
368
372
|
})
|
|
369
373
|
}
|
|
370
374
|
|
|
371
|
-
|
|
375
|
+
// A board target is configured for one app (the firmware is built per app,
|
|
376
|
+
// and its CMake refuses to run without one), so initialization needs an
|
|
377
|
+
// app: the current one when the wizard runs inside an app, else the only
|
|
378
|
+
// app in the project that targets this board, else the user's pick.
|
|
379
|
+
async function maybeInitializeBoardTarget(ctx, parsed, io, prompt, boardSetup) {
|
|
372
380
|
if (!boardSetup?.alias || !boardSetup.flashReady) return 0
|
|
373
381
|
if (option(parsed, 'initialize') === false) {
|
|
374
|
-
io.stdout(`Board initialization skipped. Later: npx gea setup --board ${boardSetup.alias}
|
|
382
|
+
io.stdout(`Board initialization skipped. Later: npx gea setup --board ${boardSetup.alias} --app <id>`)
|
|
375
383
|
return 0
|
|
376
384
|
}
|
|
377
385
|
if (!ctx.targetsRoot) {
|
|
378
|
-
io.stdout(`@geastack/targets is not installed. Later: npx gea setup --board ${boardSetup.alias}
|
|
386
|
+
io.stdout(`@geastack/targets is not installed. Later: npx gea setup --board ${boardSetup.alias} --app <id>`)
|
|
379
387
|
return 0
|
|
380
388
|
}
|
|
381
|
-
|
|
389
|
+
const app = await chooseAppForBoard(ctx, parsed, io, prompt, boardSetup.alias)
|
|
390
|
+
if (!app) return 0
|
|
391
|
+
io.stdout(`Initializing board target '${boardSetup.alias}' for app '${app.id}'...`)
|
|
382
392
|
const { buildCommand } = await import('./commands/board.mjs')
|
|
383
|
-
const setupParsed = { ...parsed, options: { ...parsed.options, board: boardSetup.alias, 'configure-only': true } }
|
|
393
|
+
const setupParsed = { ...parsed, options: { ...parsed.options, board: boardSetup.alias, app: app.id, 'configure-only': true } }
|
|
384
394
|
return buildCommand(ctx, setupParsed, [], io)
|
|
385
395
|
}
|
|
386
396
|
|
|
387
|
-
async function
|
|
397
|
+
async function chooseAppForBoard(ctx, parsed, io, prompt, alias) {
|
|
398
|
+
let current = null
|
|
399
|
+
try {
|
|
400
|
+
current = resolveRequestedApp(ctx, parsed, [])
|
|
401
|
+
} catch {
|
|
402
|
+
current = null
|
|
403
|
+
}
|
|
404
|
+
if (current && targetEnabledForApp(ctx, current, alias)) return current
|
|
405
|
+
const candidates = discoverApps(ctx).filter((app) => targetEnabledForApp(ctx, app, alias))
|
|
406
|
+
if (candidates.length === 0) {
|
|
407
|
+
io.stdout(`No app in ${ctx.projectRoot} targets '${alias}' yet; skipping board initialization. Later: npx gea setup --board ${alias} --app <id>`)
|
|
408
|
+
return null
|
|
409
|
+
}
|
|
410
|
+
if (candidates.length === 1) return candidates[0]
|
|
411
|
+
const picked = await choose(prompt, {
|
|
412
|
+
message: `Which app should the first '${alias}' build target?`,
|
|
413
|
+
choices: [
|
|
414
|
+
...candidates.map((app) => ({ value: app.id, label: app.id, description: app.root })),
|
|
415
|
+
{ value: '', label: 'Skip board initialization for now' }
|
|
416
|
+
],
|
|
417
|
+
defaultValue: candidates[0].id
|
|
418
|
+
})
|
|
419
|
+
return picked ? candidates.find((app) => app.id === picked) : null
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Resolves the ESP-IDF version target once for the whole wizard run:
|
|
423
|
+
// --idf-version / GEA_ESP_IDF_VERSION win outright; otherwise a best-effort
|
|
424
|
+
// GitHub "latest release" lookup, falling back to the pinned default when it
|
|
425
|
+
// cannot be determined. `io.fetchEspIdfLatest` lets callers (tests) inject a
|
|
426
|
+
// fake fetch instead of touching the network -- mirrors `probeSerialDevice`.
|
|
427
|
+
async function resolveEspIdfVersionForWizard(parsed, io) {
|
|
428
|
+
return resolveEspIdfVersion({
|
|
429
|
+
override: option(parsed, 'idf-version'),
|
|
430
|
+
env: io.env || process.env,
|
|
431
|
+
fetchLatest: io.fetchEspIdfLatest || fetchLatestEspIdfVersion,
|
|
432
|
+
log: io.stderr || (() => {})
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function maybeSetupEspIdf(ctx, parsed, io, prompt, { force = false, targetVersion } = {}) {
|
|
388
437
|
const env = io.env || process.env
|
|
389
|
-
const
|
|
438
|
+
const resolvedVersion = targetVersion || await resolveEspIdfVersionForWizard(parsed, io)
|
|
439
|
+
const status = detectEspIdf(env, resolvedVersion)
|
|
390
440
|
if (status.available) {
|
|
391
441
|
if (force) io.stdout(`ESP-IDF found: ${status.detail}`)
|
|
392
442
|
return 0
|
|
393
443
|
}
|
|
394
444
|
|
|
395
445
|
const install = force || await confirm(prompt, {
|
|
396
|
-
message: `ESP-IDF ${
|
|
446
|
+
message: `ESP-IDF ${resolvedVersion} was not found${status.detail ? ` (found ${status.detail})` : ''}. Install it now?`,
|
|
397
447
|
defaultValue: false
|
|
398
448
|
})
|
|
399
449
|
if (!install) {
|
|
@@ -405,7 +455,7 @@ async function maybeSetupEspIdf(ctx, parsed, io, prompt, { force = false } = {})
|
|
|
405
455
|
const dryRun = flag(parsed, 'dry-run')
|
|
406
456
|
if (!dryRun) fs.mkdirSync(path.dirname(idfDir), { recursive: true })
|
|
407
457
|
if (!exists(path.join(idfDir, 'install.sh')) && !exists(path.join(idfDir, 'install.bat'))) {
|
|
408
|
-
runExternal('git', ['clone', '-b',
|
|
458
|
+
runExternal('git', ['clone', '-b', resolvedVersion, '--recursive', 'https://github.com/espressif/esp-idf.git', idfDir], {
|
|
409
459
|
cwd: ctx.cwd,
|
|
410
460
|
env,
|
|
411
461
|
dryRun,
|
|
@@ -429,39 +479,74 @@ async function maybeSetupEspIdf(ctx, parsed, io, prompt, { force = false } = {})
|
|
|
429
479
|
return 0
|
|
430
480
|
}
|
|
431
481
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
482
|
+
// Prefers the same conventional-directory detection the build path uses
|
|
483
|
+
// (`findEspIdf` + its version.cmake), which lets a version-floor check run;
|
|
484
|
+
// falls back to a bare `idf.py --version` on PATH when no such directory is
|
|
485
|
+
// found but some ESP-IDF install has still put idf.py on PATH.
|
|
486
|
+
function detectEspIdf(env, targetVersion) {
|
|
487
|
+
const idfDir = findEspIdf(env)
|
|
488
|
+
if (idfDir) {
|
|
489
|
+
const installed = readInstalledEspIdfVersion(idfDir)
|
|
490
|
+
const detail = `${installed?.full || 'unknown version'} at ${idfDir}`
|
|
491
|
+
return { available: idfVersionMeetsTarget(installed, targetVersion), detail }
|
|
492
|
+
}
|
|
493
|
+
const legacyOutput = commandVersion('idf.py', ['--version'], env)
|
|
494
|
+
if (legacyOutput) {
|
|
495
|
+
const installed = extractIdfVersionFromText(legacyOutput)
|
|
496
|
+
const available = !installed || idfVersionMeetsTarget(installed, targetVersion)
|
|
497
|
+
return { available, detail: legacyOutput }
|
|
498
|
+
}
|
|
436
499
|
return { available: false, detail: '' }
|
|
437
500
|
}
|
|
438
501
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
502
|
+
// Nobody knows their board's USB serial by heart, so the wizard never asks
|
|
503
|
+
// for one. It asks whether the board is plugged in, reads the serial from the
|
|
504
|
+
// USB registry, and PINGs each port so the user picks by what the board says
|
|
505
|
+
// it is running rather than by a /dev name. A board that is not connected is
|
|
506
|
+
// registered without a serial; `gea boards discover --save` fills it in later.
|
|
507
|
+
async function selectUsbSerial(prompt, io, ctx, { message }) {
|
|
508
|
+
const env = io.env || process.env
|
|
509
|
+
io.stdout(message)
|
|
510
|
+
const connected = await confirm(prompt, { message: 'Is the board connected over USB right now?', defaultValue: true })
|
|
511
|
+
if (!connected) {
|
|
512
|
+
io.stdout('No USB serial recorded. Later, with the board plugged in: gea boards discover --save')
|
|
513
|
+
return ''
|
|
514
|
+
}
|
|
515
|
+
const known = loadBoardConfig(ctx)
|
|
516
|
+
const probe = io.probeSerialDevice || ((device) => probeSerialDevice(device, { env }))
|
|
517
|
+
while (true) {
|
|
518
|
+
const devices = detectSerialDevices({ env }).filter((device) => device.serial)
|
|
519
|
+
if (devices.length === 0) {
|
|
520
|
+
io.stdout('No USB board detected. Check the cable (some are power-only) and that the board is on.')
|
|
521
|
+
const retry = await confirm(prompt, { message: 'Retry detection?', defaultValue: true })
|
|
522
|
+
if (retry) continue
|
|
523
|
+
io.stdout('No USB serial recorded. Later, with the board plugged in: gea boards discover --save')
|
|
524
|
+
return ''
|
|
525
|
+
}
|
|
526
|
+
const results = await discoverBoards({ devices, boards: known, probe })
|
|
527
|
+
const describe = (result) => {
|
|
528
|
+
const bits = [result.label && result.label !== result.path ? `${result.label} on ${result.path}` : result.path, `serial ${result.serial}`]
|
|
529
|
+
if (result.responds) bits.push(result.app ? `running ${result.app}` : 'gea firmware')
|
|
530
|
+
if (result.alias) bits.push(`already registered as '${result.alias}'`)
|
|
531
|
+
return bits.join(', ')
|
|
532
|
+
}
|
|
533
|
+
if (results.length === 1) {
|
|
534
|
+
io.stdout(`Detected ${describe(results[0])}`)
|
|
535
|
+
return results[0].serial
|
|
536
|
+
}
|
|
537
|
+
const selected = await choose(prompt, {
|
|
538
|
+
message: 'Several USB devices are connected. Which one is this board?',
|
|
539
|
+
choices: [
|
|
540
|
+
...results.map((result, index) => ({ value: `device-${index}`, label: describe(result) })),
|
|
541
|
+
{ value: 'retry', label: 'Unplug the others and detect again' },
|
|
542
|
+
{ value: 'skip', label: 'Skip for now' }
|
|
543
|
+
],
|
|
544
|
+
defaultValue: 'device-0'
|
|
445
545
|
})
|
|
546
|
+
if (selected === 'skip') return ''
|
|
547
|
+
if (selected === 'retry') continue
|
|
548
|
+
return results[Number.parseInt(selected.slice('device-'.length), 10)].serial
|
|
446
549
|
}
|
|
447
|
-
|
|
448
|
-
const selected = await choose(prompt, {
|
|
449
|
-
message: 'Detected serial devices. Which board is connected?',
|
|
450
|
-
choices: [
|
|
451
|
-
...devices.map((device, index) => ({ value: `device-${index}`, label: formatSerialDevice(device) })),
|
|
452
|
-
{ value: 'manual', label: 'Enter stable USB serial manually' },
|
|
453
|
-
{ value: 'skip', label: 'Skip for now' }
|
|
454
|
-
],
|
|
455
|
-
defaultValue: 'device-0'
|
|
456
|
-
})
|
|
457
|
-
if (selected === 'skip') return ''
|
|
458
|
-
if (selected === 'manual') return ask(prompt, { message, defaultValue: '' })
|
|
459
|
-
const index = Number.parseInt(selected.slice('device-'.length), 10)
|
|
460
|
-
const device = devices[index]
|
|
461
|
-
return ask(prompt, {
|
|
462
|
-
message: `Stable USB serial for ${device.path}`,
|
|
463
|
-
defaultValue: device.serial || ''
|
|
464
|
-
})
|
|
465
550
|
}
|
|
466
551
|
|
|
467
552
|
// --global writes the alias to ~/.geastack/boards.json, --local to the
|