tapeo-cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -5
- package/cli/tapeo.mjs +69 -4
- package/package.json +1 -1
- package/shared/api.mjs +10 -0
- package/shared/fpga.mjs +72 -0
- package/shared/serial.mjs +77 -0
package/README.md
CHANGED
|
@@ -1,16 +1,50 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://tapeo.net/icons/icon-512.png" alt="Tapeo" width="96" height="96">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">Tapeo for your terminal and VS Code</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">Design, run, and fix chips from your own shell or editor, on the same projects as <a href="https://tapeo.net">tapeo.net</a>.</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/tapeo-cli"><img src="https://img.shields.io/npm/v/tapeo-cli?label=tapeo-cli&logo=npm&color=cb3837" alt="tapeo-cli on npm"></a>
|
|
11
|
+
<a href="https://marketplace.visualstudio.com/items?itemName=tapeo.tapeo-vscode"><img src="https://img.shields.io/badge/VS%20Code%20Marketplace-Tapeo-0078d4?logo=visualstudiocode&logoColor=white" alt="Tapeo for VS Code"></a>
|
|
12
|
+
<img src="https://img.shields.io/node/v/tapeo-cli?logo=nodedotjs&logoColor=white&color=5fa04e" alt="Node.js version">
|
|
13
|
+
<a href="https://tapeo.net/docs#local-clients"><img src="https://img.shields.io/badge/docs-tapeo.net-e8914a" alt="Documentation"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install -g tapeo-cli
|
|
20
|
+
tapeo login # approve this device in your browser
|
|
21
|
+
tapeo new "A 4-bit counter with enable" ./counter # design, build, and pull the files
|
|
22
|
+
cd counter && tapeo show # stage results of that build
|
|
23
|
+
# edit rtl/…, then: tapeo push --all && tapeo run --watch
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
In VS Code, install **Tapeo** from the Extensions view and run **Tapeo: Connect to Server**.
|
|
27
|
+
|
|
28
|
+
| Feature | What you get |
|
|
29
|
+
|---|---|
|
|
30
|
+
| 🧭 **Design with the Architect** | `tapeo new` asks clarifying questions, proposes design options, shows the build plan, and builds. |
|
|
31
|
+
| 🔁 **Edit → run → results** | Push your files, run the full flow or one stage, and read stage results, tool logs, and goals. |
|
|
32
|
+
| 🎯 **Errors on the right line** | `tapeo problems`, or the VS Code Problems panel, points at the line a tool complained about. |
|
|
33
|
+
| 🛠️ **AI Engineer** | `tapeo ask` streams an answer; review the proposed diff and `tapeo apply` it locally. |
|
|
34
|
+
| 🔒 **Safe sync** | Every push names the version it was based on; your local edits are never overwritten. |
|
|
35
|
+
|
|
36
|
+
## About
|
|
37
|
+
|
|
38
|
+
A terminal client and a VS Code extension for the same Tapeo projects as the web app. They call the same server, file storage, workflow queue, and plan allowances as the browser, through the token-authenticated API under `/api/client/v1`. Version 0.4.0 was published on 2026-10-24 (`tapeo-cli` on npm and **Tapeo**, `tapeo.tapeo-vscode`, on the VS Code Marketplace). They are early releases (0.x).
|
|
4
39
|
|
|
5
40
|
The edit → run → results loop works without the browser: push your files, start a run, follow it stage by stage, and read stage results, tool logs, and goal results. You can also start a new design with the Architect, ask the AI Engineer and apply its proposed changes locally, and see tool errors on the lines they name. The 2D/3D layout views, Evaluation-plan review, and Architect image/PDF attachments stay in the web workspace.
|
|
6
41
|
|
|
7
|
-
|
|
42
|
+
Self-hosted servers need migrations `0008_client_tokens` and `0009_device_authorization` (`npx prisma migrate deploy`; the Docker deployment applies them on start). Maintainers: releasing is described in `docs/RELEASING.md` in the Tapeo repository.
|
|
8
43
|
|
|
9
44
|
## Sign in
|
|
10
45
|
|
|
11
46
|
```sh
|
|
12
|
-
npm install -g
|
|
13
|
-
# once published: npm install -g tapeo-cli
|
|
47
|
+
npm install -g tapeo-cli # from source: npm install -g ./clients, or npm run tapeo -- <command>
|
|
14
48
|
tapeo login # https://tapeo.net; or: tapeo login https://your-own-server
|
|
15
49
|
```
|
|
16
50
|
|
|
@@ -22,6 +56,7 @@ tapeo login # https://tapeo.net; or: tapeo login https://y
|
|
|
22
56
|
tapeo projects # your projects
|
|
23
57
|
tapeo create "My sensor" ./my-sensor # new empty project, bound to the folder
|
|
24
58
|
tapeo pull PROJECT_ID ./existing # or bind an existing project
|
|
59
|
+
tapeo delete PROJECT_ID # delete a project on the server for good (asks for its name)
|
|
25
60
|
cd ./my-sensor
|
|
26
61
|
tapeo status # what changed locally, and the latest run
|
|
27
62
|
tapeo push --all # push every modified and new source file
|
|
@@ -42,6 +77,8 @@ tapeo open # the project's web address
|
|
|
42
77
|
tapeo --version # this client's version (tapeo help lists every command)
|
|
43
78
|
```
|
|
44
79
|
|
|
80
|
+
`tapeo delete` removes a project on the server for good, like the delete button in the browser's project list: its files, checkpoints, runs, layouts, and Engineer history (a running workflow stops). In a linked folder it deletes that folder's project unless you name another. You confirm by typing the project name (`--yes` skips this, for scripts). Local files stay, and the folder is no longer linked.
|
|
81
|
+
|
|
45
82
|
### AI
|
|
46
83
|
|
|
47
84
|
```sh
|
|
@@ -64,6 +101,23 @@ The Engineer reads the saved server copy of your files (`tapeo ask` warns when l
|
|
|
64
101
|
|
|
65
102
|
Stages: `simulation`, `synthesis`, `timing`, `physical`, `post-route-timing`, `drc`, `extraction`, `erc`, `lvs`. `--no-ai-repairs` turns off AI repairs for a stage run. A full run uses one build from your plan; a stage run does not. `tapeo run --watch` exits with code 3 when the run fails.
|
|
66
103
|
|
|
104
|
+
### FPGA boards
|
|
105
|
+
|
|
106
|
+
For a project whose target is an FPGA board, or an FPGA board and a chip (the Architect asks where a design runs; you can change it on the Workflow tab), the workflow simulates the RTL, then builds and checks a bitstream for the board on the server. Supported boards: Sipeed Tang Nano 9K and Tang Nano 20K, 1BitSquared iCEBreaker, and Radiona ULX3S (12F and 85F). A design clock other than the board clock gets a PLL generated by the toolchain's own calculator.
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
tapeo run --watch # simulate, build the bitstream, report logic use and Fmax
|
|
110
|
+
tapeo flash # download the bitstream and program the connected board
|
|
111
|
+
tapeo flash --flash # keep it after power-off (Tang Nano and ULX3S otherwise load to SRAM)
|
|
112
|
+
tapeo flash --board icebreaker # when bitstreams exist for several boards
|
|
113
|
+
tapeo monitor # the board's serial (UART) output; type a line to send it
|
|
114
|
+
tapeo monitor --port /dev/cu.usbserial-1101 --baud 9600
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`tapeo flash` runs the open-source `openFPGALoader` on your computer and tells you how to install it if it is missing (`brew install openfpgaloader` on macOS, `sudo apt install openfpgaloader` on Debian/Ubuntu, or OSS CAD Suite elsewhere). `TAPEO_OPENFPGALOADER` points to it when it is not on your PATH. The bitstream is saved read-only under `generated/fpga/<board>/`.
|
|
118
|
+
|
|
119
|
+
`tapeo monitor` needs no sign-in and works on macOS and Linux: it finds the USB serial port (name one with `--port` when there are several; a Tang Nano shows two and its serial link is usually the second) and uses 115200 baud unless you pass `--baud`. On Linux you may need to be in the `dialout` group. On Windows, use the **Serial monitor** on the project's Workflow tab in Chrome or Edge.
|
|
120
|
+
|
|
67
121
|
## VS Code
|
|
68
122
|
|
|
69
123
|
Run `npm run build:vscode`, open `clients/vscode` in VS Code, and press F5; or run `npm run package:vscode` and install `clients/vscode/tapeo-vscode.vsix` with **Extensions: Install from VSIX...**. In the Command Palette:
|
|
@@ -75,6 +129,8 @@ Run `npm run build:vscode`, open `clients/vscode` in VS Code, and press F5; or r
|
|
|
75
129
|
- **Show Workflow Runs**, **Show Run Details**, **Open a Tool Log**, **Show Goal Results**, **Cancel Workflow**, **Open Project in Browser**.
|
|
76
130
|
- **New Design with the Architect** (in an empty folder): the Architect's questions appear as quick picks with "Not sure — you choose", then the design options and the build plan; **Build** creates the project, starts the first build, and pulls the generated files when it ends.
|
|
77
131
|
- **Open AI Engineer** opens the **Tapeo Engineer** panel beside the editor: the project conversation, streamed answers, and the proposed changes with **Review** (a side-by-side diff), **Apply**, **Apply all**, and **Discard**. **Ask the AI Engineer**, **Review Proposed Changes**, **Apply All Proposed Changes**, and **Discard Proposed Changes** do the same from the Command Palette. Applying is refused while the file has unsaved edits; push afterwards.
|
|
132
|
+
- **Program FPGA Board** (FPGA projects): downloads the bitstream and programs the connected board with `openFPGALoader` in a **Tapeo FPGA** terminal.
|
|
133
|
+
- **Open Serial Monitor** (macOS and Linux): shows the board's serial output in a terminal and sends the lines you type.
|
|
78
134
|
- **Show Tool Problems**: the latest run's tool errors and warnings in the Problems panel. This also happens automatically when a run the status bar follows ends, and starting a run clears them.
|
|
79
135
|
|
|
80
136
|
The setting `tapeo.serverUrl` holds the server (Connect to Server sets it; remote servers must use HTTPS).
|
package/cli/tapeo.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir, hostname } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -8,6 +8,8 @@ import { assertUnbound, designFilesFrom, downloadFile, localChanges, moveProject
|
|
|
8
8
|
import { applyProposals, changeDiff, describeChange, discardProposals, loadProposals, saveProposals } from "../shared/proposals.mjs";
|
|
9
9
|
import { collectRunProblems, formatProblem } from "../shared/problems.mjs";
|
|
10
10
|
import { NOT_SURE, clarificationAnswersMessage } from "../shared/stream.mjs";
|
|
11
|
+
import { chooseSerialPort, openSerialPort, parseBaud, serialCandidates } from "../shared/serial.mjs";
|
|
12
|
+
import { OPENFPGALOADER_INSTALL, chooseBitstream, downloadBitstream, flashArguments, openFpgaLoaderAvailable, openFpgaLoaderExecutable } from "../shared/fpga.mjs";
|
|
11
13
|
import { createInterface } from "node:readline/promises";
|
|
12
14
|
|
|
13
15
|
const configPath = path.join(homedir(), ".config", "tapeo", "client.json");
|
|
@@ -15,7 +17,7 @@ const clientVersion = JSON.parse(await readFile(new URL("../package.json", impor
|
|
|
15
17
|
setClientIdentity("tapeo-cli", clientVersion);
|
|
16
18
|
|
|
17
19
|
function usage() {
|
|
18
|
-
console.log(`Tapeo terminal client
|
|
20
|
+
console.log(`Tapeo terminal client
|
|
19
21
|
|
|
20
22
|
Sign in
|
|
21
23
|
tapeo login [server-url] Sign in through your browser (approve this device there);
|
|
@@ -27,6 +29,9 @@ Sign in
|
|
|
27
29
|
Projects and files
|
|
28
30
|
tapeo projects List your projects
|
|
29
31
|
tapeo create <name> [dir] Create an empty project and bind a local folder
|
|
32
|
+
tapeo delete [project-id] Delete a project on the server for good (this folder's
|
|
33
|
+
project by default; asks you to type its name, --yes
|
|
34
|
+
skips that); local files are kept
|
|
30
35
|
tapeo pull <project-id> [dir] Pull source files without overwriting local edits
|
|
31
36
|
tapeo pull --outputs Also download tool outputs (generated/, signoff/) read-only
|
|
32
37
|
tapeo status What changed locally, and the latest run
|
|
@@ -57,6 +62,15 @@ Workflow
|
|
|
57
62
|
tapeo cancel [run-id] Cancel a queued or running workflow
|
|
58
63
|
tapeo open Print this project's web address
|
|
59
64
|
|
|
65
|
+
FPGA boards
|
|
66
|
+
tapeo flash [--board <id>] [--flash]
|
|
67
|
+
Download the bitstream and program the connected board with
|
|
68
|
+
openFPGALoader (--flash: keep it after power-off on boards
|
|
69
|
+
that load to SRAM by default, such as the Tang Nano 9K)
|
|
70
|
+
tapeo monitor [--port <path>] [--baud <n>]
|
|
71
|
+
Show the board's serial (UART) output and send what you
|
|
72
|
+
type; default 115200 baud (macOS and Linux)
|
|
73
|
+
|
|
60
74
|
Stages: simulation, synthesis, timing, physical, post-route-timing, drc, extraction, erc, lvs.
|
|
61
75
|
TAPEO_URL and TAPEO_TOKEN override the saved sign-in (for automation).`);
|
|
62
76
|
}
|
|
@@ -121,7 +135,7 @@ async function configuredClient() {
|
|
|
121
135
|
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
122
136
|
const serverUrl = process.env.TAPEO_URL || saved.serverUrl;
|
|
123
137
|
const token = process.env.TAPEO_TOKEN || saved.token;
|
|
124
|
-
if (!serverUrl || !token) throw new Error("Run `tapeo login <server-url>`
|
|
138
|
+
if (!serverUrl || !token) throw new Error("Run `tapeo login` first (or `tapeo login <server-url>` for your own server), or set TAPEO_URL and TAPEO_TOKEN.");
|
|
125
139
|
return new TapeoClient(serverUrl, token);
|
|
126
140
|
}
|
|
127
141
|
|
|
@@ -283,8 +297,29 @@ async function main() {
|
|
|
283
297
|
console.log("Signed out on this computer. Revoke the token on the Account page to end its access everywhere.");
|
|
284
298
|
return;
|
|
285
299
|
}
|
|
300
|
+
// The serial monitor talks only to the board, so it needs no sign-in.
|
|
301
|
+
if (command === "monitor") {
|
|
302
|
+
const { flags } = parseArguments(rest, ["port", "baud"]);
|
|
303
|
+
const port = chooseSerialPort(serialCandidates(), typeof flags.port === "string" ? flags.port : undefined);
|
|
304
|
+
const baud = parseBaud(flags.baud);
|
|
305
|
+
const serial = openSerialPort(port, baud);
|
|
306
|
+
console.error(`Connected to ${port} at ${baud} baud. Type a line and press Enter to send it; Ctrl+C stops.`);
|
|
307
|
+
serial.input.pipe(process.stdout);
|
|
308
|
+
process.stdin.setEncoding("utf8");
|
|
309
|
+
process.stdin.on("data", (text) => serial.write(text));
|
|
310
|
+
await new Promise((resolve) => {
|
|
311
|
+
const stop = () => { serial.close(); resolve(); };
|
|
312
|
+
process.once("SIGINT", stop);
|
|
313
|
+
// Ctrl+D at a keyboard stops; piped input just sends and keeps listening.
|
|
314
|
+
if (process.stdin.isTTY) process.stdin.once("end", stop);
|
|
315
|
+
serial.input.once("close", stop);
|
|
316
|
+
serial.input.once("error", (error) => { console.error(`The serial port closed: ${error.message}`); stop(); });
|
|
317
|
+
});
|
|
318
|
+
process.stdin.destroy();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
286
321
|
const client = await configuredClient();
|
|
287
|
-
const { flags, positional: args } = parseArguments(rest, ["stage", "from", "option", "limit"]);
|
|
322
|
+
const { flags, positional: args } = parseArguments(rest, ["stage", "from", "option", "limit", "board"]);
|
|
288
323
|
if (command === "new") {
|
|
289
324
|
await newDesign(client, args[0], path.resolve(args[1] || "."), flags);
|
|
290
325
|
} else if (command === "ask") {
|
|
@@ -337,6 +372,24 @@ async function main() {
|
|
|
337
372
|
const { projects } = await client.projects();
|
|
338
373
|
for (const project of projects) console.log(`${project.id}\t${project.name}`);
|
|
339
374
|
if (!projects.length) console.log("No projects yet. Run tapeo create <name> [directory].");
|
|
375
|
+
} else if (command === "delete") {
|
|
376
|
+
const directory = process.cwd();
|
|
377
|
+
const projectId = await projectIdFor(client, args[0]);
|
|
378
|
+
const { project } = await client.project(projectId);
|
|
379
|
+
if (!flags.yes) {
|
|
380
|
+
if (!process.stdin.isTTY) throw new Error(`Deleting "${project.name}" cannot be undone. Confirm with --yes.`);
|
|
381
|
+
const prompt = prompter();
|
|
382
|
+
try {
|
|
383
|
+
console.log(`This deletes "${project.name}" (${project.id}) on ${client.serverUrl} for good: its files, checkpoints, runs, layouts, and Engineer history. A running workflow stops. Local files are kept.`);
|
|
384
|
+
const typed = (await prompt.ask("Type the project name to confirm: ")).trim();
|
|
385
|
+
if (typed !== project.name.trim()) { console.log("The name did not match. Nothing was deleted."); return; }
|
|
386
|
+
} finally { prompt.close(); }
|
|
387
|
+
}
|
|
388
|
+
await client.deleteProject(projectId);
|
|
389
|
+
// A folder bound to the deleted project is unbound, so later commands do not point at it.
|
|
390
|
+
const bound = await readState(directory).catch(() => null);
|
|
391
|
+
if (bound?.projectId === projectId && bound.serverUrl === client.serverUrl) await rm(path.join(directory, ".tapeo"), { recursive: true, force: true });
|
|
392
|
+
console.log(`Deleted "${project.name}".${bound?.projectId === projectId ? " This folder is no longer linked to a Tapeo project; its files are unchanged." : ""}`);
|
|
340
393
|
} else if (command === "create") {
|
|
341
394
|
if (!args[0]?.trim()) throw new Error("Usage: tapeo create <name> [directory]");
|
|
342
395
|
const directory = path.resolve(args[1] || ".");
|
|
@@ -435,6 +488,18 @@ async function main() {
|
|
|
435
488
|
}
|
|
436
489
|
const { run } = await client.cancelRun(projectId, runId);
|
|
437
490
|
console.log(`Cancelled ${run.id}. A tool step already running finishes first; finished stages keep their results.`);
|
|
491
|
+
} else if (command === "flash") {
|
|
492
|
+
const directory = process.cwd();
|
|
493
|
+
const projectId = await projectIdFor(client);
|
|
494
|
+
const { files } = await client.project(projectId);
|
|
495
|
+
const bitstream = chooseBitstream(files, flags.board);
|
|
496
|
+
const executable = openFpgaLoaderExecutable();
|
|
497
|
+
if (!openFpgaLoaderAvailable(executable)) throw new Error(OPENFPGALOADER_INSTALL);
|
|
498
|
+
const local = await downloadBitstream(client, projectId, bitstream, directory);
|
|
499
|
+
console.log(`Programming the ${bitstream.label} with ${path.relative(directory, local)}${flags.flash ? " (writing to flash)" : ""}…`);
|
|
500
|
+
const result = spawnSync(executable, flashArguments(bitstream, local, { flash: Boolean(flags.flash) }), { stdio: "inherit" });
|
|
501
|
+
if (result.error || result.status !== 0) throw new Error("openFPGALoader could not program the board. Check the USB cable and that no other program is using the board; its messages are above.");
|
|
502
|
+
console.log(`Programmed the ${bitstream.label}.`);
|
|
438
503
|
} else if (command === "open") {
|
|
439
504
|
const projectId = await projectIdFor(client, args[0]);
|
|
440
505
|
console.log(`${client.serverUrl}/?project=${encodeURIComponent(projectId)}`);
|
package/package.json
CHANGED
package/shared/api.mjs
CHANGED
|
@@ -182,6 +182,16 @@ export class TapeoClient {
|
|
|
182
182
|
projects() { return this.request("GET", "/projects"); }
|
|
183
183
|
createProject(name, description = "", hardwareType = "Undecided") { return this.request("POST", "/projects", { name, description, hardwareType }); }
|
|
184
184
|
project(id) { return this.request("GET", `/projects/${encodeURIComponent(id)}`); }
|
|
185
|
+
/** Delete a project on the server for good (files, runs, history). */
|
|
186
|
+
async deleteProject(id) {
|
|
187
|
+
try {
|
|
188
|
+
return await this.request("DELETE", `/projects/${encodeURIComponent(id)}`);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Servers older than this client have no delete route for local clients.
|
|
191
|
+
if (error?.status === 405) throw new TapeoApiError(`This Tapeo server cannot delete projects from the terminal yet. Delete it in the browser: open ${this.serverUrl}, then use the delete button next to the project in your project list.`, 405);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
185
195
|
file(id, path) { return this.request("GET", `/projects/${encodeURIComponent(id)}/files?path=${encodeURIComponent(path)}`); }
|
|
186
196
|
saveFile(id, path, content, baseSha256, language) { return this.request("PUT", `/projects/${encodeURIComponent(id)}/files?path=${encodeURIComponent(path)}`, { content, baseSha256, language }); }
|
|
187
197
|
deleteFile(id, path, baseSha256) { return this.request("DELETE", `/projects/${encodeURIComponent(id)}/files?path=${encodeURIComponent(path)}&baseSha256=${encodeURIComponent(baseSha256)}`); }
|
package/shared/fpga.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { localTarget } from "./sync.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Programming an FPGA board from the local clients (tapeo flash, and Tapeo:
|
|
8
|
+
* Program FPGA Board in VS Code). The bitstream is built on the server; this
|
|
9
|
+
* downloads it and runs the open-source openFPGALoader on your computer.
|
|
10
|
+
* Board names mirror lib/fpga-boards.ts (tests/client-fpga.test.ts keeps them in sync).
|
|
11
|
+
*/
|
|
12
|
+
export const FPGA_PROGRAMMERS = {
|
|
13
|
+
tangnano9k: { label: "Sipeed Tang Nano 9K", openFpgaLoaderBoard: "tangnano9k" },
|
|
14
|
+
tangnano20k: { label: "Sipeed Tang Nano 20K", openFpgaLoaderBoard: "tangnano20k" },
|
|
15
|
+
icebreaker: { label: "1BitSquared iCEBreaker", openFpgaLoaderBoard: "ice40_generic" },
|
|
16
|
+
ulx3s12f: { label: "Radiona ULX3S (12F)", openFpgaLoaderBoard: "ulx3s" },
|
|
17
|
+
ulx3s85f: { label: "Radiona ULX3S (85F)", openFpgaLoaderBoard: "ulx3s" }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const OPENFPGALOADER_INSTALL = [
|
|
21
|
+
"openFPGALoader is needed to program the board. Install it, then run this again:",
|
|
22
|
+
" macOS: brew install openfpgaloader",
|
|
23
|
+
" Debian/Ubuntu: sudo apt install openfpgaloader",
|
|
24
|
+
" Windows/other: it is included in OSS CAD Suite (https://github.com/YosysHQ/oss-cad-suite-build)"
|
|
25
|
+
].join("\n");
|
|
26
|
+
|
|
27
|
+
/** The bitstreams a project has, newest builds per board (paths from the server manifest). */
|
|
28
|
+
export function findBitstreams(files) {
|
|
29
|
+
return files.flatMap((file) => {
|
|
30
|
+
const match = /^generated\/fpga\/([a-z0-9]+)\/board_top\.(fs|bin|bit)$/.exec(file.path);
|
|
31
|
+
return match && FPGA_PROGRAMMERS[match[1]] ? [{ path: file.path, boardId: match[1], ...FPGA_PROGRAMMERS[match[1]] }] : [];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pick the board to program: the only one built, or the one named with --board. */
|
|
36
|
+
export function chooseBitstream(files, boardId) {
|
|
37
|
+
const bitstreams = findBitstreams(files);
|
|
38
|
+
if (!bitstreams.length) throw new Error("This project has no bitstream yet. Run the workflow first (tapeo run --watch); FPGA projects build one for their board.");
|
|
39
|
+
if (boardId) {
|
|
40
|
+
const chosen = bitstreams.find((item) => item.boardId === boardId);
|
|
41
|
+
if (!chosen) throw new Error(`No bitstream for ${boardId}. Built for: ${bitstreams.map((item) => item.boardId).join(", ")}.`);
|
|
42
|
+
return chosen;
|
|
43
|
+
}
|
|
44
|
+
if (bitstreams.length > 1) throw new Error(`Bitstreams exist for several boards (${bitstreams.map((item) => item.boardId).join(", ")}). Name one with --board.`);
|
|
45
|
+
return bitstreams[0];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Download the bitstream into the project folder (read-only, like other tool outputs). */
|
|
49
|
+
export async function downloadBitstream(client, projectId, bitstream, directory) {
|
|
50
|
+
const { bytes } = await client.rawFile(projectId, bitstream.path);
|
|
51
|
+
const target = await localTarget(directory, bitstream.path);
|
|
52
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
53
|
+
await chmod(target, 0o644).catch(() => undefined);
|
|
54
|
+
await writeFile(target, bytes);
|
|
55
|
+
await chmod(target, 0o444).catch(() => undefined);
|
|
56
|
+
return target;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The programmer to run; TAPEO_OPENFPGALOADER overrides the executable (for example a full path). */
|
|
60
|
+
export function openFpgaLoaderExecutable() {
|
|
61
|
+
return process.env.TAPEO_OPENFPGALOADER || "openFPGALoader";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function openFpgaLoaderAvailable(executable = openFpgaLoaderExecutable()) {
|
|
65
|
+
const result = spawnSync(executable, ["-V"], { encoding: "utf8" });
|
|
66
|
+
return !result.error && result.status === 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** openFPGALoader arguments: SRAM by default (lost at power-off), flash with `flash: true`. */
|
|
70
|
+
export function flashArguments(bitstream, localPath, { flash = false } = {}) {
|
|
71
|
+
return ["-b", bitstream.openFpgaLoaderBoard, ...(flash ? ["--write-flash"] : []), localPath];
|
|
72
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { closeSync, constants, openSync, readdirSync, writeSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import tty from "node:tty";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A serial monitor for a connected FPGA board (tapeo monitor, and Tapeo: Open
|
|
7
|
+
* Serial Monitor in VS Code), without native modules: the port is opened as a
|
|
8
|
+
* file, configured with the system's stty on that open descriptor, and read as
|
|
9
|
+
* a terminal stream. macOS and Linux only; on Windows, use the browser's serial
|
|
10
|
+
* monitor on the Workflow tab (Chrome or Edge).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_BAUD = 115200;
|
|
14
|
+
export const SERIAL_UNSUPPORTED = "The serial monitor runs on macOS and Linux. On Windows, open the project's Workflow tab in Chrome or Edge and use its Serial monitor.";
|
|
15
|
+
|
|
16
|
+
/** USB serial ports that may be a board, newest naming first. macOS uses the cu.* (call-out) devices, which do not wait for a carrier. */
|
|
17
|
+
export function serialCandidates(platform = process.platform, names = safeList("/dev")) {
|
|
18
|
+
const pattern = platform === "darwin" ? /^cu\.(?:usbserial|usbmodem|wchusbserial|SLAB_USBtoUART)/ : platform === "linux" ? /^tty(?:USB|ACM)\d+$/ : null;
|
|
19
|
+
if (!pattern) return [];
|
|
20
|
+
return names.filter((name) => pattern.test(name)).sort().map((name) => `/dev/${name}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function safeList(directory) {
|
|
24
|
+
try { return readdirSync(directory); } catch { return []; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The port to open: the one named, the only one found, or an explanation. */
|
|
28
|
+
export function chooseSerialPort(candidates, requested) {
|
|
29
|
+
if (requested) return requested;
|
|
30
|
+
if (!candidates.length) throw new Error("No USB serial port was found. Connect the board by USB, or name the port with --port.");
|
|
31
|
+
if (candidates.length > 1) throw new Error(`Several serial ports were found:\n${candidates.map((item) => ` ${item}`).join("\n")}\nName one with --port. A Tang Nano shows two; its serial link is usually the second.`);
|
|
32
|
+
return candidates[0];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseBaud(value) {
|
|
36
|
+
if (value === undefined || value === true) return DEFAULT_BAUD;
|
|
37
|
+
const baud = Number(value);
|
|
38
|
+
if (!Number.isInteger(baud) || baud < 300 || baud > 4_000_000) throw new Error(`--baud must be a whole number from 300 to 4000000 (got ${value}).`);
|
|
39
|
+
return baud;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** stty settings for 8N1 raw data at `baud`, no flow control, ignoring modem lines. */
|
|
43
|
+
export function sttyArguments(baud) {
|
|
44
|
+
return [String(baud), "raw", "-echo", "cs8", "-cstopb", "-parenb", "clocal", "-crtscts", "-ixon", "-ixoff"];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Open `portPath` at `baud`. Returns the incoming stream, a write function, and close.
|
|
49
|
+
* The settings are applied while the port is open, so the driver keeps them.
|
|
50
|
+
*/
|
|
51
|
+
export function openSerialPort(portPath, baud = DEFAULT_BAUD, platform = process.platform) {
|
|
52
|
+
if (platform !== "darwin" && platform !== "linux") throw new Error(SERIAL_UNSUPPORTED);
|
|
53
|
+
let readFd;
|
|
54
|
+
try {
|
|
55
|
+
readFd = openSync(portPath, constants.O_RDWR | constants.O_NOCTTY | constants.O_NONBLOCK);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(error?.code === "EBUSY" || error?.code === "EACCES"
|
|
58
|
+
? `Could not open ${portPath}: ${error.code === "EBUSY" ? "another program is using it (close other serial monitors)" : "permission denied (on Linux, add yourself to the dialout group, then sign in again)"}.`
|
|
59
|
+
: `Could not open ${portPath}: ${error?.message ?? error}`);
|
|
60
|
+
}
|
|
61
|
+
if (!tty.isatty(readFd)) { closeSync(readFd); throw new Error(`${portPath} is not a serial port.`); }
|
|
62
|
+
const configured = spawnSync("stty", sttyArguments(baud), { stdio: [readFd, "ignore", "pipe"], encoding: "utf8" });
|
|
63
|
+
if (configured.error || configured.status !== 0) { closeSync(readFd); throw new Error(`Could not set ${portPath} to ${baud} baud: ${configured.stderr?.trim() || configured.error?.message || "stty failed"}.`); }
|
|
64
|
+
const writeFd = openSync(portPath, constants.O_WRONLY | constants.O_NOCTTY);
|
|
65
|
+
const input = new tty.ReadStream(readFd);
|
|
66
|
+
let closed = false;
|
|
67
|
+
return {
|
|
68
|
+
input,
|
|
69
|
+
write(data) { if (!closed) writeSync(writeFd, typeof data === "string" ? Buffer.from(data, "utf8") : data); },
|
|
70
|
+
close() {
|
|
71
|
+
if (closed) return;
|
|
72
|
+
closed = true;
|
|
73
|
+
input.destroy();
|
|
74
|
+
try { closeSync(writeFd); } catch { /* already closed */ }
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|