Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a9aea04
feat(desktop): Kconfig-gated CANopen/DS402, I2C scanner and Network a…
finger563 Oct 4, 2026
e22d2a7
fix(desktop): address the Copilot review of the hardware apps
finger563 Oct 4, 2026
1e55e54
fix(desktop): address the second Copilot review of the hardware apps
finger563 Oct 4, 2026
2c9244a
fix(desktop): address the third Copilot review of the hardware apps
finger563 Oct 4, 2026
6b0e710
fix(desktop): address the fourth Copilot review of the hardware apps
finger563 Oct 4, 2026
c953e44
fix(desktop): address the fifth Copilot review of the hardware apps
finger563 Oct 4, 2026
0deb512
fix(desktop): address the sixth Copilot review of the hardware apps
finger563 Oct 4, 2026
59796c5
fix(desktop): address the seventh Copilot review of the hardware apps
finger563 Oct 4, 2026
3382819
fix(desktop): address the eighth Copilot review of the hardware apps
finger563 Oct 4, 2026
76045b5
fix(desktop): SDO value field parses decimal unless prefixed with 0x …
finger563 Oct 4, 2026
7fefe07
fix(desktop): address the tenth Copilot review of the hardware apps
finger563 Oct 4, 2026
f215329
fix(desktop): only require canopen / twai on IDF >= 6; bring Ethernet…
finger563 Oct 4, 2026
dafa84f
fix(desktop): address the twelfth Copilot review of the hardware apps
finger563 Oct 4, 2026
e4686bc
fix(desktop): address the thirteenth Copilot review of the hardware apps
finger563 Oct 4, 2026
0dc44ae
fix(desktop): run every Wi-Fi station transition on a worker that wai…
finger563 Oct 4, 2026
46d7651
fix(desktop): address the fifteenth Copilot review of the Network app
finger563 Oct 4, 2026
3d8c9c0
fix(desktop,wifi): address the sixteenth Copilot review of the hardwa…
finger563 Oct 4, 2026
25f3c92
fix(desktop,wifi): correlate DISCONNECTED events by draining stale on…
finger563 Oct 4, 2026
920873e
fix(desktop): recover the Wi-Fi station deterministically when a DISC…
finger563 Oct 4, 2026
c804fb2
fix(desktop): withdraw the Network app's STA_STOP handler before the …
finger563 Oct 4, 2026
465c867
fix(desktop): I2C scan skips reserved addresses by default; Ethernet …
finger563 Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion components/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,6 @@ and covered by the host test. Mutually exclusive with
The [example](./example) serves the desktop with the Counter, About, System
Monitor, Task Manager, Log Viewer, Files + Editor and Settings apps on the
native USB port of an ESP32-S3 (WebUSB + Web Serial), next to the standard
System / Monitor / OTA / CoreDump services.
System / Monitor / OTA / CoreDump services, plus three Kconfig-gated hardware
apps: CANopen / DS402 (a simulated node or the TWAI peripheral), an I2C
scanner and Network (Wi-Fi station, Ethernet).
55 changes: 49 additions & 6 deletions components/desktop/example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,48 @@ set(CMAKE_CXX_STANDARD 20)
# from the vendored git submodules under external/ and littlefs from the
# components/littlefs submodule.
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
include($ENV{IDF_PATH}/tools/cmake/version.cmake)

# The CANopen app (simulated node or TWAI peripheral) is built on espp's twai
# component, which needs the IDF >= 6.0 esp_driver_twai node API (the simulated
# bus is a Twai drop-in and shares its Message type). The rest of the example
# builds on IDF 5.5, so the canopen / twai components are only pulled in when
# the IDF supports them -- or when overridden on the command line:
# idf.py -DDESKTOP_EXAMPLE_CANOPEN=OFF build (leave the CANopen app out)
# With it OFF, CONFIG_DESKTOP_EXAMPLE_ENABLE_CANOPEN has no effect (the app is
# not compiled; app_main logs a warning if it is enabled).
if(IDF_VERSION_MAJOR GREATER_EQUAL 6)
set(DESKTOP_EXAMPLE_CANOPEN_DEFAULT ON)
else()
set(DESKTOP_EXAMPLE_CANOPEN_DEFAULT OFF)
endif()
option(DESKTOP_EXAMPLE_CANOPEN "Build the CANopen / DS402 app (needs IDF >= 6.0 for twai)"
${DESKTOP_EXAMPLE_CANOPEN_DEFAULT})
# main/CMakeLists.txt needs the decision for its REQUIRES, which IDF evaluates
# in a separate script process (no CMake cache there): hand it over as a build
# property, which IDF writes into that process.
idf_build_set_property(DESKTOP_EXAMPLE_CANOPEN "${DESKTOP_EXAMPLE_CANOPEN}")
if(DESKTOP_EXAMPLE_CANOPEN)
set(DESKTOP_EXAMPLE_CANOPEN_DIRS "../../../components/canopen" "../../../components/twai")
set(DESKTOP_EXAMPLE_CANOPEN_COMPONENTS "canopen twai")
message(STATUS "desktop example: CANopen / DS402 app ON (IDF ${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR})")
else()
set(DESKTOP_EXAMPLE_CANOPEN_DIRS "")
set(DESKTOP_EXAMPLE_CANOPEN_COMPONENTS "")
message(STATUS "desktop example: CANopen / DS402 app OFF (DESKTOP_EXAMPLE_CANOPEN; twai needs IDF >= 6.0)")
endif()

set(EXTRA_COMPONENT_DIRS
"../../../components/base_component"
${DESKTOP_EXAMPLE_CANOPEN_DIRS}
"../../../components/cli"
"../../../components/coredump"
"../../../components/desktop"
"../../../components/dispatcher"
"../../../components/ethernet"
"../../../components/file_system"
"../../../components/format"
"../../../components/i2c"
"../../../components/logger"
"../../../components/monitor"
"../../../components/nvs"
Expand All @@ -34,6 +68,7 @@ set(EXTRA_COMPONENT_DIRS
"../../../components/system"
"../../../components/task"
"../../../components/usb_device"
"../../../components/wifi"
)

if(DEFINED ENV{IDF_COMPONENT_MANAGER} AND "$ENV{IDF_COMPONENT_MANAGER}" STREQUAL "0")
Expand All @@ -44,11 +79,19 @@ if(DEFINED ENV{IDF_COMPONENT_MANAGER} AND "$ENV{IDF_COMPONENT_MANAGER}" STREQUAL
)
endif()

set(
COMPONENTS
"main esptool_py base_component desktop dispatcher file_system format logger monitor nvs stream_frame system task usb_device esp_tinyusb esp_psram coredump ota espcoredump littlefs"
CACHE STRING
"List of components to include"
)
# The component list is a cache entry like in the other examples, so a caller's
# -DCOMPONENTS=... wins; the CANopen-dependent default is only computed when
# the caller did not supply one. (main's REQUIRES still resolve canopen / twai
# from EXTRA_COMPONENT_DIRS whenever DESKTOP_EXAMPLE_CANOPEN is ON, whatever
# the list says: that decision travels as a build property, not through
# COMPONENTS.)
if(NOT DEFINED COMPONENTS)
set(
COMPONENTS
"main esptool_py base_component desktop dispatcher file_system format logger monitor nvs stream_frame system task usb_device esp_tinyusb esp_psram coredump ota espcoredump littlefs i2c wifi ethernet cli ${DESKTOP_EXAMPLE_CANOPEN_COMPONENTS}"
CACHE STRING
"List of components to include"
)
endif()

project(desktop_example)
70 changes: 70 additions & 0 deletions components/desktop/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,34 @@ Apps (`main/apps/*.hpp`, one `register_<name>_app()` each):
console; the capture itself is the compile-time
`CONFIG_DESKTOP_EXAMPLE_LOG_CAPTURE`), all kept in NVS and restored at boot.

Hardware apps, each behind a Kconfig option (see
[Configuration](#configuration)). CANopen (on the simulated node), the I2C
scanner and the Wi-Fi group are on by default, so the CI build compiles them;
the Ethernet group defaults off and is only selectable on SoCs with an EMAC
(ESP32 / ESP32-P4, not the S3):

- **CANopen / DS402** — a CiA 301 NMT master + SDO client
(`espp::CanopenClient`) and a CiA 402 drive panel (`espp::Ds402Drive`):
node id, NMT Start / Stop / Pre-operational / Reset, NMT + drive state and
statusword, mode of operation, Enable / Disable / Quick stop / Fault reset,
a target-velocity slider, position / velocity, and a raw SDO read / write
row. The bus is either the in-firmware **simulated DS402 node** (the CAN
bridge example's `SimulatedCanBus`, no hardware needed, with an "Inject
fault" button) or the **TWAI peripheral** wired to a CAN transceiver. Every
bus transaction runs on the app's own task (SDO calls block); the window
only queues commands and the task updates the widgets.
- **I2C scanner** — probe every 7-bit address on the configured bus
(`espp::I2c`) from a short task and list what answers; read / write a
device register from the window. A bus that fails to initialize shows a
hint instead.
- **Network** — the Wi-Fi station (`espp::WifiSta`): status / SSID / IP /
RSSI / MAC, Scan (on its own task; a scan disconnects first), the AP list,
password field and Connect / Disconnect / Forget with the credentials kept
in NVS (`desktop` namespace, `wifi_ssid` / `wifi_pass`); and, on SoCs with an
EMAC, the RMII Ethernet link (`espp::Ethernet`): link / IP / MAC / speed.
The interfaces come up on the first launch and stay up when the window is
closed.

## How to use example

### Hardware Required
Expand All @@ -39,6 +67,48 @@ console / logs go to UART0 (see `sdkconfig.defaults`); with
`CONFIG_DESKTOP_EXAMPLE_LOG_CAPTURE` (default on) they are also captured for
the Log Viewer.

The hardware apps need nothing extra by default: the CANopen app talks to a
simulated node, the I2C scanner just reports an empty bus and the Network app
scans for Wi-Fi. For a real CAN bus select the TWAI peripheral and wire a
transceiver (SN65HVD230 or similar) to the configured TX / RX GPIOs; for the
Ethernet group an ESP32-Ethernet-Kit or an ESP32-P4-Function-EV-Board (the
RMII wiring is selected by `DESKTOP_EXAMPLE_ETHERNET_BOARD`; other boards:
edit `rmii_config()` in `main/apps/network_app.hpp`).

### Configuration

`idf.py menuconfig` -> *Desktop Example Configuration*:

| Option | Default | Meaning |
|---|---|---|
| `DESKTOP_EXAMPLE_LOG_CAPTURE` (+ `_BYTES`) | y (16384) | Tee the console into a ring for the Log Viewer |
| `DESKTOP_EXAMPLE_ENABLE_CANOPEN` | y | Register the CANopen / DS402 app |
| `DESKTOP_EXAMPLE_CANOPEN_BUS` | `SIMULATED` | `SIMULATED` (in-firmware DS402 node) or `TWAI` (the peripheral) |
| `DESKTOP_EXAMPLE_CANOPEN_NODE_ID` | 1 | Server node id (1..127; also the simulated node's id) |
| `DESKTOP_EXAMPLE_CAN_TX_GPIO` / `_RX_GPIO` / `_BAUDRATE` | 17 / 16 / 500000 | TWAI wiring and bit rate (TWAI bus only) |
| `DESKTOP_EXAMPLE_ENABLE_I2C` | y | Register the I2C scanner app |
| `DESKTOP_EXAMPLE_I2C_PORT` / `_SDA_GPIO` / `_SCL_GPIO` / `_FREQ_HZ` | 0 / 8 / 9 / 400000 | The I2C bus it scans |
| `DESKTOP_EXAMPLE_ENABLE_WIFI` | y | The Network app's Wi-Fi station group (`SOC_WIFI_SUPPORTED`) |
| `DESKTOP_EXAMPLE_ENABLE_ETHERNET` | n | The Network app's RMII Ethernet group (`SOC_EMAC_SUPPORTED`: ESP32 / -P4) |
| `DESKTOP_EXAMPLE_ETHERNET_BOARD` | per target | RMII wiring: `ETHERNET_KIT` (ESP32-Ethernet-Kit) or `P4_FUNCTION_EV` (ESP32-P4-Function-EV-Board, with its routable data pins) |

The hardware components (`i2c`, `wifi`, `ethernet`, `cli`) are always part
of the build (`REQUIRES` cannot depend on Kconfig); the options only decide
which apps are registered.

**Minimum IDF.** The example itself builds on IDF 5.5 (the `desktop`,
`usb_device`, `ethernet` (>= 5.4), `wifi`, `i2c` and `canopen` components
all support it). The CANopen app is the exception: it is built on the `twai`
component (the simulated bus is a `Twai` drop-in), which needs the IDF >= 6.0
`esp_driver_twai` node API. The CMakeLists therefore adds `canopen` + `twai`
and compiles the app only on IDF >= 6.0, controlled by the CMake option
`DESKTOP_EXAMPLE_CANOPEN` (default ON on IDF >= 6, OFF below; override with
`idf.py -DDESKTOP_EXAMPLE_CANOPEN=OFF build`). On an older IDF
`CONFIG_DESKTOP_EXAMPLE_ENABLE_CANOPEN` has no effect and `app_main` logs a
warning. The simulated CAN bus / DS402 node headers are included from the CAN
bridge example (`components/canopen/can_bridge_example/main`); promoting them
into the `canopen` component is a follow-up.

### Build and Flash

```
Expand Down
28 changes: 26 additions & 2 deletions components/desktop/example/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
# The hardware components are required unconditionally (REQUIRES cannot depend
# on Kconfig); the apps themselves are gated by CONFIG_DESKTOP_EXAMPLE_ENABLE_*
# in desktop_example.cpp. The exception is the CANopen app: espp's twai
# component needs IDF >= 6.0, so canopen / twai are only required when the
# top-level CMakeLists enabled DESKTOP_EXAMPLE_CANOPEN (ON by default on IDF
# >= 6; read back as a build property because IDF evaluates REQUIRES in a
# separate script process), and the app is compiled only when
# DESKTOP_EXAMPLE_CANOPEN_AVAILABLE is defined.
idf_build_get_property(desktop_example_canopen DESKTOP_EXAMPLE_CANOPEN)
set(desktop_example_requires desktop system monitor file_system nvs dispatcher task usb_device
esp_tinyusb coredump ota espcoredump i2c wifi ethernet)
set(desktop_example_include_dirs "." "apps")
if(desktop_example_canopen)
list(APPEND desktop_example_requires canopen twai)
# The CANopen app's simulated bus / DS402 node live in the can_bridge example
# for now (simulated_can_bus.hpp, simulated_ds402_node.hpp); promoting them
# into the canopen component is a follow-up.
list(APPEND desktop_example_include_dirs "../../../canopen/can_bridge_example/main")
endif()

idf_component_register(
SRC_DIRS "."
INCLUDE_DIRS "." "apps"
REQUIRES desktop system monitor file_system nvs dispatcher task usb_device esp_tinyusb coredump ota espcoredump)
INCLUDE_DIRS ${desktop_example_include_dirs}
REQUIRES ${desktop_example_requires})

if(desktop_example_canopen)
target_compile_definitions(${COMPONENT_LIB} PRIVATE DESKTOP_EXAMPLE_CANOPEN_AVAILABLE=1)
endif()
121 changes: 121 additions & 0 deletions components/desktop/example/main/Kconfig.projbuild
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,125 @@ menu "Desktop Example Configuration"
How many of the newest console bytes are kept for the Log Viewer
(and shown when it opens).

config DESKTOP_EXAMPLE_ENABLE_CANOPEN
bool "CANopen / DS402 app"
default y
help
Register the CANopen / DS402 app: an NMT master + SDO client
(espp::CanopenClient) and a CiA 402 drive panel (espp::Ds402Drive)
on the bus selected below.

The app is built on espp's twai component (the simulated bus is a
Twai drop-in), which needs IDF >= 6.0; the example's CMakeLists
therefore only compiles it on IDF >= 6 (or with
-DDESKTOP_EXAMPLE_CANOPEN=ON/OFF on the idf.py command line). On an
older IDF this option has no effect and app_main logs a warning.

choice DESKTOP_EXAMPLE_CANOPEN_BUS
prompt "CAN bus for the CANopen app"
depends on DESKTOP_EXAMPLE_ENABLE_CANOPEN
default DESKTOP_EXAMPLE_CANOPEN_BUS_SIMULATED
help
Where the CANopen app's frames go: an in-firmware simulated DS402
node (no hardware needed; the CI default) or the TWAI peripheral
wired to a CAN transceiver.

config DESKTOP_EXAMPLE_CANOPEN_BUS_SIMULATED
bool "Simulated DS402 node (no CAN hardware)"
config DESKTOP_EXAMPLE_CANOPEN_BUS_TWAI
bool "TWAI peripheral (CAN transceiver on the GPIOs below)"
endchoice

config DESKTOP_EXAMPLE_CANOPEN_NODE_ID
int "CANopen node id"
depends on DESKTOP_EXAMPLE_ENABLE_CANOPEN
default 1
range 1 127
help
The server node id the app talks to at launch (changeable in the
window). With the simulated bus this is also the simulated node's id.

config DESKTOP_EXAMPLE_CAN_TX_GPIO
int "TWAI TX GPIO"
depends on DESKTOP_EXAMPLE_CANOPEN_BUS_TWAI
default 17

config DESKTOP_EXAMPLE_CAN_RX_GPIO
int "TWAI RX GPIO"
depends on DESKTOP_EXAMPLE_CANOPEN_BUS_TWAI
default 16

config DESKTOP_EXAMPLE_CAN_BAUDRATE
int "CAN bit rate (bit/s)"
depends on DESKTOP_EXAMPLE_CANOPEN_BUS_TWAI
default 500000
range 10000 1000000

config DESKTOP_EXAMPLE_ENABLE_I2C
bool "I2C scanner app"
default y
help
Register the I2C scanner app: probe every 7-bit address on the
bus below, and read / write device registers.

config DESKTOP_EXAMPLE_I2C_PORT
int "I2C port"
depends on DESKTOP_EXAMPLE_ENABLE_I2C
default 0
range 0 1

config DESKTOP_EXAMPLE_I2C_SDA_GPIO
int "I2C SDA GPIO"
depends on DESKTOP_EXAMPLE_ENABLE_I2C
default 8

config DESKTOP_EXAMPLE_I2C_SCL_GPIO
int "I2C SCL GPIO"
depends on DESKTOP_EXAMPLE_ENABLE_I2C
default 9

config DESKTOP_EXAMPLE_I2C_FREQ_HZ
int "I2C clock (Hz)"
depends on DESKTOP_EXAMPLE_ENABLE_I2C
default 400000
range 1000 1000000

config DESKTOP_EXAMPLE_ENABLE_WIFI
bool "Network app: Wi-Fi station"
depends on SOC_WIFI_SUPPORTED
default y
help
Register the Network app's Wi-Fi group: status, scan, connect /
disconnect with the credentials kept in NVS (espp::WifiSta).

config DESKTOP_EXAMPLE_ENABLE_ETHERNET
bool "Network app: Ethernet (RMII)"
depends on SOC_EMAC_SUPPORTED
default n
help
Register the Network app's Ethernet group: link / IP / MAC / speed
of the internal EMAC over RMII (espp::Ethernet) with the wiring of
the board selected below.

choice DESKTOP_EXAMPLE_ETHERNET_BOARD
prompt "RMII board wiring"
depends on DESKTOP_EXAMPLE_ENABLE_ETHERNET
default DESKTOP_EXAMPLE_ETHERNET_BOARD_ETHERNET_KIT if IDF_TARGET_ESP32
default DESKTOP_EXAMPLE_ETHERNET_BOARD_P4_FUNCTION_EV if IDF_TARGET_ESP32P4
help
Which board's RMII pins network_app.hpp uses: the ESP32-Ethernet-Kit
(IP101 PHY, MDC 23 / MDIO 18 / reset 5, 50 MHz clock in on GPIO 0;
the ESP32's data pins are fixed) or the ESP32-P4-Function-EV-Board
(IP101 PHY, MDC 31 / MDIO 52 / reset 51, clock in on GPIO 50, and
the routable data pins TX_EN 49 / TXD0 34 / TXD1 35 / CRS_DV 28 /
RXD0 29 / RXD1 30). Other boards: edit rmii_config().

config DESKTOP_EXAMPLE_ETHERNET_BOARD_ETHERNET_KIT
bool "ESP32-Ethernet-Kit"
depends on IDF_TARGET_ESP32
config DESKTOP_EXAMPLE_ETHERNET_BOARD_P4_FUNCTION_EV
bool "ESP32-P4-Function-EV-Board"
depends on IDF_TARGET_ESP32P4
endchoice

endmenu
Loading
Loading