Skip to content

Commit 3c257c4

Browse files
committed
feat: auto create wokwi.toml / diagram.json for esp-idf projects
Also, if the project already has a diagram.json file, ensure it matches the selected esp-idf target.
1 parent 3c2572f commit 3c257c4

11 files changed

+299
-26
lines changed

src/WokwiConfig.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@ export interface WokwiTOML {
88
version: number;
99
firmware: string;
1010
elf: string;
11+
gdbServerPort?: number;
1112
};
1213
chip?: WokwiTOMLChip[];
1314
}
15+
16+
export interface WokwiTOMLConfig {
17+
firmwarePath: string;
18+
elfPath: string;
19+
gdbServerPort?: number;
20+
}
21+
22+
export function createWokwiToml(config: WokwiTOMLConfig) {
23+
const { firmwarePath, elfPath, gdbServerPort } = config;
24+
const tomlContent = [
25+
`# Wokwi Configuration File`,
26+
`# Reference: https://docs.wokwi.com/vscode/project-config`,
27+
`[wokwi]`,
28+
`version = 1`,
29+
`firmware = '${firmwarePath}'`,
30+
`elf = '${elfPath}'`,
31+
];
32+
if (gdbServerPort) {
33+
tomlContent.push(`gdbServerPort = ${gdbServerPort}`);
34+
}
35+
return tomlContent.join('\n') + '\n';
36+
}
File renamed without changes.

src/esp/idfProjectConfig.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { createConfigForIDFProject } from './idfProjectConfig.js';
3+
4+
describe('createConfigForIDFProject', () => {
5+
it('should create a config for an IDF project', () => {
6+
const config = createConfigForIDFProject({
7+
project_path: '/dev/esp/idf-master/examples/get-started/hello_world',
8+
build_dir: '/dev/esp/idf-master/examples/get-started/hello_world/build',
9+
app_elf: 'hello_world.elf',
10+
});
11+
expect(config).toMatchInlineSnapshot(`
12+
"# Wokwi Configuration File
13+
# Reference: https://docs.wokwi.com/vscode/project-config
14+
[wokwi]
15+
version = 1
16+
firmware = 'build/flasher_args.json'
17+
elf = 'build/hello_world.elf'
18+
gdbServerPort = 3333
19+
"
20+
`);
21+
});
22+
});

src/esp/idfProjectConfig.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import chalkTemplate from 'chalk-template';
2+
import { readFileSync, writeFileSync } from 'fs';
3+
import path from 'path';
4+
import { boards } from '../project/boards.js';
5+
import { createDiagram } from '../project/createDiagram.js';
6+
import { findBoard } from '../project/findBoard.js';
7+
import { createWokwiToml } from '../WokwiConfig.js';
8+
import { type ESPIDFProjectDescription } from './projectDescription.js';
9+
10+
type ICreateConfigForIDFProjectParams = Pick<
11+
ESPIDFProjectDescription,
12+
'build_dir' | 'project_path' | 'app_elf'
13+
>;
14+
15+
export function createConfigForIDFProject(idfProjectDescription: ICreateConfigForIDFProjectParams) {
16+
// eslint-disable-next-line @typescript-eslint/naming-convention
17+
const { build_dir, project_path, app_elf } = idfProjectDescription;
18+
const relativeBuildDir = path.relative(project_path, build_dir);
19+
20+
return createWokwiToml({
21+
firmwarePath: relativeBuildDir + '/' + 'flasher_args.json',
22+
elfPath: relativeBuildDir + '/' + app_elf,
23+
gdbServerPort: 3333,
24+
});
25+
}
26+
27+
type ICreateDiagramForIDFProjectParams = Pick<ESPIDFProjectDescription, 'target'>;
28+
29+
export function createDiagramForIDFProject(
30+
idfProjectDescription: ICreateDiagramForIDFProjectParams,
31+
) {
32+
const { target } = idfProjectDescription;
33+
const board = boards.find((b) => b.idfTarget === target);
34+
if (!board) {
35+
throw new Error(
36+
`Target ${target} is not currently supported by Wokwi. You can create a feature request at https://github.com/wokwi/wokwi-features/issues.`,
37+
);
38+
}
39+
return createDiagram(board.board);
40+
}
41+
42+
export interface IDFProjectConfigParams {
43+
rootDir: string;
44+
configPath: string;
45+
diagramFilePath: string;
46+
projectDescriptionPath: string;
47+
createConfig?: boolean;
48+
createDiagram?: boolean;
49+
quiet?: boolean;
50+
}
51+
52+
export function idfProjectConfig(params: IDFProjectConfigParams) {
53+
const {
54+
rootDir,
55+
configPath,
56+
diagramFilePath,
57+
projectDescriptionPath,
58+
createConfig,
59+
createDiagram,
60+
quiet,
61+
} = params;
62+
const espIdfProjectDescriptionContent = readFileSync(projectDescriptionPath, 'utf8');
63+
const idfProjectDescription = JSON.parse(espIdfProjectDescriptionContent);
64+
if (createConfig) {
65+
const wokwiConfig = createConfigForIDFProject(idfProjectDescription);
66+
writeFileSync(configPath, wokwiConfig);
67+
if (!quiet) {
68+
console.log(
69+
chalkTemplate`Created default {yellow wokwi.toml} for IDF project in {yellow ${rootDir}}.`,
70+
);
71+
}
72+
}
73+
if (!createDiagram) {
74+
const diagramContent = readFileSync(diagramFilePath, 'utf8');
75+
const diagram = JSON.parse(diagramContent);
76+
const board = findBoard(diagram);
77+
const boardInfo = boards.find((b) => b.board === board?.type);
78+
if (boardInfo && boardInfo.idfTarget !== idfProjectDescription.target) {
79+
console.error(
80+
chalkTemplate`{red Error:} The IDF project is targeting {yellow ${idfProjectDescription.target}}, but the diagram is for {yellow ${boardInfo.idfTarget}}.`,
81+
);
82+
console.error(
83+
chalkTemplate`You can use the {green --diagram-file} option to specify a diagram for a different board, or delete the {yellow diagram.json} file to automatically create a default diagram.`,
84+
);
85+
return false;
86+
}
87+
} else {
88+
const diagram = createDiagramForIDFProject(idfProjectDescription);
89+
writeFileSync(diagramFilePath, JSON.stringify(diagram, null, 2));
90+
if (!quiet) {
91+
console.log(
92+
chalkTemplate`Created default {yellow diagram.json} for IDF project in {yellow ${rootDir}}.`,
93+
);
94+
}
95+
}
96+
97+
return true;
98+
}

src/esp/projectDescription.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export interface ESPIDFProjectDescription {
2+
version: string;
3+
project_name: string;
4+
project_version: string;
5+
project_path: string;
6+
idf_path: string;
7+
build_dir: string;
8+
config_file: string;
9+
config_defaults: string;
10+
bootloader_elf: string;
11+
app_elf: string;
12+
app_bin: string;
13+
build_type: string;
14+
git_revision: string;
15+
target: string;
16+
rev: string;
17+
min_rev: string;
18+
max_rev: string;
19+
phy_data_partition: string;
20+
monitor_baud: string;
21+
monitor_toolprefix: string;
22+
c_compiler: string;
23+
config_environment: Record<string, string>;
24+
common_components: string[];
25+
build_components: string[];
26+
build_component_paths: string[];
27+
build_component_info: Record<string, ESPIDFComponentInfo>;
28+
all_component_info: Record<string, ESPIDFComponentInfo>;
29+
debug_prefix_map_gdbinit: string;
30+
gdbinit_files: Record<string, string>;
31+
debug_arguments_openocd: string;
32+
}
33+
34+
export interface ESPIDFComponentInfo {
35+
alias: string;
36+
target: string;
37+
prefix: string;
38+
dir: string;
39+
lib: string;
40+
reqs: string[];
41+
priv_reqs: string[];
42+
managed_reqs: string[];
43+
managed_priv_reqs: string[];
44+
include_dirs: string[];
45+
}

src/main.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import { EventManager } from './EventManager.js';
99
import { ExpectEngine } from './ExpectEngine.js';
1010
import { TestScenario } from './TestScenario.js';
1111
import { parseConfig } from './config.js';
12+
import { idfProjectConfig } from './esp/idfProjectConfig.js';
1213
import { cliHelp } from './help.js';
13-
import { initProjectWizard } from './project/initProjectWizard.js';
1414
import { loadChips } from './loadChips.js';
15+
import { initProjectWizard } from './project/initProjectWizard.js';
1516
import { readVersion } from './readVersion.js';
1617
import { DelayCommand } from './scenario/DelayCommand.js';
1718
import { ExpectPinCommand } from './scenario/ExpectPinCommand.js';
@@ -101,7 +102,33 @@ async function main() {
101102
const rootDir = args._[0] || '.';
102103
const configPath = path.join(rootDir, 'wokwi.toml');
103104
const diagramFilePath = path.resolve(rootDir, diagramFile ?? 'diagram.json');
104-
const configExists = existsSync(configPath);
105+
const espIdfFlasherArgsPath = path.resolve(rootDir, 'build/flasher_args.json');
106+
const espIdfProjectDescriptionPath = path.resolve(rootDir, 'build/project_description.json');
107+
const isIDFProject =
108+
existsSync(espIdfFlasherArgsPath) && existsSync(espIdfProjectDescriptionPath);
109+
let configExists = existsSync(configPath);
110+
let diagramExists = existsSync(diagramFilePath);
111+
112+
if (isIDFProject) {
113+
if (!quiet) {
114+
console.log(`Detected IDF project in ${rootDir}`);
115+
}
116+
if (
117+
!idfProjectConfig({
118+
rootDir,
119+
configPath,
120+
diagramFilePath,
121+
projectDescriptionPath: espIdfProjectDescriptionPath,
122+
createConfig: !configExists,
123+
createDiagram: !diagramExists,
124+
quiet,
125+
})
126+
) {
127+
process.exit(1);
128+
}
129+
configExists = true;
130+
diagramExists = true;
131+
}
105132

106133
if (!elf && !configExists) {
107134
console.error(

src/project/boards.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,51 @@
1-
export const boards = [
1+
export interface IBoard {
2+
title: string;
3+
board: string;
4+
family: string;
5+
idfTarget?: string;
6+
serialPins?: { RX: string; TX: string };
7+
}
8+
9+
export const boards: IBoard[] = [
210
// ESP32 DevKits
3-
{ title: 'ESP32 DevKit', board: 'board-esp32-devkit-c-v4', family: 'esp32' },
4-
{ title: 'ESP32-C3 DevKit', board: 'board-esp32-c3-devkitm-1', family: 'esp32' },
5-
{ title: 'ESP32-C6 DevKit', board: 'board-esp32-c6-devkitc-1', family: 'esp32' },
6-
{ title: 'ESP32-H2 DevKit', board: 'board-esp32-h2-devkitm-1', family: 'esp32' },
11+
{ title: 'ESP32 DevKit', board: 'board-esp32-devkit-c-v4', family: 'esp32', idfTarget: 'esp32' },
12+
{
13+
title: 'ESP32-C3 DevKit',
14+
board: 'board-esp32-c3-devkitm-1',
15+
family: 'esp32',
16+
idfTarget: 'esp32c3',
17+
},
18+
{
19+
title: 'ESP32-C6 DevKit',
20+
board: 'board-esp32-c6-devkitc-1',
21+
family: 'esp32',
22+
idfTarget: 'esp32c6',
23+
},
24+
{
25+
title: 'ESP32-H2 DevKit',
26+
board: 'board-esp32-h2-devkitm-1',
27+
family: 'esp32',
28+
idfTarget: 'esp32h2',
29+
},
730
{
831
title: 'ESP32-P4-Function-EV-Board',
932
board: 'board-esp32-p4-function-ev',
1033
family: 'esp32',
34+
idfTarget: 'esp32p4',
1135
serialPins: { RX: '38', TX: '37' },
1236
},
13-
{ title: 'ESP32-S2 DevKit', board: 'board-esp32-s2-devkitm-1', family: 'esp32' },
14-
{ title: 'ESP32-S3 DevKit', board: 'board-esp32-s3-devkitc-1', family: 'esp32' },
37+
{
38+
title: 'ESP32-S2 DevKit',
39+
board: 'board-esp32-s2-devkitm-1',
40+
family: 'esp32',
41+
idfTarget: 'esp32s2',
42+
},
43+
{
44+
title: 'ESP32-S3 DevKit',
45+
board: 'board-esp32-s3-devkitc-1',
46+
family: 'esp32',
47+
idfTarget: 'esp32s3',
48+
},
1549

1650
// ESP32-based boards
1751
{ title: 'ESP32-C3 Rust DevKit', board: 'board-esp32-c3-rust-1', family: 'esp32' },

src/project/diagram.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export interface Diagram {
2+
version: number;
3+
author?: string;
4+
editor?: string;
5+
parts: Part[];
6+
connections: Connection[];
7+
serialMonitor?: SerialMonitorConfig;
8+
dependencies: Record<string, string>;
9+
}
10+
11+
export interface Part {
12+
type: string;
13+
id: string;
14+
top?: number;
15+
left?: number;
16+
rotate?: number;
17+
attrs?: Record<string, string>;
18+
}
19+
20+
export type Connection = [string, string, string, string[]];
21+
22+
export interface SerialMonitorConfig {
23+
display?: 'never' | 'always' | 'auto' | 'plotter' | 'terminal';
24+
newline?: 'none' | 'cr' | 'lf' | 'crlf';
25+
convertEol?: boolean;
26+
}

src/project/findBoard.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { type Diagram } from './diagram.js';
2+
import { boards } from './boards.js';
3+
4+
export function findBoard(diagram: Diagram) {
5+
return diagram.parts?.find((part) => boards.some((b) => b.board === part.type));
6+
}

src/project/initProjectWizard.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { boards } from './boards.js';
66
import { createDiagram } from './createDiagram.js';
77
import { findFirmwarePath } from './findFirmwarePath.js';
88
import { detectProjectType } from './projectType.js';
9+
import { createWokwiToml } from '../WokwiConfig.js';
910

1011
export async function initProjectWizard(rootDir: string, opts: { diagramFile?: string }) {
1112
const configPath = path.join(rootDir, 'wokwi.toml');
@@ -46,8 +47,8 @@ export async function initProjectWizard(rootDir: string, opts: { diagramFile?: s
4647
projectType === 'esp-idf'
4748
? boards.filter((board) => board.family === 'esp32')
4849
: projectType === 'pico-sdk'
49-
? boards.filter((board) => board.family === 'rp2')
50-
: boards;
50+
? boards.filter((board) => board.family === 'rp2')
51+
: boards;
5152

5253
const boardType = await select({
5354
message: 'Select the board to simulate:',
@@ -121,20 +122,11 @@ export async function initProjectWizard(rootDir: string, opts: { diagramFile?: s
121122
}
122123
}
123124

124-
const tomlContent = [
125-
`# Wokwi Configuration File`,
126-
`# Reference: https://docs.wokwi.com/vscode/project-config`,
127-
`[wokwi]`,
128-
`version = 1`,
129-
`firmware = '${firmwarePath}'`,
130-
`elf = '${elfPath}'`,
131-
];
132-
if (vsCodeDebug) {
133-
tomlContent.push(`gdbServerPort=3333`);
134-
}
135-
136125
log.info(`Writing wokwi.toml...`);
137-
writeFileSync(configPath, tomlContent.join('\n') + '\n');
126+
writeFileSync(
127+
configPath,
128+
createWokwiToml({ firmwarePath, elfPath, gdbServerPort: vsCodeDebug ? 3333 : undefined }),
129+
);
138130

139131
log.info(`Writing diagram.json...`);
140132
writeFileSync(diagramFilePath, JSON.stringify(createDiagram(boardType as string), null, 2));

0 commit comments

Comments
 (0)