@geastack/cli 0.1.9 → 0.1.10
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 +20 -15
- package/docs/NPX-COMMANDS.md +26 -29
- package/docs/SPEC.md +17 -0
- package/package.json +2 -2
- package/src/chips.mjs +295 -0
- package/src/context.mjs +2 -2
- package/src/create-geastack.mjs +2 -2
- package/src/gea.mjs +7 -0
- package/src/setup-wizard.mjs +119 -240
package/README.md
CHANGED
|
@@ -73,20 +73,13 @@ flowchart TD
|
|
|
73
73
|
ota --> reviewKnown["Review board setup"]
|
|
74
74
|
reviewKnown --> writeKnown["Write .gea/boards.json"]
|
|
75
75
|
|
|
76
|
-
interactive -->|"Custom board
|
|
77
|
-
custom -->
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
peripherals --> connection
|
|
84
|
-
connection --> notes["Notes and datasheet links"]
|
|
85
|
-
notes --> reviewCustom["Review custom profile"]
|
|
86
|
-
reviewCustom --> writeProfile["Write .gea/boards/alias.json"]
|
|
87
|
-
writeProfile --> maybeAlias{"Base target selected?"}
|
|
88
|
-
maybeAlias -->|"Yes"| writeCustomAlias["Write alias to .gea/boards.json"]
|
|
89
|
-
maybeAlias -->|"No"| profileOnly["Profile only, not flash-ready yet"]
|
|
76
|
+
interactive -->|"Custom board target"| custom["Choose MCU and compatible chips"]
|
|
77
|
+
custom --> chipConfig["Ask interface and pin questions<br/>from @geastack/chips/catalog.json"]
|
|
78
|
+
chipConfig --> features["Optional microSD and launcher button"]
|
|
79
|
+
features --> connection["Detect initial USB connection"]
|
|
80
|
+
connection --> reviewCustom["Review native composition"]
|
|
81
|
+
reviewCustom --> writeProfile["Write .gea/targets/alias.json"]
|
|
82
|
+
writeProfile --> writeCustomAlias["Write alias to .gea/boards.json"]
|
|
90
83
|
|
|
91
84
|
interactive -->|"npm dependencies only"| npmInstall["Run npm install when package.json exists"]
|
|
92
85
|
interactive -->|"ESP-IDF toolchain only"| idfOnly["Install or check ESP-IDF v6.0.1"]
|
|
@@ -94,7 +87,6 @@ flowchart TD
|
|
|
94
87
|
writeKnown --> initialize["Initialize board target"]
|
|
95
88
|
writeCustomAlias --> initialize
|
|
96
89
|
initialize --> ready["Ready: npx gea flash --board alias --monitor"]
|
|
97
|
-
profileOnly --> done["Done"]
|
|
98
90
|
npmInstall --> done
|
|
99
91
|
idfOnly --> done
|
|
100
92
|
directIdf --> done
|
|
@@ -107,6 +99,19 @@ host bindings, and the other native packages from npm. A project can use a
|
|
|
107
99
|
local CLI with `npx gea` or a global installation with `gea`; neither command
|
|
108
100
|
depends on a GeaStack source checkout.
|
|
109
101
|
|
|
102
|
+
Custom boards remain editable after setup:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
gea chips list
|
|
106
|
+
gea chips info co5300
|
|
107
|
+
gea chips add co5300 ft3168 --board my-board
|
|
108
|
+
gea chips remove ft3168 --board my-board
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
These commands update the app-local target definition. They do not copy native
|
|
112
|
+
sources into the application; the target adapter compiles the selected drivers
|
|
113
|
+
directly from the installed `@geastack/chips` package.
|
|
114
|
+
|
|
110
115
|
## Development
|
|
111
116
|
|
|
112
117
|
```sh
|
package/docs/NPX-COMMANDS.md
CHANGED
|
@@ -23,7 +23,7 @@ Use these release packages instead:
|
|
|
23
23
|
the `create-geastack` bin.
|
|
24
24
|
- `@geastack/cli`: scoped private Gea CLI package exposing the `gea` bin.
|
|
25
25
|
|
|
26
|
-
`create-geastack` should add `@geastack/cli` as a
|
|
26
|
+
`create-geastack` should add `@geastack/cli` as a dependency in generated
|
|
27
27
|
projects and install dependencies by default in interactive terminals. After
|
|
28
28
|
that, inside the project, `npx gea ...` works because npm finds the local
|
|
29
29
|
`node_modules/.bin/gea` binary. It does not require an npm package named `gea`.
|
|
@@ -31,7 +31,7 @@ that, inside the project, `npx gea ...` works because npm finds the local
|
|
|
31
31
|
If someone is wiring an existing app by hand, install the package first:
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
npm install
|
|
34
|
+
npm install @geastack/cli
|
|
35
35
|
npx gea setup
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -89,7 +89,7 @@ packages are scoped and configured for restricted npmjs publication.
|
|
|
89
89
|
`gea setup` with no `--board` or `--target` opens a guided setup flow:
|
|
90
90
|
|
|
91
91
|
- known supported board, with descriptions for each board;
|
|
92
|
-
- custom board
|
|
92
|
+
- custom board target composed from the installed chip catalog;
|
|
93
93
|
- npm dependency check/install only;
|
|
94
94
|
- ESP-IDF toolchain check/install only.
|
|
95
95
|
|
|
@@ -102,38 +102,35 @@ initializes the selected board target so the next command can be
|
|
|
102
102
|
.gea/boards.json
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
Custom-board setup
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
sensors, power, transport, and notes.
|
|
109
|
-
- `Fast profile`: core board identity, display/touch, transport, and inferred
|
|
110
|
-
defaults for optional peripherals.
|
|
111
|
-
|
|
112
|
-
Both paths show a review screen before writing a profile under:
|
|
105
|
+
Custom-board setup reads `@geastack/chips/catalog.json`, offers only drivers
|
|
106
|
+
with a compatible platform binding, and asks the configuration questions
|
|
107
|
+
declared by each selected driver. It shows a review before writing the target:
|
|
113
108
|
|
|
114
109
|
```text
|
|
115
|
-
.gea/
|
|
110
|
+
.gea/targets/<alias>.json
|
|
116
111
|
```
|
|
117
112
|
|
|
118
|
-
The
|
|
113
|
+
The target captures:
|
|
119
114
|
|
|
120
115
|
- MCU / SoC;
|
|
121
|
-
-
|
|
122
|
-
- display
|
|
123
|
-
- touch controller and
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
116
|
+
- the generic platform base and adapter;
|
|
117
|
+
- display controller, interface, dimensions, bus, and pins;
|
|
118
|
+
- touch controller, bus, reset, and interrupt pins;
|
|
119
|
+
- power, IMU, and audio drivers;
|
|
120
|
+
- shared buses and audio pins;
|
|
121
|
+
- optional storage and launcher-button pins;
|
|
122
|
+
- the initial USB serial connection.
|
|
123
|
+
|
|
124
|
+
The wizard also writes `.gea/boards.json`, where the physical board alias points
|
|
125
|
+
to that definition. A partial composition is saved but is not described as
|
|
126
|
+
flash-ready. It can be completed later with:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
npx gea chips list
|
|
130
|
+
npx gea chips info co5300
|
|
131
|
+
npx gea chips add co5300 --board my-board
|
|
132
|
+
npx gea chips remove co5300 --board my-board
|
|
133
|
+
```
|
|
137
134
|
|
|
138
135
|
ESP-IDF setup is available as:
|
|
139
136
|
|
package/docs/SPEC.md
CHANGED
|
@@ -87,6 +87,23 @@ The CLI should pass through board aliases from the active board config. A projec
|
|
|
87
87
|
uses its own `.gea/boards.json`; otherwise the CLI reads the board catalog shipped
|
|
88
88
|
by the installed `@geastack/targets` package.
|
|
89
89
|
|
|
90
|
+
### `gea chips`
|
|
91
|
+
|
|
92
|
+
Inspects the installed `@geastack/chips` catalog and composes a custom board
|
|
93
|
+
definition without copying native driver source into the app.
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
gea chips list
|
|
97
|
+
gea chips info co5300
|
|
98
|
+
gea chips add co5300 ft3168 --board my-board
|
|
99
|
+
gea chips remove ft3168 --board my-board
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`add` asks only the interface and pin questions declared by each chip. Automation
|
|
103
|
+
can answer them with repeated `--set chip.path=value` options. The command rejects
|
|
104
|
+
chips without a binding for the board adapter and MCU before changing the target
|
|
105
|
+
definition.
|
|
106
|
+
|
|
90
107
|
### `gea monitor`
|
|
91
108
|
|
|
92
109
|
Starts a log monitor for a configured board or target.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geastack/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Command-line front door for GeaStack apps, targets, and local toolchains.",
|
|
6
6
|
"publishConfig": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@geastack/core": "0.1.4",
|
|
31
|
-
"@geastack/targets": "0.1.
|
|
31
|
+
"@geastack/targets": "0.1.5"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"check": "node --check bin/gea.mjs && node --check bin/create-geastack.mjs && find src test packages -name '*.mjs' -print0 | xargs -0 -n1 node --check",
|
package/src/chips.mjs
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { flag, option } from './args.mjs'
|
|
4
|
+
import { ExitCode, fail } from './errors.mjs'
|
|
5
|
+
import { exists, readJson, writeJson } from './fs-utils.mjs'
|
|
6
|
+
import { ask, choose, createPrompt } from './prompts.mjs'
|
|
7
|
+
|
|
8
|
+
export async function runChips(ctx, parsed, rest, io = {}) {
|
|
9
|
+
const action = rest[0] || 'list'
|
|
10
|
+
const chipIds = rest.slice(1)
|
|
11
|
+
const stdout = io.stdout || console.log
|
|
12
|
+
const catalog = loadChipCatalog(ctx)
|
|
13
|
+
|
|
14
|
+
if (action === 'list') {
|
|
15
|
+
const entries = Object.entries(catalog).sort((a, b) => a[0].localeCompare(b[0]))
|
|
16
|
+
if (flag(parsed, 'json')) stdout(JSON.stringify(Object.fromEntries(entries), null, 2))
|
|
17
|
+
else {
|
|
18
|
+
for (const [id, chip] of entries) {
|
|
19
|
+
const adapters = Object.keys(chip.adapters || {})
|
|
20
|
+
stdout(`${id}\t${chip.category}\t${chip.label}${adapters.length ? `\t${adapters.join(',')}` : '\tunbound'}`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (action === 'info') {
|
|
27
|
+
if (chipIds.length !== 1) fail('chips info requires one chip id.', ExitCode.usage)
|
|
28
|
+
const chip = catalog[chipIds[0]]
|
|
29
|
+
if (!chip) failUnknownChip(chipIds[0], catalog)
|
|
30
|
+
if (flag(parsed, 'json')) stdout(JSON.stringify({ id: chipIds[0], ...chip }, null, 2))
|
|
31
|
+
else {
|
|
32
|
+
stdout(`${chipIds[0]}: ${chip.label}`)
|
|
33
|
+
stdout(`Category: ${chip.category}`)
|
|
34
|
+
stdout(`Interfaces: ${(chip.interfaces || []).join(', ') || 'none'}`)
|
|
35
|
+
stdout(`Adapters: ${Object.keys(chip.adapters || {}).join(', ') || 'no platform binding yet'}`)
|
|
36
|
+
for (const field of chip.configuration || []) stdout(`Configure: ${field.path} (${field.label})`)
|
|
37
|
+
}
|
|
38
|
+
return 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (action !== 'add' && action !== 'remove') {
|
|
42
|
+
fail(`Unknown chips action '${action}'. Expected list, info, add, or remove.`, ExitCode.usage)
|
|
43
|
+
}
|
|
44
|
+
if (chipIds.length === 0) fail(`chips ${action} requires at least one chip id.`, ExitCode.usage)
|
|
45
|
+
|
|
46
|
+
const location = resolveTargetDefinition(ctx, parsed)
|
|
47
|
+
const definition = readJson(location.definitionPath)
|
|
48
|
+
definition.chips ||= {}
|
|
49
|
+
|
|
50
|
+
if (action === 'remove') {
|
|
51
|
+
for (const id of chipIds) {
|
|
52
|
+
const role = Object.keys(definition.chips).find((key) => selectedDriver(definition.chips[key]) === id)
|
|
53
|
+
if (!role) fail(`Chip '${id}' is not selected by board '${location.boardName}'.`, ExitCode.usage)
|
|
54
|
+
delete definition.chips[role]
|
|
55
|
+
stdout(`Removed ${id} from ${location.boardName}.`)
|
|
56
|
+
}
|
|
57
|
+
writeJson(location.definitionPath, definition)
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const prompt = createPrompt(io)
|
|
62
|
+
try {
|
|
63
|
+
const provided = parseSetOptions(parsed)
|
|
64
|
+
for (const id of chipIds) {
|
|
65
|
+
const descriptor = catalog[id]
|
|
66
|
+
if (!descriptor) failUnknownChip(id, catalog)
|
|
67
|
+
const role = await configureChipSelection({
|
|
68
|
+
definition,
|
|
69
|
+
id,
|
|
70
|
+
descriptor,
|
|
71
|
+
provided,
|
|
72
|
+
prompt,
|
|
73
|
+
io,
|
|
74
|
+
replace: flag(parsed, 'replace'),
|
|
75
|
+
boardName: location.boardName
|
|
76
|
+
})
|
|
77
|
+
stdout(`Added ${id} as ${role} on ${location.boardName}.`)
|
|
78
|
+
}
|
|
79
|
+
if (provided.size > 0) {
|
|
80
|
+
fail(`Unused --set values: ${[...provided.keys()].join(', ')}`, ExitCode.usage)
|
|
81
|
+
}
|
|
82
|
+
validateGpioAssignments(definition)
|
|
83
|
+
writeJson(location.definitionPath, definition)
|
|
84
|
+
stdout(`Updated ${location.definitionPath}`)
|
|
85
|
+
return 0
|
|
86
|
+
} finally {
|
|
87
|
+
await prompt.close()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function configureChipSelection({ definition, id, descriptor, provided = new Map(), prompt, io = {}, replace = false, boardName = definition.id }) {
|
|
92
|
+
definition.chips ||= {}
|
|
93
|
+
const adapter = definition.adapter || 'esp32-idf'
|
|
94
|
+
const binding = descriptor.adapters?.[adapter]
|
|
95
|
+
if (!binding) fail(`Chip '${id}' has no ${adapter} binding. 'gea chips info ${id}' shows its current support.`, ExitCode.targetUnavailable)
|
|
96
|
+
if (Array.isArray(binding.mcus) && !binding.mcus.includes(definition.mcu)) {
|
|
97
|
+
fail(`Chip '${id}' does not support MCU '${definition.mcu}' through ${adapter}.`, ExitCode.targetUnavailable)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const role = descriptor.category
|
|
101
|
+
const previous = definition.chips[role]
|
|
102
|
+
if (previous && selectedDriver(previous) !== id && !replace) {
|
|
103
|
+
fail(`Board '${boardName}' already uses '${selectedDriver(previous)}' as ${role}. Pass --replace to change it.`, ExitCode.usage)
|
|
104
|
+
}
|
|
105
|
+
const selection = previous && selectedDriver(previous) === id ? structuredClone(previous) : { driver: id }
|
|
106
|
+
selection.driver = id
|
|
107
|
+
delete selection.controller
|
|
108
|
+
selection.interface = await configuredInterface({ id, descriptor, selection, provided, prompt, io })
|
|
109
|
+
if (selection.interface === 'i2c') {
|
|
110
|
+
definition.buses ||= {}
|
|
111
|
+
definition.buses.i2c ||= {}
|
|
112
|
+
await configureFields({
|
|
113
|
+
id: 'i2c',
|
|
114
|
+
target: definition.buses.i2c,
|
|
115
|
+
fields: [
|
|
116
|
+
{ path: 'sda', label: 'Shared I2C SDA pin', type: 'pin' },
|
|
117
|
+
{ path: 'scl', label: 'Shared I2C SCL pin', type: 'pin' }
|
|
118
|
+
],
|
|
119
|
+
provided,
|
|
120
|
+
prompt,
|
|
121
|
+
io
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
await configureFields({ id, target: selection, fields: descriptor.configuration || [], provided, prompt, io })
|
|
125
|
+
definition.chips[role] = selection
|
|
126
|
+
return role
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function loadChipCatalog(ctx) {
|
|
130
|
+
const catalogPath = path.join(ctx.chipsPackageDir || '', 'catalog.json')
|
|
131
|
+
if (!ctx.chipsPackageDir || !exists(catalogPath)) {
|
|
132
|
+
fail(`Missing @geastack/chips catalog: ${catalogPath || '@geastack/chips/catalog.json'}`, ExitCode.missingDependency)
|
|
133
|
+
}
|
|
134
|
+
const catalog = readJson(catalogPath)
|
|
135
|
+
if (catalog.schemaVersion !== 1 || !catalog.chips || typeof catalog.chips !== 'object') {
|
|
136
|
+
fail(`Unsupported @geastack/chips catalog schema in ${catalogPath}.`, ExitCode.missingDependency)
|
|
137
|
+
}
|
|
138
|
+
return catalog.chips
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resolveTargetDefinition(ctx, parsed) {
|
|
142
|
+
const configPath = option(parsed, 'boards-config')
|
|
143
|
+
? path.resolve(ctx.cwd, option(parsed, 'boards-config'))
|
|
144
|
+
: ctx.projectBoardsConfig
|
|
145
|
+
if (!configPath || !exists(configPath)) fail('No project .gea/boards.json was found. Run gea setup and create a custom board first.', ExitCode.usage)
|
|
146
|
+
const boards = readJson(configPath)
|
|
147
|
+
const requested = option(parsed, 'board', '')
|
|
148
|
+
const customBoards = Object.entries(boards).filter(([, entry]) => typeof entry?.targetDefinition === 'string' && entry.targetDefinition)
|
|
149
|
+
const boardName = requested || (customBoards.length === 1 ? customBoards[0][0] : '')
|
|
150
|
+
if (!boardName) fail('Select a custom board with --board <alias>.', ExitCode.usage)
|
|
151
|
+
const board = boards[boardName]
|
|
152
|
+
if (!board) fail(`Unknown board '${boardName}' in ${configPath}.`, ExitCode.usage)
|
|
153
|
+
if (!board.targetDefinition) fail(`Board '${boardName}' is a preset and has no editable target definition.`, ExitCode.usage)
|
|
154
|
+
const definitionPath = path.resolve(path.dirname(configPath), board.targetDefinition)
|
|
155
|
+
if (!exists(definitionPath)) fail(`Target definition for '${boardName}' does not exist: ${definitionPath}`, ExitCode.usage)
|
|
156
|
+
return { boardName, board, configPath, definitionPath }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function validateGpioAssignments(definition) {
|
|
160
|
+
const pins = []
|
|
161
|
+
collectGpioAssignments(definition, [], false, pins)
|
|
162
|
+
const used = new Map()
|
|
163
|
+
for (const [label, pin] of pins) {
|
|
164
|
+
const previous = used.get(pin)
|
|
165
|
+
if (previous) fail(`GPIO ${pin} is assigned to both ${previous} and ${label}.`, ExitCode.usage)
|
|
166
|
+
used.set(pin, label)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function collectGpioAssignments(value, pathParts, pinMap, out) {
|
|
171
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return
|
|
172
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
173
|
+
const nextPath = [...pathParts, key]
|
|
174
|
+
const nextPinMap = pinMap || key === 'pins' || pathParts[0] === 'buses'
|
|
175
|
+
if (typeof entry === 'number' && entry >= 0 && (nextPinMap || key === 'pin')) {
|
|
176
|
+
out.push([nextPath.join('.'), entry])
|
|
177
|
+
} else if (entry && typeof entry === 'object') {
|
|
178
|
+
collectGpioAssignments(entry, nextPath, nextPinMap, out)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function configuredInterface({ id, descriptor, selection, provided, prompt, io }) {
|
|
184
|
+
const interfaces = Array.isArray(descriptor.interfaces) ? descriptor.interfaces : []
|
|
185
|
+
const supplied = provided.get(`${id}.interface`)
|
|
186
|
+
if (supplied !== undefined) {
|
|
187
|
+
provided.delete(`${id}.interface`)
|
|
188
|
+
if (!interfaces.includes(supplied)) fail(`Chip '${id}' does not support interface '${supplied}'.`, ExitCode.usage)
|
|
189
|
+
return supplied
|
|
190
|
+
}
|
|
191
|
+
if (selection.interface && interfaces.includes(selection.interface)) return selection.interface
|
|
192
|
+
if (interfaces.length === 1) return interfaces[0]
|
|
193
|
+
if (!canAnswer(prompt, io)) fail(`Set ${id}.interface with --set ${id}.interface=<value>.`, ExitCode.usage)
|
|
194
|
+
return choose(prompt, {
|
|
195
|
+
message: `${descriptor.label} interface`,
|
|
196
|
+
choices: interfaces.map((value) => ({ value, label: value.toUpperCase() })),
|
|
197
|
+
defaultValue: interfaces[0]
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function configureFields({ id, target, fields, provided, prompt, io }) {
|
|
202
|
+
for (const field of fields) {
|
|
203
|
+
const supplied = provided.get(`${id}.${field.path}`)
|
|
204
|
+
if (supplied !== undefined) {
|
|
205
|
+
provided.delete(`${id}.${field.path}`)
|
|
206
|
+
const error = validateFieldValue(supplied, field)
|
|
207
|
+
if (error) fail(`Invalid ${id}.${field.path}: ${error}.`, ExitCode.usage)
|
|
208
|
+
setPath(target, field.path, parseFieldValue(supplied, field))
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
const current = getPath(target, field.path)
|
|
212
|
+
if (current !== undefined && current !== null) continue
|
|
213
|
+
if (!canAnswer(prompt, io)) {
|
|
214
|
+
if (field.default !== undefined) {
|
|
215
|
+
setPath(target, field.path, field.default)
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
if (field.optional) {
|
|
219
|
+
setPath(target, field.path, null)
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
fail(`Missing ${id}.${field.path}. Pass --set ${id}.${field.path}=<value> or run interactively.`, ExitCode.usage)
|
|
223
|
+
}
|
|
224
|
+
if (field.type === 'choice') {
|
|
225
|
+
const value = await choose(prompt, {
|
|
226
|
+
message: field.label,
|
|
227
|
+
choices: field.values.map((entry) => ({ value: entry, label: entry })),
|
|
228
|
+
defaultValue: field.default || field.values[0]
|
|
229
|
+
})
|
|
230
|
+
setPath(target, field.path, value)
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
const value = await ask(prompt, {
|
|
234
|
+
message: field.optional ? `${field.label} (blank for none)` : field.label,
|
|
235
|
+
defaultValue: field.default === undefined ? '' : String(field.default),
|
|
236
|
+
validate: (entry) => validateFieldValue(entry, field)
|
|
237
|
+
})
|
|
238
|
+
setPath(target, field.path, field.optional && value === '' ? null : parseFieldValue(value, field))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function parseSetOptions(parsed) {
|
|
243
|
+
const raw = parsed.options.set
|
|
244
|
+
const entries = raw === undefined ? [] : Array.isArray(raw) ? raw : [raw]
|
|
245
|
+
const out = new Map()
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
const index = String(entry).indexOf('=')
|
|
248
|
+
if (index <= 0) fail(`Invalid --set '${entry}'. Expected --set chip.path=value.`, ExitCode.usage)
|
|
249
|
+
out.set(String(entry).slice(0, index), String(entry).slice(index + 1))
|
|
250
|
+
}
|
|
251
|
+
return out
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseFieldValue(value, field) {
|
|
255
|
+
if (field.optional && (value === '' || value === 'none' || value === 'null')) return null
|
|
256
|
+
if (field.type === 'integer' || field.type === 'pin') return Number.parseInt(value, 10)
|
|
257
|
+
return String(value)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function validateFieldValue(value, field) {
|
|
261
|
+
if (field.optional && value === '') return ''
|
|
262
|
+
if (field.type === 'choice') return field.values.includes(value) ? '' : `choose one of ${field.values.join(', ')}`
|
|
263
|
+
if (field.type !== 'integer' && field.type !== 'pin') return ''
|
|
264
|
+
if (!/^-?\d+$/.test(String(value))) return 'enter an integer'
|
|
265
|
+
const number = Number(value)
|
|
266
|
+
const min = field.type === 'pin' ? 0 : field.min
|
|
267
|
+
const max = field.type === 'pin' ? 48 : field.max
|
|
268
|
+
if (min !== undefined && number < min) return `minimum is ${min}`
|
|
269
|
+
if (max !== undefined && number > max) return `maximum is ${max}`
|
|
270
|
+
return ''
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getPath(value, dottedPath) {
|
|
274
|
+
return dottedPath.split('.').reduce((current, part) => current?.[part], value)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function setPath(value, dottedPath, next) {
|
|
278
|
+
const parts = dottedPath.split('.')
|
|
279
|
+
let current = value
|
|
280
|
+
for (const part of parts.slice(0, -1)) current = current[part] ||= {}
|
|
281
|
+
current[parts.at(-1)] = next
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function selectedDriver(selection) {
|
|
285
|
+
return String(selection?.driver || selection?.controller || '')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function failUnknownChip(id, catalog) {
|
|
289
|
+
const available = Object.keys(catalog).sort().join(', ')
|
|
290
|
+
fail(`Unknown chip '${id}'. Available chips: ${available}`, ExitCode.usage)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function canAnswer(prompt, io) {
|
|
294
|
+
return Boolean(io.prompt || io.stdin?.isTTY || (!io.stdin && process.stdin.isTTY))
|
|
295
|
+
}
|
package/src/context.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export function createContext(parsed, _env = process.env, cwd = process.cwd()) {
|
|
|
13
13
|
const absoluteCwd = path.resolve(cwd)
|
|
14
14
|
const projectRoot = findNodeProjectRoot(absoluteCwd) || absoluteCwd
|
|
15
15
|
const initialAnchors = [projectRoot, cliPackageRoot]
|
|
16
|
-
const targetsRoot = resolveInstalledPackageDir('@geastack/targets', initialAnchors)
|
|
16
|
+
const targetsRoot = _env.GEA_TARGETS_ROOT || resolveInstalledPackageDir('@geastack/targets', initialAnchors)
|
|
17
17
|
const corePackageDir = resolveInstalledPackageDir('@geastack/core', initialAnchors)
|
|
18
18
|
const packageAnchors = [projectRoot, cliPackageRoot, targetsRoot, corePackageDir].filter(Boolean)
|
|
19
19
|
const compilerPackageDir = resolveInstalledPackageDir('@geastack/compiler', packageAnchors)
|
|
@@ -28,7 +28,7 @@ export function createContext(parsed, _env = process.env, cwd = process.cwd()) {
|
|
|
28
28
|
cliPackageRoot,
|
|
29
29
|
compilerPackageDir,
|
|
30
30
|
corePackageDir,
|
|
31
|
-
chipsPackageDir: resolveInstalledPackageDir('@geastack/chips', packageAnchors),
|
|
31
|
+
chipsPackageDir: _env.GEA_CHIPS_DIR || resolveInstalledPackageDir('@geastack/chips', packageAnchors),
|
|
32
32
|
elementsPackageDir: resolveInstalledPackageDir('@geastack/elements', packageAnchors),
|
|
33
33
|
enginePackageDir: resolveInstalledPackageDir('@geastack/engine', packageAnchors),
|
|
34
34
|
geaosPackageDir: resolveInstalledPackageDir('@geastack/geaos', packageAnchors),
|
package/src/create-geastack.mjs
CHANGED
|
@@ -308,11 +308,11 @@ function packageJson({ appId, displayName, targets, entry, coreDependency, cliDe
|
|
|
308
308
|
dependencies: {
|
|
309
309
|
...(sourcePackage.dependencies || {}),
|
|
310
310
|
'@geajs/core': sourcePackage.dependencies?.['@geajs/core'] || '^1.3.0',
|
|
311
|
-
'@geastack/core': coreDependency
|
|
311
|
+
'@geastack/core': coreDependency,
|
|
312
|
+
'@geastack/cli': cliDependency
|
|
312
313
|
},
|
|
313
314
|
devDependencies: {
|
|
314
315
|
...(sourcePackage.devDependencies || {}),
|
|
315
|
-
'@geastack/cli': cliDependency,
|
|
316
316
|
typescript: sourcePackage.devDependencies?.typescript || 'latest',
|
|
317
317
|
...(targets.web ? { vite: sourcePackage.devDependencies?.vite || 'latest' } : {})
|
|
318
318
|
},
|
package/src/gea.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs'
|
|
|
3
3
|
|
|
4
4
|
import { flag, option, parseArgs } from './args.mjs'
|
|
5
5
|
import { runCreateGeastack } from './create-geastack.mjs'
|
|
6
|
+
import { runChips } from './chips.mjs'
|
|
6
7
|
import { createChildEnv, createContext } from './context.mjs'
|
|
7
8
|
import { ExitCode, fail } from './errors.mjs'
|
|
8
9
|
import { exists, readJson } from './fs-utils.mjs'
|
|
@@ -59,6 +60,8 @@ export async function runGea(argv, io = {}) {
|
|
|
59
60
|
return build(ctx, parsed, rest, { stdout, env })
|
|
60
61
|
case 'setup':
|
|
61
62
|
return setup(ctx, parsed, { stdout, env, stdin, output, prompt })
|
|
63
|
+
case 'chips':
|
|
64
|
+
return runChips(ctx, parsed, rest, { stdout, env, stdin, output, prompt })
|
|
62
65
|
case 'flash':
|
|
63
66
|
return flash(ctx, parsed, rest, { stdout, env })
|
|
64
67
|
case 'monitor':
|
|
@@ -432,6 +435,10 @@ function usage() {
|
|
|
432
435
|
gea create <name> [--starter counter|empty|example] [--targets <list>]
|
|
433
436
|
gea doctor [--strict] [--json]
|
|
434
437
|
gea setup --board <alias>
|
|
438
|
+
gea chips list [--json]
|
|
439
|
+
gea chips info <chip> [--json]
|
|
440
|
+
gea chips add <chip...> [--board <alias>] [--set chip.path=value]
|
|
441
|
+
gea chips remove <chip...> [--board <alias>]
|
|
435
442
|
gea dev [app] [--target web] [--port 5181]
|
|
436
443
|
gea build [app] [--target web|macos|ios|android|<target>] [--board <alias>] [--output-tag <tag>]
|
|
437
444
|
gea flash [app] --board <alias> [--monitor] [--port auto]
|
package/src/setup-wizard.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import os from 'node:os'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
5
|
import { knownBoards } from './board-catalog.mjs'
|
|
6
|
+
import { configureChipSelection, loadChipCatalog, validateGpioAssignments } from './chips.mjs'
|
|
6
7
|
import { flag, option } from './args.mjs'
|
|
7
8
|
import { createChildEnv } from './context.mjs'
|
|
8
9
|
import { ExitCode, fail } from './errors.mjs'
|
|
@@ -143,7 +144,7 @@ async function setupKnownBoard(ctx, parsed, io, prompt) {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
async function setupCustomBoard(ctx, parsed, io, prompt) {
|
|
146
|
-
renderStep(io, 'Board', ['
|
|
147
|
+
renderStep(io, 'Board', ['Create an app-local hardware target from the installed chip catalog.'])
|
|
147
148
|
const alias = await ask(prompt, {
|
|
148
149
|
message: 'Custom board alias',
|
|
149
150
|
defaultValue: 'custom-board',
|
|
@@ -151,230 +152,119 @@ async function setupCustomBoard(ctx, parsed, io, prompt) {
|
|
|
151
152
|
})
|
|
152
153
|
const mcu = await choose(prompt, {
|
|
153
154
|
message: 'MCU / SoC',
|
|
154
|
-
choices: [
|
|
155
|
-
{ value: 'esp32-s3', label: 'ESP32-S3' },
|
|
156
|
-
{ value: 'esp32-p4', label: 'ESP32-P4' },
|
|
157
|
-
{ value: 'esp32-c6', label: 'ESP32-C6' },
|
|
158
|
-
{ value: 'esp32', label: 'ESP32' },
|
|
159
|
-
{ value: 'other', label: 'Other / not listed' }
|
|
160
|
-
],
|
|
161
|
-
defaultValue: 'esp32-s3'
|
|
162
|
-
})
|
|
163
|
-
const mcuName = mcu === 'other' ? await ask(prompt, { message: 'MCU / SoC name', defaultValue: '' }) : mcu
|
|
164
|
-
const baseTarget = await choose(prompt, {
|
|
165
|
-
message: 'Closest existing target to start from',
|
|
166
|
-
choices: [
|
|
167
|
-
{ value: '', label: 'None yet, generate profile only' },
|
|
168
|
-
...knownBoards.map((board) => ({ value: board.target, label: `${board.label} (${board.target})` }))
|
|
169
|
-
],
|
|
170
|
-
defaultValue: ''
|
|
171
|
-
})
|
|
172
|
-
const depth = await choose(prompt, {
|
|
173
|
-
message: 'How much hardware detail do you want to enter?',
|
|
174
155
|
choices: [
|
|
175
156
|
{
|
|
176
|
-
value: '
|
|
177
|
-
label: '
|
|
178
|
-
description: '
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
value: 'quick',
|
|
182
|
-
label: 'Fast profile',
|
|
183
|
-
description: 'Core board, display/touch, transport, and sensible defaults for everything else.'
|
|
157
|
+
value: 'esp32s3',
|
|
158
|
+
label: 'ESP32-S3',
|
|
159
|
+
description: 'Composable ESP-IDF target with display, touch, power, IMU, audio, WiFi, BLE, and OTA support.'
|
|
184
160
|
}
|
|
185
161
|
],
|
|
186
|
-
defaultValue: '
|
|
162
|
+
defaultValue: 'esp32s3'
|
|
187
163
|
})
|
|
188
164
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
]
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
{ value: 'i2c', label: 'I2C' },
|
|
222
|
-
{ value: 'spi', label: 'SPI' },
|
|
223
|
-
{ value: 'gpio', label: 'GPIO buttons/interrupts' },
|
|
224
|
-
{ value: 'other', label: 'Other' }
|
|
225
|
-
],
|
|
226
|
-
defaultValue: 'i2c'
|
|
227
|
-
}) : ''
|
|
228
|
-
|
|
229
|
-
const defaults = defaultCustomPeripherals(mcuName)
|
|
230
|
-
let wifi = defaults.wifi
|
|
231
|
-
let ble = defaults.ble
|
|
232
|
-
let gpsModule = ''
|
|
233
|
-
let gpsInterface = ''
|
|
234
|
-
let audioCodec = ''
|
|
235
|
-
let audioOutput = ''
|
|
236
|
-
let audioInput = ''
|
|
237
|
-
let storage = defaults.storage
|
|
238
|
-
let sensors = ''
|
|
239
|
-
let power = 'USB'
|
|
240
|
-
|
|
241
|
-
if (depth === 'full') {
|
|
242
|
-
renderStep(io, 'Peripherals', ['Add wireless, location, audio, storage, sensors, and power details.'])
|
|
243
|
-
wifi = await choose(prompt, {
|
|
244
|
-
message: 'WiFi',
|
|
165
|
+
const definition = {
|
|
166
|
+
id: alias,
|
|
167
|
+
extends: 'esp32-s3',
|
|
168
|
+
adapter: 'esp32-idf',
|
|
169
|
+
mcu,
|
|
170
|
+
buses: {},
|
|
171
|
+
chips: {},
|
|
172
|
+
storage: {
|
|
173
|
+
microSD: {
|
|
174
|
+
interface: 'sdmmc-1bit',
|
|
175
|
+
pins: { clk: null, cmd: null, data0: null }
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
controls: {
|
|
179
|
+
launcherButton: { pin: null, activeLevel: 0 }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const catalog = loadChipCatalog(ctx)
|
|
184
|
+
const roles = [
|
|
185
|
+
['display', 'Display controller'],
|
|
186
|
+
['touch', 'Touch controller'],
|
|
187
|
+
['power', 'Power-management controller'],
|
|
188
|
+
['imu', 'Inertial measurement unit'],
|
|
189
|
+
['audio', 'Audio codec']
|
|
190
|
+
]
|
|
191
|
+
renderStep(io, 'Chips', ['Choose each controller from @geastack/chips. Pin questions come from the installed catalog.'])
|
|
192
|
+
for (const [role, label] of roles) {
|
|
193
|
+
const compatible = Object.entries(catalog)
|
|
194
|
+
.filter(([, chip]) => chip.category === role && chip.adapters?.['esp32-idf']?.mcus?.includes(mcu))
|
|
195
|
+
const selected = await choose(prompt, {
|
|
196
|
+
message: label,
|
|
245
197
|
choices: [
|
|
246
|
-
{ value:
|
|
247
|
-
{ value: '
|
|
248
|
-
{ value: 'none', label: 'None' }
|
|
198
|
+
...compatible.map(([id, chip]) => ({ value: id, label: `${id} — ${chip.label}` })),
|
|
199
|
+
{ value: '', label: 'None / configure later' }
|
|
249
200
|
],
|
|
250
|
-
defaultValue:
|
|
201
|
+
defaultValue: compatible[0]?.[0] || ''
|
|
251
202
|
})
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
203
|
+
if (!selected) continue
|
|
204
|
+
await configureChipSelection({
|
|
205
|
+
definition,
|
|
206
|
+
id: selected,
|
|
207
|
+
descriptor: catalog[selected],
|
|
208
|
+
prompt,
|
|
209
|
+
io,
|
|
210
|
+
replace: true,
|
|
211
|
+
boardName: alias
|
|
260
212
|
})
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
audioOutput = audioCodec ? await choose(prompt, {
|
|
274
|
-
message: 'Audio output',
|
|
275
|
-
choices: [
|
|
276
|
-
{ value: 'i2s-speaker', label: 'I2S speaker/output' },
|
|
277
|
-
{ value: 'dac', label: 'DAC output' },
|
|
278
|
-
{ value: 'pwm', label: 'PWM/buzzer' },
|
|
279
|
-
{ value: 'other', label: 'Other' }
|
|
280
|
-
],
|
|
281
|
-
defaultValue: 'i2s-speaker'
|
|
282
|
-
}) : ''
|
|
283
|
-
audioInput = audioCodec ? await choose(prompt, {
|
|
284
|
-
message: 'Audio input',
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
renderStep(io, 'Board features', ['Configure physical features that are not separate chip drivers.'])
|
|
216
|
+
if (await confirm(prompt, { message: 'Does this board expose a microSD slot?', defaultValue: false })) {
|
|
217
|
+
definition.storage.microSD.pins.clk = await askPin(prompt, 'microSD clock pin')
|
|
218
|
+
definition.storage.microSD.pins.cmd = await askPin(prompt, 'microSD command pin')
|
|
219
|
+
definition.storage.microSD.pins.data0 = await askPin(prompt, 'microSD data 0 pin')
|
|
220
|
+
}
|
|
221
|
+
if (await confirm(prompt, { message: 'Use a hardware button to return to the launcher?', defaultValue: false })) {
|
|
222
|
+
definition.controls.launcherButton.pin = await askPin(prompt, 'Launcher button pin')
|
|
223
|
+
definition.controls.launcherButton.activeLevel = Number(await choose(prompt, {
|
|
224
|
+
message: 'Launcher button active level',
|
|
285
225
|
choices: [
|
|
286
|
-
{ value: '
|
|
287
|
-
{ value: '
|
|
288
|
-
{ value: 'pdm-mic', label: 'PDM microphone' },
|
|
289
|
-
{ value: 'analog-mic', label: 'Analog microphone' },
|
|
290
|
-
{ value: 'other', label: 'Other' }
|
|
226
|
+
{ value: '0', label: 'Active low' },
|
|
227
|
+
{ value: '1', label: 'Active high' }
|
|
291
228
|
],
|
|
292
|
-
defaultValue: '
|
|
293
|
-
})
|
|
294
|
-
storage = await ask(prompt, { message: 'Storage chips/features, comma-separated', defaultValue: defaults.storage })
|
|
295
|
-
sensors = await ask(prompt, { message: 'Sensors, comma-separated (IMU, light, temp, etc.)', defaultValue: '' })
|
|
296
|
-
power = await ask(prompt, { message: 'Power path (USB, battery charger, PMIC, etc.)', defaultValue: 'USB' })
|
|
297
|
-
} else {
|
|
298
|
-
renderStep(io, 'Peripherals', [
|
|
299
|
-
`Using defaults: WiFi ${wifi}, BLE ${ble}, storage ${storage}, power USB.`,
|
|
300
|
-
`You can edit .gea/boards/${alias}.json later if the board has GPS, audio, or sensors.`
|
|
301
|
-
])
|
|
229
|
+
defaultValue: '0'
|
|
230
|
+
}))
|
|
302
231
|
}
|
|
303
232
|
|
|
304
|
-
renderStep(io, 'Connection', ['
|
|
305
|
-
const
|
|
306
|
-
message: '
|
|
307
|
-
choices: [
|
|
308
|
-
{ value: 'usbSerial', label: 'USB serial' },
|
|
309
|
-
{ value: 'ota', label: 'WiFi OTA' },
|
|
310
|
-
{ value: 'both', label: 'USB serial + OTA' },
|
|
311
|
-
{ value: 'custom', label: 'Custom' }
|
|
312
|
-
],
|
|
313
|
-
defaultValue: 'usbSerial'
|
|
233
|
+
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 number (leave blank to auto-detect or pass --port)'
|
|
314
236
|
})
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const profile = compactObject({
|
|
324
|
-
alias,
|
|
325
|
-
kind: 'custom-board-profile',
|
|
326
|
-
targetFamily: 'esp32',
|
|
237
|
+
|
|
238
|
+
const configPath = boardConfigPath(ctx, parsed)
|
|
239
|
+
const definitionPath = path.join(path.dirname(configPath), 'targets', `${alias}.json`)
|
|
240
|
+
const relativeDefinition = path.relative(path.dirname(configPath), definitionPath)
|
|
241
|
+
const entry = compactObject({
|
|
242
|
+
target: alias,
|
|
243
|
+
targetDefinition: relativeDefinition,
|
|
327
244
|
adapter: 'esp32-idf',
|
|
328
|
-
|
|
329
|
-
mcu: mcuName,
|
|
330
|
-
display: compactObject({
|
|
331
|
-
kind: displayKind,
|
|
332
|
-
controller: displayController,
|
|
333
|
-
interface: displayInterface,
|
|
334
|
-
resolution
|
|
335
|
-
}),
|
|
336
|
-
touch: compactObject({
|
|
337
|
-
controller: touchController,
|
|
338
|
-
interface: touchInterface
|
|
339
|
-
}),
|
|
340
|
-
wireless: compactObject({ wifi, ble }),
|
|
341
|
-
gps: compactObject({ module: gpsModule, interface: gpsInterface }),
|
|
342
|
-
audio: compactObject({ codec: audioCodec, output: audioOutput, input: audioInput }),
|
|
343
|
-
storage: csv(storage),
|
|
344
|
-
sensors: csv(sensors),
|
|
345
|
-
power,
|
|
245
|
+
appPlatform: 'esp32',
|
|
346
246
|
transports: compactObject({
|
|
347
|
-
usbSerial: usbSerial ? { serial: usbSerial } : undefined
|
|
348
|
-
|
|
349
|
-
}),
|
|
350
|
-
notes
|
|
247
|
+
usbSerial: usbSerial ? { serial: usbSerial } : undefined
|
|
248
|
+
})
|
|
351
249
|
})
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
renderCustomBoardReview(io, { alias,
|
|
356
|
-
if (!await shouldSaveSetup(parsed, prompt, 'Save this custom board
|
|
250
|
+
const requiredRoles = roles.map(([role]) => role)
|
|
251
|
+
const missingRoles = requiredRoles.filter((role) => !definition.chips[role])
|
|
252
|
+
validateGpioAssignments(definition)
|
|
253
|
+
renderCustomBoardReview(io, { alias, definition, definitionPath, configPath, entry, missingRoles })
|
|
254
|
+
if (!await shouldSaveSetup(parsed, prompt, 'Save this custom board target?')) {
|
|
357
255
|
return { alias, cancelled: true }
|
|
358
256
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
transports: profile.transports
|
|
369
|
-
})
|
|
370
|
-
writeJsonEnsured(configPath, config)
|
|
371
|
-
io.stdout(`Wrote experimental board alias '${alias}' to ${configPath}`)
|
|
372
|
-
return { alias, flashReady: true }
|
|
373
|
-
} else {
|
|
374
|
-
io.stdout('No board alias was added because no base target was selected.')
|
|
375
|
-
io.stdout('Add a target backend before flashing this custom profile.')
|
|
376
|
-
return { alias, flashReady: false }
|
|
257
|
+
|
|
258
|
+
writeJsonEnsured(definitionPath, definition)
|
|
259
|
+
const config = readBoardConfig(configPath)
|
|
260
|
+
config[alias] = entry
|
|
261
|
+
writeJsonEnsured(configPath, config)
|
|
262
|
+
io.stdout(`Wrote custom target to ${definitionPath}`)
|
|
263
|
+
io.stdout(`Wrote board alias '${alias}' to ${configPath}`)
|
|
264
|
+
if (missingRoles.length) {
|
|
265
|
+
io.stdout(`Add the remaining roles with: npx gea chips add <chip> --board ${alias}`)
|
|
377
266
|
}
|
|
267
|
+
return { alias, flashReady: missingRoles.length === 0 }
|
|
378
268
|
}
|
|
379
269
|
|
|
380
270
|
function renderHeader(io, title, lines = []) {
|
|
@@ -405,25 +295,24 @@ function renderKnownBoardReview(io, { alias, board, configPath, entry }) {
|
|
|
405
295
|
])
|
|
406
296
|
}
|
|
407
297
|
|
|
408
|
-
function renderCustomBoardReview(io, { alias,
|
|
298
|
+
function renderCustomBoardReview(io, { alias, definition, definitionPath, configPath, entry, missingRoles }) {
|
|
409
299
|
renderStep(io, 'Review', [
|
|
410
300
|
'This is what GeaStack will save.'
|
|
411
301
|
])
|
|
412
302
|
writeRows(io, [
|
|
413
303
|
['Alias', alias],
|
|
414
|
-
['MCU / SoC',
|
|
415
|
-
['
|
|
416
|
-
['Display', describeObject(
|
|
417
|
-
['Touch', describeObject(
|
|
418
|
-
['
|
|
419
|
-
['
|
|
420
|
-
['Audio', describeObject(
|
|
421
|
-
['
|
|
422
|
-
['
|
|
423
|
-
['
|
|
424
|
-
['
|
|
425
|
-
['
|
|
426
|
-
['Board config', flashReady ? configPath : 'not written until a base target is selected']
|
|
304
|
+
['MCU / SoC', definition.mcu],
|
|
305
|
+
['Platform base', definition.extends],
|
|
306
|
+
['Display', describeObject(definition.chips.display)],
|
|
307
|
+
['Touch', describeObject(definition.chips.touch)],
|
|
308
|
+
['Power', describeObject(definition.chips.power)],
|
|
309
|
+
['IMU', describeObject(definition.chips.imu)],
|
|
310
|
+
['Audio', describeObject(definition.chips.audio)],
|
|
311
|
+
['I2C', describeObject(definition.buses.i2c)],
|
|
312
|
+
['USB serial', entry.transports?.usbSerial?.serial || 'auto / pass --port'],
|
|
313
|
+
['Missing roles', missingRoles.join(', ') || 'none'],
|
|
314
|
+
['Target definition', definitionPath],
|
|
315
|
+
['Board config', configPath]
|
|
427
316
|
])
|
|
428
317
|
}
|
|
429
318
|
|
|
@@ -461,17 +350,6 @@ function describeObject(value) {
|
|
|
461
350
|
return entries.length ? entries.join('; ') : 'not set'
|
|
462
351
|
}
|
|
463
352
|
|
|
464
|
-
function defaultCustomPeripherals(mcuName) {
|
|
465
|
-
const normalized = String(mcuName || '').toLowerCase()
|
|
466
|
-
const espWithWireless = ['esp32', 'esp32-s3', 'esp32-c3', 'esp32-c6', 'esp32-h2'].includes(normalized)
|
|
467
|
-
const hasPsramByDefault = ['esp32-s3', 'esp32-p4'].includes(normalized)
|
|
468
|
-
return {
|
|
469
|
-
wifi: espWithWireless && normalized !== 'esp32-h2' ? 'built-in' : 'none',
|
|
470
|
-
ble: espWithWireless ? 'built-in' : 'none',
|
|
471
|
-
storage: hasPsramByDefault ? 'flash, psram' : 'flash'
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
353
|
async function maybeInstallNpmDependencies(ctx, parsed, io, prompt, { force = false } = {}) {
|
|
476
354
|
const install = force || option(parsed, 'install') === true || await confirm(prompt, {
|
|
477
355
|
message: 'Install npm dependencies now if package.json exists?',
|
|
@@ -612,6 +490,18 @@ function validateAlias(value) {
|
|
|
612
490
|
return ''
|
|
613
491
|
}
|
|
614
492
|
|
|
493
|
+
async function askPin(prompt, message) {
|
|
494
|
+
return Number(await ask(prompt, {
|
|
495
|
+
message,
|
|
496
|
+
defaultValue: '',
|
|
497
|
+
validate: (value) => {
|
|
498
|
+
if (!/^\d+$/.test(value)) return 'enter a GPIO number from 0 through 48'
|
|
499
|
+
const pin = Number(value)
|
|
500
|
+
return pin >= 0 && pin <= 48 ? '' : 'enter a GPIO number from 0 through 48'
|
|
501
|
+
}
|
|
502
|
+
}))
|
|
503
|
+
}
|
|
504
|
+
|
|
615
505
|
function compactObject(value) {
|
|
616
506
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => {
|
|
617
507
|
if (entry === undefined || entry === null || entry === '') return false
|
|
@@ -620,14 +510,3 @@ function compactObject(value) {
|
|
|
620
510
|
return true
|
|
621
511
|
}))
|
|
622
512
|
}
|
|
623
|
-
|
|
624
|
-
function csv(value) {
|
|
625
|
-
return String(value || '')
|
|
626
|
-
.split(',')
|
|
627
|
-
.map((entry) => entry.trim())
|
|
628
|
-
.filter(Boolean)
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function list(value) {
|
|
632
|
-
return Array.isArray(value) && value.length ? value.join(', ') : 'not set'
|
|
633
|
-
}
|