feat(esp32): pure ESP-IDF language mode for the ESP32 family (#139)
Adds a third entry to the board language selector next to Arduino C++
and MicroPython: ESP-IDF. In this mode the user writes a plain ESP-IDF
project — app_main() entry point, FreeRTOS + driver APIs — and the
backend compiles it through the same ESP-IDF toolchain it already uses
for ESP32 Arduino sketches, just without the arduino-esp32 component.
Backend:
- CompileRequest.language ('espidf') threaded through the sync + async
compile paths and folded into the dedup job key (language='arduino'
and omitted hash identically so old clients keep dedupping).
- espidf_compiler: pure_idf flag. User files are written into main/
as-is (no Arduino.h wrap, no velxio_compat.h, Arduino library
resolution skipped), ARDUINO_ESP32_PATH is dropped from the build env
and VELXIO_PURE_SKETCH raised so the template CMake compiles the
user's own sources via a glob branch. Pure builds get their own
persistent build-dir variant through the eff_hash fold.
- QEMU WiFi compat for IDF-style code: esp_wifi.h/esp_wifi_init
detection sets has_wifi, and literal #define SSID/PASS plus
wifi_config_t designated initializers are normalized to the QEMU AP.
- CONFIG_ARDUINO_* lines are stripped from sdkconfig.defaults in pure
mode (the symbols don't exist without the arduino component).
Frontend:
- LanguageMode gains 'espidf'; BOARD_SUPPORTS_ESPIDF covers the ESP32
family (Xtensa, S3, C3). Toolbar shows the option only for those.
- Switching modes seeds a main.c blink skeleton (app_main + gpio
driver), mirroring the MicroPython main.py flow.
- compileCode sends language='espidf'; run/stop paths are unchanged
(the QEMU worker consumes the same merged flash image).
- New gallery example: esp32-idf-blink (LED + resistor on GPIO 2).
Tests: unit coverage for the build-env switch, IDF wifi normalization,
job-key variance, file-group seeding and the new example; verified
end-to-end in a container from the prod image (pure build produces a
bootable flash image; Arduino-mode build unchanged, same variant hash).
This commit is contained in:
parent
4766789632
commit
734b7d0487
|
|
@ -62,6 +62,7 @@ def _job_key(
|
|||
spiffs_files: list[dict] | None = None,
|
||||
libraries: list[str] | None = None,
|
||||
owner_id: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> str:
|
||||
"""Stable content hash of (files, board, options, spiffs, libraries, owner)
|
||||
used as the deduplication key.
|
||||
|
|
@ -111,6 +112,13 @@ def _job_key(
|
|||
h.update(b"owner:")
|
||||
h.update(owner_id.encode())
|
||||
h.update(b"\0")
|
||||
if language and language != "arduino":
|
||||
# Pure ESP-IDF mode produces a different binary from the same bytes —
|
||||
# never dedup across language modes. Guarded so 'arduino' (explicit or
|
||||
# omitted) keeps the historical key.
|
||||
h.update(b"lang:")
|
||||
h.update(language.encode())
|
||||
h.update(b"\0")
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
|
|
@ -168,6 +176,11 @@ class CompileRequest(BaseModel):
|
|||
# merged only if it's declared here, so a sketch never picks up an unrelated
|
||||
# library from the shared dir. None / omitted = legacy scan-all (unchanged).
|
||||
libraries: list[str] | None = None
|
||||
# Pure ESP-IDF language mode (issue #139). 'espidf' compiles the files as
|
||||
# a pure ESP-IDF project: the user provides app_main() and IDF APIs, and
|
||||
# the arduino-esp32 component is left out of the build entirely. None /
|
||||
# 'arduino' = classic Arduino sketch compile. ESP32 boards only.
|
||||
language: str | None = None
|
||||
|
||||
|
||||
class CompileResponse(BaseModel):
|
||||
|
|
@ -298,8 +311,27 @@ async def _run_compile(
|
|||
else:
|
||||
allowed_libraries, owner_id = scope
|
||||
|
||||
pure_idf = request.language == "espidf"
|
||||
if pure_idf and not request.board_fqbn.startswith("esp32:"):
|
||||
return CompileResponse(
|
||||
success=False,
|
||||
stdout="",
|
||||
stderr="",
|
||||
error="ESP-IDF language mode is only supported on ESP32 boards.",
|
||||
)
|
||||
if pure_idf and not espidf_compiler.available:
|
||||
return CompileResponse(
|
||||
success=False,
|
||||
stdout="",
|
||||
stderr="",
|
||||
error="ESP-IDF toolchain is not available on this server.",
|
||||
)
|
||||
|
||||
if request.board_fqbn.startswith("esp32:") and espidf_compiler.available:
|
||||
logger.info(f"[compile] Using ESP-IDF for {request.board_fqbn}")
|
||||
logger.info(
|
||||
f"[compile] Using ESP-IDF for {request.board_fqbn}"
|
||||
+ (" (pure ESP-IDF mode)" if pure_idf else "")
|
||||
)
|
||||
spiffs_dicts = (
|
||||
[f.model_dump() for f in request.spiffs_files]
|
||||
if request.spiffs_files else None
|
||||
|
|
@ -311,6 +343,7 @@ async def _run_compile(
|
|||
spiffs_files=spiffs_dicts,
|
||||
allowed_libraries=allowed_libraries,
|
||||
owner_id=owner_id,
|
||||
pure_idf=pure_idf,
|
||||
)
|
||||
return CompileResponse(
|
||||
success=result["success"],
|
||||
|
|
@ -608,6 +641,7 @@ async def compile_start(
|
|||
files, request.board_fqbn, request.board_options, spiffs_dicts,
|
||||
sorted(allowed_libraries) if allowed_libraries else None,
|
||||
owner_id if allowed_libraries else None,
|
||||
language=request.language,
|
||||
)
|
||||
existing_id = JOB_BY_KEY.get(key)
|
||||
if existing_id is not None:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,37 @@
|
|||
# Pure ESP-IDF language mode (issue #139): the user's files are the main
|
||||
# component sources — they provide app_main() and use IDF APIs directly.
|
||||
# espidf_compiler sets VELXIO_PURE_SKETCH (and drops ARDUINO_ESP32_PATH from
|
||||
# the build env) when the frontend requests language='espidf'. As the `main`
|
||||
# component, ESP-IDF gives these sources an implicit dependency on every
|
||||
# other component in the build, so driver/, esp_wifi, esp_http_server etc.
|
||||
# resolve without an explicit REQUIRES.
|
||||
if(DEFINED ENV{VELXIO_PURE_SKETCH})
|
||||
# No CONFIGURE_DEPENDS — see the comment on the Arduino glob below.
|
||||
file(GLOB _user_srcs
|
||||
"${CMAKE_CURRENT_LIST_DIR}/*.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/*.c")
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${_user_srcs}
|
||||
INCLUDE_DIRS "."
|
||||
)
|
||||
|
||||
# Same -Werror relaxations as the Arduino branch: IDF's defaults are
|
||||
# ruthless for user code pasted from tutorials.
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE
|
||||
-Wno-error=comment
|
||||
-Wno-error=parentheses
|
||||
-Wno-error=sign-compare
|
||||
-Wno-error=narrowing
|
||||
-Wno-error=write-strings
|
||||
-Wno-error=missing-field-initializers
|
||||
-Wno-error=reorder
|
||||
-Wno-error=unused-variable
|
||||
-Wno-error=unused-but-set-variable)
|
||||
# If Arduino component is available, use C++ main and link Arduino.
|
||||
# The component name matches the directory basename of ARDUINO_ESP32_PATH
|
||||
# (e.g. "arduino-esp32" when cloned from GitHub).
|
||||
if(DEFINED ENV{ARDUINO_ESP32_PATH})
|
||||
elseif(DEFINED ENV{ARDUINO_ESP32_PATH})
|
||||
get_filename_component(_arduino_comp_name $ENV{ARDUINO_ESP32_PATH} NAME)
|
||||
|
||||
# arduino-esp32 ships esp32-camera as a precompiled static lib but does
|
||||
|
|
|
|||
|
|
@ -339,6 +339,36 @@ class ESPIDFCompiler:
|
|||
"""Check if sketch uses WiFi."""
|
||||
return bool(re.search(r'#include\s*[<"]WiFi\.h[">]|WiFi\.begin\(', code))
|
||||
|
||||
def _detect_idf_wifi_usage(self, code: str) -> bool:
|
||||
"""Pure ESP-IDF mode: does the project use the esp_wifi stack?"""
|
||||
return bool(re.search(r'#include\s*[<"]esp_wifi\.h[">]|esp_wifi_init\s*\(', code))
|
||||
|
||||
def _normalize_wifi_for_qemu_idf(self, code: str) -> str:
|
||||
"""
|
||||
Pure ESP-IDF variant of _normalize_wifi_for_qemu. IDF projects set
|
||||
credentials via `#define WIFI_SSID "..."` or wifi_config_t designated
|
||||
initializers (`.ssid = "..."`, `.password = "..."`). Rewrite the
|
||||
common literal forms so the firmware associates with the open AP the
|
||||
QEMU fork broadcasts (_QEMU_WIFI_SSID); anything more dynamic
|
||||
(strcpy into the struct at runtime) is left alone and simply won't
|
||||
connect.
|
||||
"""
|
||||
code = re.sub(
|
||||
r'(#define\s+\w*SSID\w*\s+)"[^"]*"',
|
||||
rf'\1"{_QEMU_WIFI_SSID}"',
|
||||
code,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
code = re.sub(
|
||||
r'(#define\s+\w*(?:PASS|PASSWORD|PSK)\w*\s+)"[^"]*"',
|
||||
r'\1""',
|
||||
code,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
code = re.sub(r'(\.ssid\s*=\s*)"[^"]*"', rf'\1"{_QEMU_WIFI_SSID}"', code)
|
||||
code = re.sub(r'(\.password\s*=\s*)"[^"]*"', r'\1""', code)
|
||||
return code
|
||||
|
||||
def _detect_webserver_usage(self, code: str) -> bool:
|
||||
"""Check if sketch uses WebServer."""
|
||||
return bool(re.search(
|
||||
|
|
@ -1211,13 +1241,22 @@ class ESPIDFCompiler:
|
|||
)
|
||||
return safe_name
|
||||
|
||||
def _build_env(self, idf_target: str) -> dict:
|
||||
"""Build environment dict for ESP-IDF subprocess."""
|
||||
def _build_env(self, idf_target: str, pure_idf: bool = False) -> dict:
|
||||
"""Build environment dict for ESP-IDF subprocess.
|
||||
|
||||
pure_idf: omit ARDUINO_ESP32_PATH (the template CMake only pulls the
|
||||
arduino-esp32 component when that var is set) and raise
|
||||
VELXIO_PURE_SKETCH so main/CMakeLists.txt compiles the user's own
|
||||
app_main() sources instead of the Arduino sketch wrapper.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env['IDF_PATH'] = self.idf_path
|
||||
env['IDF_TARGET'] = idf_target
|
||||
|
||||
if self.has_arduino:
|
||||
if pure_idf:
|
||||
env.pop('ARDUINO_ESP32_PATH', None)
|
||||
env['VELXIO_PURE_SKETCH'] = '1'
|
||||
elif self.has_arduino:
|
||||
env['ARDUINO_ESP32_PATH'] = self.arduino_path
|
||||
|
||||
# On Windows, ESP-IDF uses its own Python venv
|
||||
|
|
@ -1745,6 +1784,7 @@ class ESPIDFCompiler:
|
|||
spiffs_files: list[dict] | None = None,
|
||||
allowed_libraries: set[str] | None = None,
|
||||
owner_id: str | None = None,
|
||||
pure_idf: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
Compile Arduino sketch using ESP-IDF.
|
||||
|
|
@ -1777,6 +1817,12 @@ class ESPIDFCompiler:
|
|||
only sdkconfig-affecting options are — because the SPIFFS image is
|
||||
rebuilt on every compile anyway and folding it in would burn the
|
||||
C/C++ ninja cache on every file edit.
|
||||
|
||||
pure_idf: pure ESP-IDF language mode (issue #139). The user's files
|
||||
ARE the IDF main component sources (they provide app_main()); the
|
||||
arduino-esp32 component is left out of the build entirely and
|
||||
Arduino library resolution is skipped. Gets its own persistent
|
||||
build-dir variant via the eff_hash fold below.
|
||||
"""
|
||||
if not self.available:
|
||||
return {
|
||||
|
|
@ -1842,8 +1888,12 @@ class ESPIDFCompiler:
|
|||
('m:' + ','.join(sorted(allowed)) + ('|s:' + scope_token if scope_token else ''))
|
||||
if allowed is not None else 'scanall'
|
||||
)
|
||||
# Pure ESP-IDF mode gets its own build-dir variant: same bytes
|
||||
# compiled with vs without the arduino-esp32 component produce
|
||||
# entirely different cmake graphs + objects.
|
||||
_lang_token = '|lang:pure' if pure_idf else ''
|
||||
eff_hash = hashlib.sha256(
|
||||
(options_hash + '|' + _libs_token + '|i:' + _ext_inc_token).encode()
|
||||
(options_hash + '|' + _libs_token + '|i:' + _ext_inc_token + _lang_token).encode()
|
||||
).hexdigest()[:12]
|
||||
try:
|
||||
if _USE_PERSISTENT_DIR:
|
||||
|
|
@ -1853,6 +1903,7 @@ class ESPIDFCompiler:
|
|||
project_dir, files, idf_target, is_c3,
|
||||
progress_callback, normalized_opts, spiffs_files,
|
||||
allowed_libraries=allowed, libraries_dir=scope_dir,
|
||||
pure_idf=pure_idf,
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix='espidf_') as temp_dir:
|
||||
project_dir = Path(temp_dir) / 'project'
|
||||
|
|
@ -1862,6 +1913,7 @@ class ESPIDFCompiler:
|
|||
project_dir, files, idf_target, is_c3,
|
||||
progress_callback, normalized_opts, spiffs_files,
|
||||
allowed_libraries=allowed, libraries_dir=scope_dir,
|
||||
pure_idf=pure_idf,
|
||||
)
|
||||
finally:
|
||||
if scope_dir is not None:
|
||||
|
|
@ -1882,7 +1934,10 @@ class ESPIDFCompiler:
|
|||
return r2
|
||||
return r
|
||||
|
||||
result = await _attempt_safe(allowed_libraries)
|
||||
# Pure ESP-IDF mode never resolves Arduino libraries — force the
|
||||
# no-manifest path (the manifest names Arduino libs, which don't
|
||||
# exist in a pure IDF build).
|
||||
result = await _attempt_safe(None if pure_idf else allowed_libraries)
|
||||
|
||||
# Graceful fallback (P2). A manifest-scoped compile that fails because a
|
||||
# header isn't in the manifest (an undeclared / transitive dependency)
|
||||
|
|
@ -1891,7 +1946,7 @@ class ESPIDFCompiler:
|
|||
# manifest can be auto-completed (P2.4) or the user prompted to add the
|
||||
# missing library. The caller holds the per-target lock for this whole
|
||||
# method, so the retry safely reuses the same build dir.
|
||||
if allowed_libraries is not None and not result.get('success'):
|
||||
if allowed_libraries is not None and not pure_idf and not result.get('success'):
|
||||
missing = self._missing_library_headers(result)
|
||||
if missing:
|
||||
logger.warning(
|
||||
|
|
@ -1919,11 +1974,17 @@ class ESPIDFCompiler:
|
|||
spiffs_files: list[dict] | None = None,
|
||||
allowed_libraries: set[str] | None = None,
|
||||
libraries_dir: Path | None = None,
|
||||
pure_idf: bool = False,
|
||||
) -> dict:
|
||||
"""Inner compile body: writes sketch + libs into `project_dir`,
|
||||
runs cmake + ninja, merges binaries. Caller is responsible for
|
||||
creating `project_dir` (with the template tree already copied in)
|
||||
and for managing its lifecycle (persistent vs tempfile).
|
||||
|
||||
pure_idf: the user's files are the IDF main component sources
|
||||
(app_main entry point) — no Arduino wrap, no Arduino libraries.
|
||||
The template's main/CMakeLists.txt picks its pure branch via the
|
||||
VELXIO_PURE_SKETCH env var set in _build_env.
|
||||
"""
|
||||
# board_options is already normalised by compile() — defensive in
|
||||
# case _compile_in_dir is called directly from a test path.
|
||||
|
|
@ -1935,6 +1996,15 @@ class ESPIDFCompiler:
|
|||
# template tree. Doing this BEFORE cmake configure means the new
|
||||
# CONFIG_* lines reach kconfig on its first read.
|
||||
rendered_sdkconfig = self._render_sdkconfig(board_options, _TEMPLATE_DIR)
|
||||
if pure_idf:
|
||||
# Without the arduino-esp32 component the CONFIG_ARDUINO_* /
|
||||
# CONFIG_AUTOSTART_ARDUINO symbols don't exist. kconfig only
|
||||
# warns on unknown symbols, but strip them so the generated
|
||||
# sdkconfig stays honest about what the build contains.
|
||||
rendered_sdkconfig = '\n'.join(
|
||||
line for line in rendered_sdkconfig.splitlines()
|
||||
if not line.startswith(('CONFIG_ARDUINO', 'CONFIG_AUTOSTART_ARDUINO'))
|
||||
) + '\n'
|
||||
defaults_path = project_dir / 'sdkconfig.defaults'
|
||||
prev_defaults = (
|
||||
defaults_path.read_text(encoding='utf-8') if defaults_path.exists() else None
|
||||
|
|
@ -1969,10 +2039,44 @@ class ESPIDFCompiler:
|
|||
# We normalize ANY user SSID → "Velxio-GUEST", enforce channel 6,
|
||||
# and use open auth (empty password) so the connection always works.
|
||||
# Detect WiFi BEFORE normalization so the flag reflects the original sketch.
|
||||
has_wifi = self._detect_wifi_usage(main_content)
|
||||
main_content = self._normalize_wifi_for_qemu(main_content)
|
||||
if pure_idf:
|
||||
_all_text = '\n'.join(f.get('content', '') for f in files)
|
||||
has_wifi = self._detect_idf_wifi_usage(_all_text)
|
||||
else:
|
||||
has_wifi = self._detect_wifi_usage(main_content)
|
||||
main_content = self._normalize_wifi_for_qemu(main_content)
|
||||
|
||||
if self.has_arduino:
|
||||
if pure_idf:
|
||||
# Pure ESP-IDF mode (issue #139): the user's files ARE the main
|
||||
# component sources — app_main() entry point, IDF APIs, compiled
|
||||
# by the template CMake's VELXIO_PURE_SKETCH glob branch. No
|
||||
# Arduino wrap, no velxio_compat.h, no Arduino libraries.
|
||||
main_dir = project_dir / 'main'
|
||||
# Template / other-mode leftovers must not reach the pure glob
|
||||
# (main.cpp would drag in setup()/loop() references; a stale
|
||||
# sketch.ino.cpp would redefine symbols). Deleted BEFORE writing
|
||||
# so a user file with the same name wins.
|
||||
for leftover in ('main.c', 'main.cpp', 'sketch.ino.cpp', 'sketch_translated.c'):
|
||||
(main_dir / leftover).unlink(missing_ok=True)
|
||||
wrote_any = False
|
||||
for f in files:
|
||||
# basename() the client-supplied name so it can't escape main/
|
||||
name = PurePosixPath(str(f.get('name') or '').replace('\\', '/')).name
|
||||
if not name:
|
||||
continue
|
||||
content = f.get('content', '')
|
||||
if has_wifi:
|
||||
content = self._normalize_wifi_for_qemu_idf(content)
|
||||
(main_dir / name).write_text(content, encoding='utf-8')
|
||||
wrote_any = True
|
||||
if not wrote_any:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'No source files provided.',
|
||||
'stdout': '',
|
||||
'stderr': '',
|
||||
}
|
||||
elif self.has_arduino:
|
||||
# Arduino-as-component mode: copy sketch as .cpp
|
||||
sketch_cpp = project_dir / 'main' / 'sketch.ino.cpp'
|
||||
# Prepend Arduino.h + velxio_compat.h if not already included.
|
||||
|
|
@ -2088,7 +2192,7 @@ class ESPIDFCompiler:
|
|||
build_dir = project_dir / 'build'
|
||||
build_dir.mkdir(exist_ok=True)
|
||||
|
||||
env = self._build_env(idf_target)
|
||||
env = self._build_env(idf_target, pure_idf=pure_idf)
|
||||
|
||||
# Step 1: cmake configure
|
||||
cmake_cmd = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* Tests for GitHub issue #139 — pure ESP-IDF language mode
|
||||
* https://github.com/davidmonterocrespo24/velxio/issues/139
|
||||
*
|
||||
* The ESP32 family gains a third entry in the language selector next to
|
||||
* Arduino C++ and MicroPython: "ESP-IDF". In that mode the user writes a
|
||||
* plain IDF project (app_main() entry point, FreeRTOS + driver APIs) and
|
||||
* the backend compiles it through the same ESP-IDF toolchain it already
|
||||
* uses for Arduino sketches — just without the arduino-esp32 component.
|
||||
*
|
||||
* Pure Vitest unit tests — no QEMU, no network, no DOM.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON } from '../types/board';
|
||||
import type { BoardKind } from '../types/board';
|
||||
import { useEditorStore } from '../store/useEditorStore';
|
||||
import { exampleProjects } from '../data/examples';
|
||||
|
||||
describe('issue #139 — BOARD_SUPPORTS_ESPIDF', () => {
|
||||
const ESP32_KINDS: BoardKind[] = [
|
||||
'esp32',
|
||||
'esp32-devkit-c-v4',
|
||||
'esp32-cam',
|
||||
'wemos-lolin32-lite',
|
||||
'esp32-s3',
|
||||
'xiao-esp32-s3',
|
||||
'arduino-nano-esp32',
|
||||
'esp32-c3',
|
||||
'xiao-esp32-c3',
|
||||
'aitewinrobot-esp32c3-supermini',
|
||||
];
|
||||
|
||||
for (const kind of ESP32_KINDS) {
|
||||
it(`${kind} supports ESP-IDF mode`, () => {
|
||||
expect(BOARD_SUPPORTS_ESPIDF.has(kind)).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
it('non-ESP32 boards do NOT support ESP-IDF mode', () => {
|
||||
const NON_ESP32: BoardKind[] = [
|
||||
'arduino-uno',
|
||||
'raspberry-pi-pico',
|
||||
'pi-pico-w',
|
||||
'raspberry-pi-3',
|
||||
'stm32-bluepill',
|
||||
'attiny85',
|
||||
];
|
||||
for (const kind of NON_ESP32) {
|
||||
expect(BOARD_SUPPORTS_ESPIDF.has(kind)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('every ESP-IDF board also offers the language selector (MicroPython set)', () => {
|
||||
// The toolbar renders the selector when BOARD_SUPPORTS_MICROPYTHON
|
||||
// matches and adds the ESP-IDF option when BOARD_SUPPORTS_ESPIDF also
|
||||
// matches — so every espidf-capable board must be in the outer set or
|
||||
// the option would be unreachable.
|
||||
for (const kind of BOARD_SUPPORTS_ESPIDF) {
|
||||
expect(BOARD_SUPPORTS_MICROPYTHON.has(kind)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('issue #139 — espidf file group defaults', () => {
|
||||
it("createFileGroup(groupId, 'espidf') seeds main.c with app_main()", () => {
|
||||
const groupId = 'group-esp32-espidf-test';
|
||||
useEditorStore.getState().createFileGroup(groupId, 'espidf');
|
||||
const files = useEditorStore.getState().getGroupFiles(groupId);
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0].name).toBe('main.c');
|
||||
expect(files[0].content).toContain('app_main');
|
||||
expect(files[0].content).toContain('freertos/FreeRTOS.h');
|
||||
expect(files[0].content).toContain('driver/gpio.h');
|
||||
// Must NOT be an Arduino sketch — that's the whole point of the mode.
|
||||
expect(files[0].content).not.toContain('setup()');
|
||||
expect(files[0].content).not.toContain('Arduino.h');
|
||||
useEditorStore.getState().deleteFileGroup(groupId);
|
||||
});
|
||||
|
||||
it("createFileGroup(groupId, 'arduino') still seeds sketch.ino (regression)", () => {
|
||||
const groupId = 'group-esp32-arduino-test';
|
||||
useEditorStore.getState().createFileGroup(groupId, 'arduino');
|
||||
const files = useEditorStore.getState().getGroupFiles(groupId);
|
||||
expect(files[0].name).toBe('sketch.ino');
|
||||
useEditorStore.getState().deleteFileGroup(groupId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('issue #139 — esp32-idf-blink gallery example', () => {
|
||||
const example = exampleProjects.find((e) => e.id === 'esp32-idf-blink');
|
||||
|
||||
it('exists and targets an ESP32 board in espidf mode', () => {
|
||||
expect(example).toBeDefined();
|
||||
expect(example?.boardType).toBe('esp32');
|
||||
expect(example?.languageMode).toBe('espidf');
|
||||
});
|
||||
|
||||
it('ships a main.c with an app_main() entry point', () => {
|
||||
const mainFile = example?.files?.find((f) => f.name === 'main.c');
|
||||
expect(mainFile).toBeDefined();
|
||||
expect(mainFile?.content).toContain('void app_main(void)');
|
||||
expect(mainFile?.content).not.toContain('Arduino.h');
|
||||
});
|
||||
});
|
||||
|
|
@ -7,7 +7,7 @@ import { type VerificationResult } from '../../simulation/verify/circuitVerifier
|
|||
import { verifyCircuitFromStore } from '../../simulation/verify/verifyFromStore';
|
||||
import { CircuitVerificationModal } from '../simulator/CircuitVerificationModal';
|
||||
import type { BoardKind, LanguageMode } from '../../types/board';
|
||||
import { BOARD_KIND_FQBN, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, boardDisplayName } from '../../types/board';
|
||||
import { BOARD_KIND_FQBN, BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, boardDisplayName } from '../../types/board';
|
||||
import { compileCode } from '../../services/compilation';
|
||||
import {
|
||||
compileRom,
|
||||
|
|
@ -588,6 +588,9 @@ export const EditorToolbar = ({
|
|||
// P2.4 — THIS board's declared manifest (compile scope). Per-board so
|
||||
// two boards can use different libraries without clashing.
|
||||
libraries: activeBoard?.libraries?.length ? activeBoard.libraries : null,
|
||||
// Pure ESP-IDF mode (issue #139): tell the backend to compile the
|
||||
// user's app_main() sources without the arduino-esp32 component.
|
||||
language: activeBoard?.languageMode === 'espidf' ? 'espidf' : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1050,7 +1053,7 @@ export const EditorToolbar = ({
|
|||
})),
|
||||
]);
|
||||
},
|
||||
{ boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles, libraries: board.libraries?.length ? board.libraries : null },
|
||||
{ boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles, libraries: board.libraries?.length ? board.libraries : null, language: board.languageMode === 'espidf' ? 'espidf' : undefined },
|
||||
);
|
||||
|
||||
const resultLogs = parseCompileResult(result, label, boardTarget);
|
||||
|
|
@ -1354,9 +1357,11 @@ export const EditorToolbar = ({
|
|||
<>
|
||||
<div className="editor-toolbar-wrapper" style={{ position: 'relative' }}>
|
||||
<div className="editor-toolbar" ref={toolbarRef}>
|
||||
{/* MicroPython language selector — only when active board supports it.
|
||||
The board context pill that used to live here was removed: it
|
||||
duplicated the BoardSelector dropdown elsewhere in the toolbar. */}
|
||||
{/* Language selector — only when active board supports an
|
||||
alternative to Arduino C++ (MicroPython on Pico/ESP32 boards,
|
||||
pure ESP-IDF on the ESP32 family — issue #139). The board
|
||||
context pill that used to live here was removed: it duplicated
|
||||
the BoardSelector dropdown elsewhere in the toolbar. */}
|
||||
{activeBoard && BOARD_SUPPORTS_MICROPYTHON.has(activeBoard.boardKind) && (
|
||||
<select
|
||||
className="tb-lang-select"
|
||||
|
|
@ -1380,6 +1385,9 @@ export const EditorToolbar = ({
|
|||
>
|
||||
<option value="arduino">Arduino C++</option>
|
||||
<option value="micropython">MicroPython</option>
|
||||
{BOARD_SUPPORTS_ESPIDF.has(activeBoard.boardKind) && (
|
||||
<option value="espidf">ESP-IDF</option>
|
||||
)}
|
||||
</select>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,10 +60,11 @@ export interface ExampleProject {
|
|||
/** Code for single-board examples (ignored when boards[] is set, or when files[] is provided). */
|
||||
code: string;
|
||||
/**
|
||||
* Optional language mode for the active board. When 'micropython', loadExample
|
||||
* switches the board into MicroPython mode before populating files.
|
||||
* Optional language mode for the active board. When 'micropython' or
|
||||
* 'espidf', loadExample switches the board into that mode before
|
||||
* populating files.
|
||||
*/
|
||||
languageMode?: 'arduino' | 'micropython';
|
||||
languageMode?: 'arduino' | 'micropython' | 'espidf';
|
||||
/**
|
||||
* Optional multi-file payload for single-board examples. When present it
|
||||
* overrides ``code`` — every entry is loaded into the active file group as-is.
|
||||
|
|
@ -213,6 +214,61 @@ void loop() {
|
|||
{ id: 'w-scl', start: { componentId: 'esp32', pinName: '22' }, end: { componentId: 'oled', pinName: 'SCL' }, color: '#ff8800' },
|
||||
],
|
||||
},
|
||||
// ── Pure ESP-IDF (issue #139): user app_main(), no Arduino core. ──
|
||||
{
|
||||
id: 'esp32-idf-blink',
|
||||
title: 'ESP32: Pure ESP-IDF Blink',
|
||||
description:
|
||||
'Blink an LED from a pure ESP-IDF project — app_main(), FreeRTOS delays and the GPIO driver, no Arduino core. Pick "ESP-IDF" in the language selector to write more projects like this.',
|
||||
category: 'basics',
|
||||
difficulty: 'intermediate',
|
||||
boardType: 'esp32',
|
||||
languageMode: 'espidf',
|
||||
tags: ['esp32', 'esp-idf', 'freertos', 'gpio'],
|
||||
code: '',
|
||||
files: [
|
||||
{
|
||||
name: 'main.c',
|
||||
content: `// Pure ESP-IDF blink — no Arduino core, just IDF APIs.
|
||||
// app_main() is the ESP-IDF entry point (instead of setup()/loop()).
|
||||
#include <stdio.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#define LED_PIN GPIO_NUM_2
|
||||
|
||||
static const char *TAG = "blink";
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "ESP-IDF blink starting");
|
||||
gpio_reset_pin(LED_PIN);
|
||||
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
|
||||
|
||||
int level = 0;
|
||||
while (1) {
|
||||
level = !level;
|
||||
gpio_set_level(LED_PIN, level);
|
||||
ESP_LOGI(TAG, "LED %s", level ? "on" : "off");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
components: [
|
||||
// 220R keeps the LED under its 20 mA rating on a 3.3 V GPIO.
|
||||
{ type: 'wokwi-resistor', id: 'r-led', x: 340, y: 120, properties: { value: '220' } },
|
||||
{ type: 'wokwi-led', id: 'led-ext', x: 470, y: 100, properties: { color: 'red' } },
|
||||
],
|
||||
wires: [
|
||||
{ id: 'w-gpio2-r', start: { componentId: 'esp32', pinName: '2' }, end: { componentId: 'r-led', pinName: '1' }, color: '#e74c3c' },
|
||||
{ id: 'w-r-led', start: { componentId: 'r-led', pinName: '2' }, end: { componentId: 'led-ext', pinName: 'A' }, color: '#e74c3c' },
|
||||
{ id: 'w-gnd', start: { componentId: 'led-ext', pinName: 'C' }, end: { componentId: 'esp32', pinName: 'GND' }, color: '#2c3e50' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pico-oled-4pin-i2c',
|
||||
title: 'Raspberry Pi Pico: SSD1306 OLED (4-pin I2C)',
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ export interface CompileExtras {
|
|||
// Sent as the ESP-IDF resolution SCOPE; null/omitted = legacy scan-all.
|
||||
// Ignored by the backend for non-ESP32 (arduino-cli) boards.
|
||||
libraries?: string[] | null;
|
||||
// Pure ESP-IDF mode (issue #139): 'espidf' compiles the files as a pure
|
||||
// ESP-IDF project (user app_main, no arduino-esp32 component). Omitted /
|
||||
// undefined = classic Arduino sketch compile. ESP32 boards only.
|
||||
language?: 'espidf';
|
||||
}
|
||||
|
||||
export interface CompileResult {
|
||||
|
|
@ -113,6 +117,7 @@ export async function compileCode(
|
|||
board_options,
|
||||
spiffs_files,
|
||||
libraries,
|
||||
language: extras?.language ?? null,
|
||||
},
|
||||
{ withCredentials: true, timeout: 30000 },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,31 @@ while True:
|
|||
time.sleep(1)
|
||||
`;
|
||||
|
||||
// Pure ESP-IDF mode (issue #139): the user's own app_main(), compiled by the
|
||||
// backend's ESP-IDF toolchain WITHOUT the arduino-esp32 component. GPIO 2 is
|
||||
// the built-in LED on most ESP32 dev boards.
|
||||
const DEFAULT_ESPIDF_CONTENT = `// ESP-IDF Blink Example
|
||||
#include <stdio.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/gpio.h"
|
||||
|
||||
#define LED_PIN GPIO_NUM_2
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
gpio_reset_pin(LED_PIN);
|
||||
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
|
||||
|
||||
while (1) {
|
||||
gpio_set_level(LED_PIN, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
gpio_set_level(LED_PIN, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DEFAULT_PY_CONTENT = `import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
|
|
@ -369,7 +394,10 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||
let fileName: string;
|
||||
let content: string;
|
||||
const isEsp32 = groupId.includes('esp32');
|
||||
if (isMicroPython && isEsp32) {
|
||||
if (languageMode === 'espidf') {
|
||||
fileName = 'main.c';
|
||||
content = DEFAULT_ESPIDF_CONTENT;
|
||||
} else if (isMicroPython && isEsp32) {
|
||||
fileName = 'main.py';
|
||||
content = DEFAULT_ESP32_MICROPYTHON_CONTENT;
|
||||
} else if (isMicroPython) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import type { I2CDevice } from '../simulation/I2CBusManager';
|
|||
import type { RP2040I2CDevice } from '../simulation/RP2040Simulator';
|
||||
import type { Wire, WireInProgress, WireEndpoint } from '../types/wire';
|
||||
import type { BoardKind, BoardInstance, LanguageMode, WifiStatus } from '../types/board';
|
||||
import { BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, isStm32BoardKind } from '../types/board';
|
||||
import { BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, isStm32BoardKind } from '../types/board';
|
||||
import { boardGateDecision, proBoardFeatureName, triggerProUpgradePrompt } from '../lib/proBoardGate';
|
||||
import { calculatePinPosition } from '../utils/pinPositionCalculator';
|
||||
import { useOscilloscopeStore } from './useOscilloscopeStore';
|
||||
|
|
@ -1701,8 +1701,9 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
const board = get().boards.find((b) => b.id === boardId);
|
||||
if (!board) return;
|
||||
|
||||
// Only allow MicroPython for supported boards
|
||||
// Only allow MicroPython / ESP-IDF for supported boards
|
||||
if (mode === 'micropython' && !BOARD_SUPPORTS_MICROPYTHON.has(board.boardKind)) return;
|
||||
if (mode === 'espidf' && !BOARD_SUPPORTS_ESPIDF.has(board.boardKind)) return;
|
||||
|
||||
// Stop any running simulation
|
||||
if (board.running) get().stopBoard(boardId);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export type BoardKind =
|
|||
| 'stm32-netduino2' // Netduino 2 (F205, Cortex-M3), QEMU (serial until F205 GPIO wired)
|
||||
| 'attiny85'; // AVR ATtiny85, browser emulation (avr8js)
|
||||
|
||||
export type LanguageMode = 'arduino' | 'micropython';
|
||||
export type LanguageMode = 'arduino' | 'micropython' | 'espidf';
|
||||
|
||||
/** True for every Raspberry Pi backed by the QEMU bridge (Zero, 1, 2, 3, 4, 5).
|
||||
* Excludes the Pico boards (RP2040, browser emulation). */
|
||||
|
|
@ -63,6 +63,26 @@ export const BOARD_SUPPORTS_MICROPYTHON = new Set<BoardKind>([
|
|||
'aitewinrobot-esp32c3-supermini',
|
||||
]);
|
||||
|
||||
/** Boards that can run pure ESP-IDF projects (app_main entry point, IDF
|
||||
* APIs only — no Arduino core). The backend compiles them through the same
|
||||
* ESP-IDF toolchain it already uses for ESP32 Arduino sketches, just
|
||||
* without the arduino-esp32 component. ESP32 family only (issue #139). */
|
||||
export const BOARD_SUPPORTS_ESPIDF = new Set<BoardKind>([
|
||||
// ESP32 Xtensa (QEMU bridge)
|
||||
'esp32',
|
||||
'esp32-devkit-c-v4',
|
||||
'esp32-cam',
|
||||
'wemos-lolin32-lite',
|
||||
// ESP32-S3 Xtensa (QEMU bridge)
|
||||
'esp32-s3',
|
||||
'xiao-esp32-s3',
|
||||
'arduino-nano-esp32',
|
||||
// ESP32-C3 RISC-V (QEMU bridge)
|
||||
'esp32-c3',
|
||||
'xiao-esp32-c3',
|
||||
'aitewinrobot-esp32c3-supermini',
|
||||
]);
|
||||
|
||||
export interface WifiStatus {
|
||||
status: string; // 'initializing' | 'connected' | 'got_ip' | 'disconnected'
|
||||
ssid?: string;
|
||||
|
|
@ -94,7 +114,7 @@ export interface BoardInstance {
|
|||
serialBaudRate: number;
|
||||
serialMonitorOpen: boolean;
|
||||
activeFileGroupId: string;
|
||||
languageMode: LanguageMode; // 'arduino' (default) or 'micropython'
|
||||
languageMode: LanguageMode; // 'arduino' (default), 'micropython' or 'espidf'
|
||||
hasWifi?: boolean; // set by compiler — true when sketch uses WiFi
|
||||
wifiStatus?: WifiStatus;
|
||||
bleStatus?: BleStatus;
|
||||
|
|
|
|||
|
|
@ -277,8 +277,11 @@ export async function loadExample(
|
|||
.getState()
|
||||
.boards.find((b) => b.id === liveBoardId);
|
||||
|
||||
if (example.languageMode === 'micropython' && liveBoard) {
|
||||
setBoardLanguageMode(liveBoard.id, 'micropython');
|
||||
if (
|
||||
(example.languageMode === 'micropython' || example.languageMode === 'espidf') &&
|
||||
liveBoard
|
||||
) {
|
||||
setBoardLanguageMode(liveBoard.id, example.languageMode);
|
||||
}
|
||||
|
||||
const editorStore = useEditorStore.getState();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
"""
|
||||
Tests for the pure ESP-IDF language mode (issue #139).
|
||||
|
||||
Covers the pieces that don't need the ESP-IDF toolchain: the build env
|
||||
switch, the QEMU WiFi normalization for IDF-style code, the IDF wifi
|
||||
detection, and the compile-job dedup key variance.
|
||||
|
||||
Run from the repo root:
|
||||
python -m pytest test/backend/unit/test_espidf_pure_mode.py -v
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'backend'))
|
||||
|
||||
from app.services.espidf_compiler import ESPIDFCompiler, _QEMU_WIFI_SSID
|
||||
from app.api.routes import compile as compile_module
|
||||
|
||||
|
||||
def make_compiler(has_arduino: bool = True) -> ESPIDFCompiler:
|
||||
comp = ESPIDFCompiler.__new__(ESPIDFCompiler)
|
||||
comp.idf_path = '/opt/esp-idf'
|
||||
comp.arduino_path = '/opt/arduino-esp32' if has_arduino else ''
|
||||
comp.has_arduino = has_arduino
|
||||
return comp
|
||||
|
||||
|
||||
class TestBuildEnvPureMode(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.comp = make_compiler(has_arduino=True)
|
||||
|
||||
def test_arduino_mode_sets_arduino_path(self):
|
||||
env = self.comp._build_env('esp32')
|
||||
self.assertEqual(env.get('ARDUINO_ESP32_PATH'), '/opt/arduino-esp32')
|
||||
self.assertNotIn('VELXIO_PURE_SKETCH', env)
|
||||
|
||||
def test_pure_mode_drops_arduino_and_flags_pure(self):
|
||||
env = self.comp._build_env('esp32', pure_idf=True)
|
||||
self.assertNotIn('ARDUINO_ESP32_PATH', env)
|
||||
self.assertEqual(env.get('VELXIO_PURE_SKETCH'), '1')
|
||||
|
||||
def test_pure_mode_overrides_inherited_arduino_path(self):
|
||||
"""The uvicorn process env carries ARDUINO_ESP32_PATH in Docker —
|
||||
pure mode must strip the inherited copy too, or the template CMake
|
||||
would still pick the Arduino branch."""
|
||||
os.environ['ARDUINO_ESP32_PATH'] = '/opt/arduino-esp32'
|
||||
try:
|
||||
env = self.comp._build_env('esp32', pure_idf=True)
|
||||
self.assertNotIn('ARDUINO_ESP32_PATH', env)
|
||||
finally:
|
||||
del os.environ['ARDUINO_ESP32_PATH']
|
||||
|
||||
|
||||
class TestIdfWifiDetection(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.comp = make_compiler()
|
||||
|
||||
def test_detects_esp_wifi_include(self):
|
||||
self.assertTrue(self.comp._detect_idf_wifi_usage('#include "esp_wifi.h"'))
|
||||
self.assertTrue(self.comp._detect_idf_wifi_usage('#include <esp_wifi.h>'))
|
||||
|
||||
def test_detects_esp_wifi_init_call(self):
|
||||
code = 'wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();\nesp_wifi_init(&cfg);'
|
||||
self.assertTrue(self.comp._detect_idf_wifi_usage(code))
|
||||
|
||||
def test_ignores_plain_gpio_project(self):
|
||||
code = '#include "driver/gpio.h"\nvoid app_main(void) {}'
|
||||
self.assertFalse(self.comp._detect_idf_wifi_usage(code))
|
||||
|
||||
|
||||
class TestIdfWifiNormalization(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.comp = make_compiler()
|
||||
|
||||
def test_define_ssid_rewritten(self):
|
||||
code = '#define WIFI_SSID "MyHomeNetwork"\n#define WIFI_PASS "hunter2"'
|
||||
out = self.comp._normalize_wifi_for_qemu_idf(code)
|
||||
self.assertIn(f'#define WIFI_SSID "{_QEMU_WIFI_SSID}"', out)
|
||||
self.assertIn('#define WIFI_PASS ""', out)
|
||||
|
||||
def test_designated_initializers_rewritten(self):
|
||||
code = (
|
||||
'wifi_config_t wifi_config = {\n'
|
||||
' .sta = {\n'
|
||||
' .ssid = "MyHomeNetwork",\n'
|
||||
' .password = "hunter2",\n'
|
||||
' },\n'
|
||||
'};'
|
||||
)
|
||||
out = self.comp._normalize_wifi_for_qemu_idf(code)
|
||||
self.assertIn(f'.ssid = "{_QEMU_WIFI_SSID}"', out)
|
||||
self.assertIn('.password = ""', out)
|
||||
self.assertNotIn('MyHomeNetwork', out)
|
||||
self.assertNotIn('hunter2', out)
|
||||
|
||||
def test_non_wifi_code_untouched(self):
|
||||
code = '#include "driver/gpio.h"\nvoid app_main(void) { }\n'
|
||||
self.assertEqual(self.comp._normalize_wifi_for_qemu_idf(code), code)
|
||||
|
||||
|
||||
class TestJobKeyLanguageVariance(unittest.TestCase):
|
||||
|
||||
FILES = [{'name': 'main.c', 'content': 'void app_main(void) {}'}]
|
||||
|
||||
def test_language_changes_key(self):
|
||||
k_arduino = compile_module._job_key(self.FILES, 'esp32:esp32:esp32')
|
||||
k_espidf = compile_module._job_key(
|
||||
self.FILES, 'esp32:esp32:esp32', language='espidf',
|
||||
)
|
||||
self.assertNotEqual(k_arduino, k_espidf)
|
||||
|
||||
def test_explicit_arduino_keeps_historical_key(self):
|
||||
"""language='arduino' and language=None must hash identically so
|
||||
pre-feature clients keep dedupping against new-client submissions."""
|
||||
k_none = compile_module._job_key(self.FILES, 'esp32:esp32:esp32')
|
||||
k_arduino = compile_module._job_key(
|
||||
self.FILES, 'esp32:esp32:esp32', language='arduino',
|
||||
)
|
||||
self.assertEqual(k_none, k_arduino)
|
||||
|
||||
def test_espidf_key_is_stable(self):
|
||||
k1 = compile_module._job_key(self.FILES, 'esp32:esp32:esp32', language='espidf')
|
||||
k2 = compile_module._job_key(self.FILES, 'esp32:esp32:esp32', language='espidf')
|
||||
self.assertEqual(k1, k2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue