@geastack/cli 0.1.52 → 0.1.54
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 +14 -1
- package/docs/ESP32-WAVESHARE-AMOLED-QUICKSTART.md +2 -2
- package/docs/NPX-COMMANDS.md +3 -1
- package/docs/SETUP.md +81 -15
- package/docs/SPEC.md +8 -5
- package/package.json +2 -3
- package/src/apps/app-index-writer.mjs +15 -0
- package/src/apps/apple-icons.mjs +147 -0
- package/src/apps/bundle-writer.mjs +156 -0
- package/src/apps/launcher-catalog.mjs +114 -0
- package/src/apps/openai-icons.mjs +356 -0
- package/src/apps/zip-writer.mjs +104 -0
- package/src/ble/ble-ota.swift +291 -0
- package/src/boards/config.mjs +114 -0
- package/src/boards/custom-target.mjs +309 -0
- package/src/boards/resolve.mjs +145 -0
- package/src/boards/targets.mjs +45 -0
- package/src/boards/usb.mjs +252 -0
- package/src/chips.mjs +1 -1
- package/src/commands/apps.mjs +204 -0
- package/src/commands/board.mjs +402 -0
- package/src/commands/boards.mjs +334 -0
- package/src/commands/doctor.mjs +91 -0
- package/src/context.mjs +50 -54
- package/src/device/device.mjs +96 -0
- package/src/device/image.mjs +115 -0
- package/src/device/serial.mjs +430 -0
- package/src/device/wifi.mjs +190 -0
- package/src/esp32/build.mjs +321 -0
- package/src/esp32/capabilities.mjs +54 -0
- package/src/esp32/flash.mjs +181 -0
- package/src/esp32/idf-env.mjs +171 -0
- package/src/esp32/ota.mjs +80 -0
- package/src/esp32/partitions.mjs +59 -0
- package/src/esp32/sdkconfig.mjs +103 -0
- package/src/esp32/wifi-config.mjs +72 -0
- package/src/gea.mjs +89 -427
- package/src/geaos/adapter.mjs +119 -0
- package/src/heap-report.mjs +1276 -0
- package/src/manifest.mjs +135 -36
- package/src/rp2350/adapter.mjs +155 -0
- package/src/serial-devices.mjs +32 -20
- package/src/setup-wizard.mjs +11 -12
- package/src/taurus/adapter.mjs +52 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
// A composed board: a JSON definition (chips from @geastack/chips plus pins
|
|
5
|
+
// and buses) that extends a built-in base target. The CLI turns it into a
|
|
6
|
+
// generated board.h + target.cmake inside the app's build directory before
|
|
7
|
+
// IDF configures, so the target project only ever includes generated files.
|
|
8
|
+
|
|
9
|
+
const supportedBase = 'esp32-s3'
|
|
10
|
+
|
|
11
|
+
function object(value, label) {
|
|
12
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`)
|
|
13
|
+
return value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function text(value, label) {
|
|
17
|
+
if (typeof value !== 'string' || !value.trim()) throw new Error(`${label} must be a non-empty string.`)
|
|
18
|
+
return value.trim()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function integer(value, label, { min = 0, max = 48 } = {}) {
|
|
22
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
23
|
+
throw new Error(`${label} must be an integer from ${min} through ${max}.`)
|
|
24
|
+
}
|
|
25
|
+
return value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pin(value, label, { optional = false } = {}) {
|
|
29
|
+
if (optional && (value === null || value === undefined || value === 'none')) return -1
|
|
30
|
+
return integer(value, label)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function exact(value, expected, label) {
|
|
34
|
+
const actual = text(value, label).toLowerCase()
|
|
35
|
+
if (actual !== expected) {
|
|
36
|
+
throw new Error(`${label} '${actual}' is not supported by the ${supportedBase} base. Supported value: ${expected}.`)
|
|
37
|
+
}
|
|
38
|
+
return actual
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function loadChipCatalogFromDir(chipsDir) {
|
|
42
|
+
const packageDir = text(chipsDir, 'chips package directory')
|
|
43
|
+
const catalogPath = path.join(packageDir, 'catalog.json')
|
|
44
|
+
if (!existsSync(catalogPath)) throw new Error(`Chip catalog not found: ${catalogPath}`)
|
|
45
|
+
const catalog = object(JSON.parse(readFileSync(catalogPath, 'utf8')), 'Chip catalog')
|
|
46
|
+
if (catalog.schemaVersion !== 1) throw new Error(`Unsupported chip catalog schema: ${catalog.schemaVersion}`)
|
|
47
|
+
return object(catalog.chips, 'Chip catalog entries')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function selectedChip(chips, role, category, adapter, mcu, catalog) {
|
|
51
|
+
const selection = object(chips[role], `chips.${role}`)
|
|
52
|
+
const driver = text(selection.driver || selection.controller, `chips.${role}.driver`).toLowerCase()
|
|
53
|
+
const descriptor = object(catalog[driver], `Catalog entry for '${driver}'`)
|
|
54
|
+
if (descriptor.category !== category) {
|
|
55
|
+
throw new Error(`Chip '${driver}' is a ${descriptor.category}, so it cannot fill the ${role} role.`)
|
|
56
|
+
}
|
|
57
|
+
const interfaceName = text(selection.interface, `chips.${role}.interface`).toLowerCase()
|
|
58
|
+
if (!Array.isArray(descriptor.interfaces) || !descriptor.interfaces.includes(interfaceName)) {
|
|
59
|
+
throw new Error(`Chip '${driver}' does not support the '${interfaceName}' interface.`)
|
|
60
|
+
}
|
|
61
|
+
const adapterInfo = descriptor.adapters?.[adapter]
|
|
62
|
+
if (!adapterInfo) throw new Error(`Chip '${driver}' has no ${adapter} binding in the installed catalog.`)
|
|
63
|
+
if (Array.isArray(adapterInfo.mcus) && !adapterInfo.mcus.includes(mcu)) {
|
|
64
|
+
throw new Error(`Chip '${driver}' does not support MCU '${mcu}' through ${adapter}.`)
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...selection,
|
|
68
|
+
driver,
|
|
69
|
+
interface: interfaceName,
|
|
70
|
+
nativeSources: Array.isArray(descriptor.sources) ? descriptor.sources : [],
|
|
71
|
+
bindingSources: Array.isArray(adapterInfo.bindingSources) ? adapterInfo.bindingSources : []
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function normalizeCustomTarget(raw, catalog) {
|
|
76
|
+
const definition = object(raw, 'Target definition')
|
|
77
|
+
const id = text(definition.id, 'id')
|
|
78
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(id)) throw new Error('id may contain lowercase letters, digits, dots, underscores, and hyphens.')
|
|
79
|
+
const base = exact(definition.extends, supportedBase, 'extends')
|
|
80
|
+
const mcu = exact(definition.mcu, 'esp32s3', 'mcu')
|
|
81
|
+
const adapter = definition.adapter ? exact(definition.adapter, 'esp32-idf', 'adapter') : 'esp32-idf'
|
|
82
|
+
const chips = object(definition.chips, 'chips')
|
|
83
|
+
const display = object(selectedChip(chips, 'display', 'display', adapter, mcu, catalog), 'chips.display')
|
|
84
|
+
const touch = object(selectedChip(chips, 'touch', 'touch', adapter, mcu, catalog), 'chips.touch')
|
|
85
|
+
const power = object(selectedChip(chips, 'power', 'power', adapter, mcu, catalog), 'chips.power')
|
|
86
|
+
const imu = object(selectedChip(chips, 'imu', 'imu', adapter, mcu, catalog), 'chips.imu')
|
|
87
|
+
const audio = object(selectedChip(chips, 'audio', 'audio', adapter, mcu, catalog), 'chips.audio')
|
|
88
|
+
const buses = object(definition.buses, 'buses')
|
|
89
|
+
const i2c = object(buses.i2c, 'buses.i2c')
|
|
90
|
+
const storage = object(definition.storage, 'storage')
|
|
91
|
+
const microSD = object(storage.microSD, 'storage.microSD')
|
|
92
|
+
const controls = object(definition.controls, 'controls')
|
|
93
|
+
const launcherButton = object(controls.launcherButton, 'controls.launcherButton')
|
|
94
|
+
|
|
95
|
+
const displayPins = object(display.pins, 'chips.display.pins')
|
|
96
|
+
const touchPins = object(touch.pins, 'chips.touch.pins')
|
|
97
|
+
const audioPins = object(audio.pins, 'chips.audio.pins')
|
|
98
|
+
const storagePins = object(microSD.pins, 'storage.microSD.pins')
|
|
99
|
+
|
|
100
|
+
const spiHost = String(display.spiHost).toLowerCase()
|
|
101
|
+
if (!['spi2', 'spi3'].includes(spiHost)) throw new Error("chips.display.spiHost must be 'spi2' or 'spi3'.")
|
|
102
|
+
|
|
103
|
+
const target = {
|
|
104
|
+
id,
|
|
105
|
+
extends: base,
|
|
106
|
+
adapter,
|
|
107
|
+
mcu,
|
|
108
|
+
buses: {
|
|
109
|
+
i2c: {
|
|
110
|
+
sda: pin(i2c.sda, 'buses.i2c.sda'),
|
|
111
|
+
scl: pin(i2c.scl, 'buses.i2c.scl')
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
chips: {
|
|
115
|
+
display: {
|
|
116
|
+
driver: display.driver,
|
|
117
|
+
interface: exact(display.interface, 'qspi', 'chips.display.interface'),
|
|
118
|
+
width: integer(display.width, 'chips.display.width', { min: 1, max: 4096 }),
|
|
119
|
+
height: integer(display.height, 'chips.display.height', { min: 1, max: 4096 }),
|
|
120
|
+
spiHost,
|
|
121
|
+
nativeSources: display.nativeSources,
|
|
122
|
+
bindingSources: display.bindingSources,
|
|
123
|
+
pins: {
|
|
124
|
+
cs: pin(displayPins.cs, 'chips.display.pins.cs'),
|
|
125
|
+
pclk: pin(displayPins.pclk, 'chips.display.pins.pclk'),
|
|
126
|
+
data0: pin(displayPins.data0, 'chips.display.pins.data0'),
|
|
127
|
+
data1: pin(displayPins.data1, 'chips.display.pins.data1'),
|
|
128
|
+
data2: pin(displayPins.data2, 'chips.display.pins.data2'),
|
|
129
|
+
data3: pin(displayPins.data3, 'chips.display.pins.data3'),
|
|
130
|
+
reset: pin(displayPins.reset, 'chips.display.pins.reset'),
|
|
131
|
+
te: pin(displayPins.te, 'chips.display.pins.te', { optional: true })
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
touch: {
|
|
135
|
+
driver: touch.driver,
|
|
136
|
+
interface: exact(touch.interface, 'i2c', 'chips.touch.interface'),
|
|
137
|
+
nativeSources: touch.nativeSources,
|
|
138
|
+
bindingSources: touch.bindingSources,
|
|
139
|
+
pins: {
|
|
140
|
+
reset: pin(touchPins.reset, 'chips.touch.pins.reset'),
|
|
141
|
+
interrupt: pin(touchPins.interrupt, 'chips.touch.pins.interrupt')
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
power: {
|
|
145
|
+
driver: power.driver,
|
|
146
|
+
interface: exact(power.interface, 'i2c', 'chips.power.interface'),
|
|
147
|
+
nativeSources: power.nativeSources,
|
|
148
|
+
bindingSources: power.bindingSources
|
|
149
|
+
},
|
|
150
|
+
imu: {
|
|
151
|
+
driver: imu.driver,
|
|
152
|
+
interface: exact(imu.interface, 'i2c', 'chips.imu.interface'),
|
|
153
|
+
nativeSources: imu.nativeSources,
|
|
154
|
+
bindingSources: imu.bindingSources
|
|
155
|
+
},
|
|
156
|
+
audio: {
|
|
157
|
+
driver: audio.driver,
|
|
158
|
+
interface: exact(audio.interface, 'i2s', 'chips.audio.interface'),
|
|
159
|
+
nativeSources: audio.nativeSources,
|
|
160
|
+
bindingSources: audio.bindingSources,
|
|
161
|
+
pins: {
|
|
162
|
+
mclk: pin(audioPins.mclk, 'chips.audio.pins.mclk'),
|
|
163
|
+
bclk: pin(audioPins.bclk, 'chips.audio.pins.bclk'),
|
|
164
|
+
ws: pin(audioPins.ws, 'chips.audio.pins.ws'),
|
|
165
|
+
dout: pin(audioPins.dout, 'chips.audio.pins.dout'),
|
|
166
|
+
din: pin(audioPins.din, 'chips.audio.pins.din'),
|
|
167
|
+
powerAmplifier: pin(audioPins.powerAmplifier, 'chips.audio.pins.powerAmplifier')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
storage: {
|
|
172
|
+
microSD: {
|
|
173
|
+
interface: exact(microSD.interface, 'sdmmc-1bit', 'storage.microSD.interface'),
|
|
174
|
+
pins: {
|
|
175
|
+
clk: pin(storagePins.clk, 'storage.microSD.pins.clk', { optional: true }),
|
|
176
|
+
cmd: pin(storagePins.cmd, 'storage.microSD.pins.cmd', { optional: true }),
|
|
177
|
+
data0: pin(storagePins.data0, 'storage.microSD.pins.data0', { optional: true })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
controls: {
|
|
182
|
+
launcherButton: {
|
|
183
|
+
pin: pin(launcherButton.pin, 'controls.launcherButton.pin', { optional: true }),
|
|
184
|
+
activeLevel: integer(launcherButton.activeLevel, 'controls.launcherButton.activeLevel', { min: 0, max: 1 })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
validatePinAssignments(target)
|
|
189
|
+
return target
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function validatePinAssignments(target) {
|
|
193
|
+
const pins = [
|
|
194
|
+
['I2C SDA', target.buses.i2c.sda],
|
|
195
|
+
['I2C SCL', target.buses.i2c.scl],
|
|
196
|
+
['display CS', target.chips.display.pins.cs],
|
|
197
|
+
['display PCLK', target.chips.display.pins.pclk],
|
|
198
|
+
['display DATA0', target.chips.display.pins.data0],
|
|
199
|
+
['display DATA1', target.chips.display.pins.data1],
|
|
200
|
+
['display DATA2', target.chips.display.pins.data2],
|
|
201
|
+
['display DATA3', target.chips.display.pins.data3],
|
|
202
|
+
['display reset', target.chips.display.pins.reset],
|
|
203
|
+
['display TE', target.chips.display.pins.te],
|
|
204
|
+
['touch reset', target.chips.touch.pins.reset],
|
|
205
|
+
['touch interrupt', target.chips.touch.pins.interrupt],
|
|
206
|
+
['audio MCLK', target.chips.audio.pins.mclk],
|
|
207
|
+
['audio BCLK', target.chips.audio.pins.bclk],
|
|
208
|
+
['audio WS', target.chips.audio.pins.ws],
|
|
209
|
+
['audio DOUT', target.chips.audio.pins.dout],
|
|
210
|
+
['audio DIN', target.chips.audio.pins.din],
|
|
211
|
+
['audio amplifier', target.chips.audio.pins.powerAmplifier],
|
|
212
|
+
['microSD CLK', target.storage.microSD.pins.clk],
|
|
213
|
+
['microSD CMD', target.storage.microSD.pins.cmd],
|
|
214
|
+
['microSD DATA0', target.storage.microSD.pins.data0],
|
|
215
|
+
['launcher button', target.controls.launcherButton.pin]
|
|
216
|
+
]
|
|
217
|
+
const used = new Map()
|
|
218
|
+
for (const [label, value] of pins) {
|
|
219
|
+
if (value < 0) continue
|
|
220
|
+
const previous = used.get(value)
|
|
221
|
+
if (previous) throw new Error(`GPIO ${value} is assigned to both ${previous} and ${label}.`)
|
|
222
|
+
used.set(value, label)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function gpio(value) {
|
|
227
|
+
return value < 0 ? 'GPIO_NUM_NC' : `GPIO_NUM_${value}`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function renderBoardHeader(target) {
|
|
231
|
+
const { i2c } = target.buses
|
|
232
|
+
const { display, touch, audio } = target.chips
|
|
233
|
+
const sd = target.storage.microSD
|
|
234
|
+
const launcher = target.controls.launcherButton
|
|
235
|
+
return `#pragma once
|
|
236
|
+
|
|
237
|
+
#include "driver/gpio.h"
|
|
238
|
+
#include "driver/i2s_types.h"
|
|
239
|
+
#include "driver/spi_master.h"
|
|
240
|
+
|
|
241
|
+
namespace gea::platform::board {
|
|
242
|
+
|
|
243
|
+
struct I2cBusConfig { gpio_num_t sda; gpio_num_t scl; };
|
|
244
|
+
struct Co5300DisplayConfig { spi_host_device_t spiHost; gpio_num_t cs; gpio_num_t pclk; gpio_num_t data0; gpio_num_t data1; gpio_num_t data2; gpio_num_t data3; gpio_num_t reset; gpio_num_t te; };
|
|
245
|
+
struct Ft3168TouchConfig { gpio_num_t reset; gpio_num_t interrupt; };
|
|
246
|
+
struct Es8311AudioConfig { int i2sPort; gpio_num_t mclk; gpio_num_t bclk; gpio_num_t ws; gpio_num_t dout; gpio_num_t din; gpio_num_t powerAmplifier; };
|
|
247
|
+
struct SdMmcConfig { gpio_num_t clk; gpio_num_t cmd; gpio_num_t data0; };
|
|
248
|
+
struct LauncherButtonConfig { gpio_num_t pin; int activeLevel; };
|
|
249
|
+
|
|
250
|
+
inline constexpr I2cBusConfig i2c{ .sda = ${gpio(i2c.sda)}, .scl = ${gpio(i2c.scl)} };
|
|
251
|
+
inline constexpr Co5300DisplayConfig display{
|
|
252
|
+
.spiHost = ${display.spiHost === 'spi3' ? 'SPI3_HOST' : 'SPI2_HOST'}, .cs = ${gpio(display.pins.cs)}, .pclk = ${gpio(display.pins.pclk)},
|
|
253
|
+
.data0 = ${gpio(display.pins.data0)}, .data1 = ${gpio(display.pins.data1)},
|
|
254
|
+
.data2 = ${gpio(display.pins.data2)}, .data3 = ${gpio(display.pins.data3)},
|
|
255
|
+
.reset = ${gpio(display.pins.reset)}, .te = ${gpio(display.pins.te)}
|
|
256
|
+
};
|
|
257
|
+
inline constexpr Ft3168TouchConfig touch{ .reset = ${gpio(touch.pins.reset)}, .interrupt = ${gpio(touch.pins.interrupt)} };
|
|
258
|
+
inline constexpr Es8311AudioConfig audio{
|
|
259
|
+
.i2sPort = I2S_NUM_AUTO, .mclk = ${gpio(audio.pins.mclk)}, .bclk = ${gpio(audio.pins.bclk)},
|
|
260
|
+
.ws = ${gpio(audio.pins.ws)}, .dout = ${gpio(audio.pins.dout)}, .din = ${gpio(audio.pins.din)},
|
|
261
|
+
.powerAmplifier = ${gpio(audio.pins.powerAmplifier)}
|
|
262
|
+
};
|
|
263
|
+
inline constexpr SdMmcConfig storage{ .clk = ${gpio(sd.pins.clk)}, .cmd = ${gpio(sd.pins.cmd)}, .data0 = ${gpio(sd.pins.data0)} };
|
|
264
|
+
inline constexpr LauncherButtonConfig launcherButton{ .pin = ${gpio(launcher.pin)}, .activeLevel = ${launcher.activeLevel} };
|
|
265
|
+
|
|
266
|
+
} // namespace gea::platform::board
|
|
267
|
+
`
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function cmakeQuote(value) {
|
|
271
|
+
return `"${String(value).replace(/\\/g, '/').replace(/"/g, '\\"')}"`
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function renderTargetCmake(target, includeDir) {
|
|
275
|
+
const { display, touch, power, imu, audio } = target.chips
|
|
276
|
+
const chipSource = (source) => ` "\${GEA_CHIPS}/${source}"`
|
|
277
|
+
const bindingSource = (source) => ` "\${GEA_EMBEDDED_ROOT}/targets/esp32/${source}"`
|
|
278
|
+
const displaySources = [...display.nativeSources.map(chipSource), ...display.bindingSources.map(bindingSource)]
|
|
279
|
+
const peripheralSources = [power, imu, touch, audio]
|
|
280
|
+
.flatMap((chip) => [...chip.nativeSources.map(chipSource), ...chip.bindingSources.map(bindingSource)])
|
|
281
|
+
return `set(GEA_CUSTOM_TARGET_ACTIVE 1)
|
|
282
|
+
set(GEA_CUSTOM_TARGET_INCLUDE_DIR ${cmakeQuote(includeDir)})
|
|
283
|
+
set(GEA_CUSTOM_TARGET_DISPLAY_SOURCES
|
|
284
|
+
${displaySources.join('\n')}
|
|
285
|
+
)
|
|
286
|
+
set(GEA_CUSTOM_TARGET_PERIPHERAL_SOURCES
|
|
287
|
+
${peripheralSources.join('\n')}
|
|
288
|
+
)
|
|
289
|
+
set(GEA_CUSTOM_TARGET_COMPILE_DEFINITIONS
|
|
290
|
+
GEA_EMBEDDED_DISPLAY_WIDTH=${display.width}
|
|
291
|
+
GEA_EMBEDDED_DISPLAY_HEIGHT=${display.height}
|
|
292
|
+
)
|
|
293
|
+
`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function writeCustomTarget({ definitionPath, outDir, catalog }) {
|
|
297
|
+
const target = normalizeCustomTarget(JSON.parse(readFileSync(definitionPath, 'utf8')), catalog)
|
|
298
|
+
mkdirSync(outDir, { recursive: true })
|
|
299
|
+
const headerPath = path.join(outDir, 'board.h')
|
|
300
|
+
const cmakePath = path.join(outDir, 'target.cmake')
|
|
301
|
+
writeIfChanged(headerPath, renderBoardHeader(target))
|
|
302
|
+
writeIfChanged(cmakePath, renderTargetCmake(target, outDir))
|
|
303
|
+
return { target, headerPath, cmakePath }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function writeIfChanged(file, contents) {
|
|
307
|
+
if (existsSync(file) && readFileSync(file, 'utf8') === contents) return
|
|
308
|
+
writeFileSync(file, contents)
|
|
309
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { boardConfigOrigins, boardConfigPath, loadBoardConfig, normalizeBoardConfig } from './config.mjs'
|
|
5
|
+
import { loadTargets } from './targets.mjs'
|
|
6
|
+
import { resolveUsbSerialPort } from './usb.mjs'
|
|
7
|
+
|
|
8
|
+
export const usbSerialAdapters = new Set(['esp32-idf', 'rp2350-pico', 'taurus-s3'])
|
|
9
|
+
export const geaosAdapters = new Set(['geaos-linux', 'geaos-arm64'])
|
|
10
|
+
|
|
11
|
+
function boardTransport(board, name) {
|
|
12
|
+
return board?.transports?.[name] || {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isAuto(value) {
|
|
16
|
+
return !value || /^auto$/i.test(value) || value === '<auto>'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Turns a board alias (or a bare target id) into everything a command needs:
|
|
20
|
+
// the target project directory, its adapter, chip/flash metadata and the
|
|
21
|
+
// transports the command asked for. `needs.usbPort` resolves the board's USB
|
|
22
|
+
// serial to today's /dev port; `needs.otaHost` resolves transports.ota.host.
|
|
23
|
+
// Commands state what they need instead of the resolver guessing from names.
|
|
24
|
+
export function resolveBoardSelection({
|
|
25
|
+
ctx = null,
|
|
26
|
+
boardName = '',
|
|
27
|
+
targetName = '',
|
|
28
|
+
requestedPort = '',
|
|
29
|
+
requestedHost = '',
|
|
30
|
+
needs = {},
|
|
31
|
+
targets = ctx ? loadTargets(ctx) : {},
|
|
32
|
+
config = ctx ? loadBoardConfig(ctx) : {},
|
|
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(),
|
|
36
|
+
usbSerialResolver = resolveUsbSerialPort,
|
|
37
|
+
deferUsbPort = false
|
|
38
|
+
} = {}) {
|
|
39
|
+
const boards = normalizeBoardConfig(config)
|
|
40
|
+
const board = boardName ? boards[boardName] : null
|
|
41
|
+
if (boardName && !board) {
|
|
42
|
+
throw new Error(`Unknown board '${boardName}'. Run gea boards list, or gea boards add to register it.`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let target = board?.target || targetName || ''
|
|
46
|
+
if (!target) throw new Error('No board selected. Pass --board <alias> (gea boards list) or --target <id>.')
|
|
47
|
+
let targetBase = target
|
|
48
|
+
let targetDefinition = ''
|
|
49
|
+
let definition = null
|
|
50
|
+
if (board?.targetDefinition) {
|
|
51
|
+
targetDefinition = path.resolve(configDir, board.targetDefinition)
|
|
52
|
+
if (!existsSync(targetDefinition)) {
|
|
53
|
+
throw new Error(`Target definition for board '${boardName}' was not found: ${targetDefinition}`)
|
|
54
|
+
}
|
|
55
|
+
definition = JSON.parse(readFileSync(targetDefinition, 'utf8'))
|
|
56
|
+
if (!definition || typeof definition !== 'object' || Array.isArray(definition)) {
|
|
57
|
+
throw new Error(`Target definition must be a JSON object: ${targetDefinition}`)
|
|
58
|
+
}
|
|
59
|
+
if (!definition.id) throw new Error(`Target definition is missing id: ${targetDefinition}`)
|
|
60
|
+
if (!definition.extends) throw new Error(`Target definition is missing extends: ${targetDefinition}`)
|
|
61
|
+
if (board.target && board.target !== definition.id) {
|
|
62
|
+
throw new Error(`Board '${boardName}' selects target '${board.target}', but ${targetDefinition} defines '${definition.id}'.`)
|
|
63
|
+
}
|
|
64
|
+
target = definition.id
|
|
65
|
+
targetBase = definition.extends
|
|
66
|
+
}
|
|
67
|
+
const targetInfo = targets[targetBase] || {}
|
|
68
|
+
const adapter = board?.adapter || definition?.adapter || targetInfo.adapter || ''
|
|
69
|
+
if (!adapter) throw new Error(`Unknown target '${target}'. It is not in targets.json and the board declares no adapter.`)
|
|
70
|
+
|
|
71
|
+
const usb = boardTransport(board, 'usbSerial')
|
|
72
|
+
if (usb.path) {
|
|
73
|
+
throw new Error(`Board '${boardName}' uses transports.usbSerial.path, which is no longer supported; set transports.usbSerial.serial instead.`)
|
|
74
|
+
}
|
|
75
|
+
let port = ''
|
|
76
|
+
let host = ''
|
|
77
|
+
const usbSerial = usb.serial || ''
|
|
78
|
+
|
|
79
|
+
if (needs.usbPort && usbSerialAdapters.has(adapter)) {
|
|
80
|
+
if (!isAuto(requestedPort)) {
|
|
81
|
+
port = requestedPort
|
|
82
|
+
} else if (usbSerial) {
|
|
83
|
+
if (!deferUsbPort) port = usbSerialResolver({ serial: usbSerial })
|
|
84
|
+
} else if (boardName) {
|
|
85
|
+
// A WiFi-only board hits this on monitor/screenshot. Point at the
|
|
86
|
+
// cable-free command instead of dead-ending on USB.
|
|
87
|
+
const wireless = boardTransport(board, 'ota').host
|
|
88
|
+
? ` This board has transports.ota.host, so 'gea logs --board ${boardName}' and 'gea screenshot --board ${boardName}' work with no cable.`
|
|
89
|
+
: ''
|
|
90
|
+
throw new Error(`Board '${boardName}' does not define transports.usbSerial.serial, and no USB port was passed.${wireless}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// geaos devices are identified by USB serial too, but tolerantly: a build
|
|
95
|
+
// with the watch detached must still work, so an unresolved port stays
|
|
96
|
+
// empty and the adapter errors at the point it needs the device.
|
|
97
|
+
if (needs.usbPort && geaosAdapters.has(adapter)) {
|
|
98
|
+
if (!isAuto(requestedPort)) {
|
|
99
|
+
port = requestedPort
|
|
100
|
+
} else if (usbSerial && !deferUsbPort) {
|
|
101
|
+
try {
|
|
102
|
+
port = usbSerialResolver({ serial: usbSerial })
|
|
103
|
+
} catch {
|
|
104
|
+
port = ''
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (needs.otaHost) {
|
|
110
|
+
if (!isAuto(requestedHost)) {
|
|
111
|
+
host = requestedHost
|
|
112
|
+
} else {
|
|
113
|
+
host = boardTransport(board, 'ota').host || ''
|
|
114
|
+
if (!host) throw new Error(`Board '${boardName || target}' does not define transports.ota.host, and no --host was passed.`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const selection = {
|
|
119
|
+
boardName,
|
|
120
|
+
target,
|
|
121
|
+
adapter,
|
|
122
|
+
targetDir: board?.targetDir || targetInfo.targetDir || '',
|
|
123
|
+
flashSize: board?.flashSize || targetInfo.flashSize || '',
|
|
124
|
+
appPlatform: board?.appPlatform || targetInfo.appPlatform || '',
|
|
125
|
+
compatibleAppPlatforms: Array.isArray(targetInfo.compatibleAppPlatforms) ? targetInfo.compatibleAppPlatforms : [],
|
|
126
|
+
idfTarget: board?.idfTarget || targetInfo.idfTarget || '',
|
|
127
|
+
esptoolChip: board?.esptoolChip || targetInfo.esptoolChip || '',
|
|
128
|
+
mainTaskStackSize: board?.mainTaskStackSize || targetInfo.mainTaskStackSize || '',
|
|
129
|
+
ipcTaskStackSize: board?.ipcTaskStackSize || targetInfo.ipcTaskStackSize || '',
|
|
130
|
+
port,
|
|
131
|
+
host,
|
|
132
|
+
usbSerial,
|
|
133
|
+
usbRestartAfterFlash: usb.restartAfterFlash || '',
|
|
134
|
+
otaHost: boardTransport(board, 'ota').host || '',
|
|
135
|
+
telnetHost: boardTransport(board, 'telnet').host || board?.telnetHost || targetInfo.telnetHost || '',
|
|
136
|
+
telnetPort: String(boardTransport(board, 'telnet').port || board?.telnetPort || targetInfo.telnetPort || ''),
|
|
137
|
+
fastbootSerial: boardTransport(board, 'fastboot').serial || board?.fastbootSerial || '',
|
|
138
|
+
mtkWorkdir: boardTransport(board, 'mtk').workdir || board?.mtkWorkdir || '',
|
|
139
|
+
mtkBootSlot: boardTransport(board, 'mtk').bootSlot || board?.mtkBootSlot || '',
|
|
140
|
+
mtkMethod: boardTransport(board, 'mtk').method || board?.mtkMethod || '',
|
|
141
|
+
mtkMonitorGlob: boardTransport(board, 'mtk').monitorGlob || board?.mtkMonitorGlob || '',
|
|
142
|
+
targetDefinition
|
|
143
|
+
}
|
|
144
|
+
return selection
|
|
145
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { ExitCode, fail } from '../errors.mjs'
|
|
4
|
+
import { exists, readJson } from '../fs-utils.mjs'
|
|
5
|
+
|
|
6
|
+
// Built-in targets are DATA shipped by @geastack/targets (targets.json at the
|
|
7
|
+
// package root). Each entry names an adapter and where its project lives; the
|
|
8
|
+
// project may sit in another package (geaos boards live in @geastack/geaos).
|
|
9
|
+
export function loadTargets(ctx) {
|
|
10
|
+
if (!ctx.targetsRoot) return {}
|
|
11
|
+
const file = path.join(ctx.targetsRoot, 'targets.json')
|
|
12
|
+
if (!exists(file)) return {}
|
|
13
|
+
const raw = readJson(file)
|
|
14
|
+
const out = {}
|
|
15
|
+
for (const [id, entry] of Object.entries(raw)) {
|
|
16
|
+
const { targetPath, package: packageName, ...rest } = entry
|
|
17
|
+
if (!targetPath) fail(`Built-in target '${id}' is missing targetPath in ${file}.`, ExitCode.missingDependency)
|
|
18
|
+
const packageRoot = packageName ? packageDirFor(ctx, packageName) : ctx.targetsRoot
|
|
19
|
+
out[id] = {
|
|
20
|
+
...rest,
|
|
21
|
+
id,
|
|
22
|
+
package: packageName || '@geastack/targets',
|
|
23
|
+
targetDir: packageRoot ? path.join(packageRoot, targetPath) : ''
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function requireTargets(ctx) {
|
|
30
|
+
if (!ctx.targetsRoot) {
|
|
31
|
+
fail('@geastack/targets is not installed in this project (npm i @geastack/targets).', ExitCode.missingDependency)
|
|
32
|
+
}
|
|
33
|
+
const targets = loadTargets(ctx)
|
|
34
|
+
if (Object.keys(targets).length === 0) {
|
|
35
|
+
fail(`No targets.json found in ${ctx.targetsRoot}.`, ExitCode.missingDependency)
|
|
36
|
+
}
|
|
37
|
+
return targets
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function packageDirFor(ctx, packageName) {
|
|
41
|
+
for (const field of Object.keys(ctx)) {
|
|
42
|
+
if (typeof ctx.packageName === 'function' && ctx.packageName(field) === packageName) return ctx[field]
|
|
43
|
+
}
|
|
44
|
+
return ''
|
|
45
|
+
}
|