Pinscope on Zephyr
Remote access, logs, metrics, crash reports and firmware updates for any Zephyr board with a network: WiFi, Ethernet or cellular. Two lines in prj.conf, no code.
With an AI assistant. Copy the instructions and paste them into Claude, Cursor, Copilot or ChatGPT in your project: it adds Pinscope the way this page says, and asks you for the project key instead of writing it into your code. Or give it the link: zephyr.md.
# Add Pinscope to a Zephyr application
Pinscope gives the device's web server a secure address, sends LOG_* output, metrics and fatal errors to the Pinscope dashboard, and updates the firmware through MCUboot. You are adding it to an existing Zephyr 4.x application on a board with a network (WiFi, Ethernet or cellular). Full docs: https://pinscope.dev/docs/zephyr
## First, look at the project
- Find the west manifest (`west.yml`) and the application's `prj.conf`.
- Note how the network comes up (WiFi connect code, Ethernet, DHCP or a static address) and whether the app serves HTTP, and on which port.
- Note whether it builds with sysbuild and MCUboot.
## Steps
1. Add the module to `west.yml`, under `projects`, then run `west update`:
```yaml
- name: pinscope
url: https://github.com/pinscope-dev/pinscope-zephyr
revision: v0.2.5
path: modules/lib/pinscope
```
2. Switch it on in `prj.conf`:
```
CONFIG_PINSCOPE=y
```
3. The project key. The user copies it from the dashboard: their project, tab **Connect a device**. It goes into `CONFIG_PINSCOPE_PROJECT_KEY`. Keep it out of the committed `prj.conf`: put it in a local file that is not committed, for example `key.conf` next to `prj.conf` with `CONFIG_PINSCOPE_PROJECT_KEY="psk_..."`, add `key.conf` to `.gitignore`, and build with `-- -D<app>_EXTRA_CONF_FILE=key.conf` under sysbuild, where `<app>` is the application folder's name (`-- -DEXTRA_CONF_FILE=key.conf` without sysbuild).
4. No code is needed: Pinscope starts by itself once the system is up, switches on what it uses (IPv4, TCP, sockets, DNS, DHCP, settings, the random generator) and starts DHCP if the interface has no address. The application keeps bringing the network up as it does now.
5. If the app's HTTP server is not on port 80, set `CONFIG_PINSCOPE_LOCAL_PORT`.
6. Settings: Pinscope keeps the device's credential in Zephyr settings, on the flash partition labelled `storage_partition`. Some boards (the ST Nucleo boards among them) define none, and the build then stops with a message that shows the overlay to add: put it in `boards/<board>.overlay`, in free flash that no image slot or MCUboot uses. For the NUCLEO-F429ZI with MCUboot that is `partition@8000`, 32 KB.
7. Firmware updates use MCUboot. If the app does not use it yet, add to `sysbuild.conf`:
```
SB_CONFIG_BOOTLOADER_MCUBOOT=y
SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y
```
and build with `--sysbuild`. ECDSA P-256 keeps MCUboot small enough for the 32 KB many boards give it (with the default RSA it overflows on the Nucleo boards). The board needs `boot_partition`, `slot0_partition` and `slot1_partition`. Tell the user to sign with their own key in production (`SB_CONFIG_BOOT_SIGNATURE_KEY_FILE`): the default key is public.
8. The network: Pinscope starts DHCP only when the interface has no address, and leaves a fixed address alone. If the app sets one (`CONFIG_NET_CONFIG_MY_IPV4_ADDR`), check it is real: the addresses in Zephyr's samples (192.0.2.x, 10.0.0.x without a gateway) cannot reach the internet, and the device would never connect. Ask the user whether to switch to DHCP (remove the fixed address settings) or give a real address, netmask and gateway (`CONFIG_NET_CONFIG_MY_IPV4_GW`).
9. RP2040 boards (Pico W, W55RP20) have no random generator Zephyr knows: build with `-S pinscope-rp2040`. Elsewhere Pinscope's keys come from the board's hardware generator; a sample's `CONFIG_TEST_RANDOM_GENERATOR` affects only Zephyr's non-cryptographic random numbers, but tell the user it is not meant for products.
10. Optional, for the EU Cyber Resilience Act: if the user wants vulnerability tracking, an SBOM of each build comes from `west spdx --init -d build` before the build, building with `-DCONFIG_BUILD_OUTPUT_META=y`, then `west spdx -d build` (writes `build/spdx/*.spdx`); the user attaches it to the uploaded firmware in the dashboard's Security tab, or from CI with `ci.sh ... --sbom`. Do not add it to the build unasked.
## Check
- `west build -b <board> --sysbuild` succeeds.
- After flashing, the log shows `pinscope: online at https://...` and the device appears in the dashboard. For updates the user uploads `build/<app>/zephyr/zephyr.signed.bin`, and `zephyr.elf` for decoded fatal errors.
## Rules
- Never invent a project key and never write the user's key into a file that is committed. Ask the user for it, and keep it where the steps below say. A key is `psk_` followed by 24 letters, digits, `-` or `_`, 28 characters in all; if the one you are given is not, ask again.
- Change as little as you can: add Pinscope, do not restructure the project, rename files or touch unrelated code.
- Do not add a TLS library, certificates or port forwarding. Pinscope makes one outbound connection to `connect.pinscope.dev` on TCP port 443 and encrypts it itself.
- Do not edit Pinscope's own source; configure it.
- When you are done, tell the user exactly what you changed and what they still have to do (enter the key, flash, upload the firmware for updates).
Try the sample first
A workspace with Zephyr 4.5 and Pinscope, in one command, then the sample on your board. You need west and the Zephyr SDK, and the project key from Connect a device:
west init -m https://github.com/pinscope-dev/pinscope-zephyr pinscope-ws cd pinscope-ws && west update west packages pip --install export PINSCOPE_KEY=psk_... west build -b nucleo_f429zi --sysbuild modules/lib/pinscope/sample west flash
On the nRF7002 DK, fetch the WiFi firmware once (west blobs fetch nrf_wifi) and give the network: WIFI_SSID=... WIFI_PASSWORD=... west build -b nrf7002dk/nrf5340/cpuapp --sysbuild modules/lib/pinscope/sample. The board appears under Devices; open /crash on its page to see a fatal error reported after the reboot.
1. Connect
Add the module
In your west.yml, under projects, then run west update:
- name: pinscope url: https://github.com/pinscope-dev/pinscope-zephyr path: modules/lib/pinscope
The source is yours to read and to change. Its licence allows use with the Pinscope service only.
Enter your project key
In the dashboard, open your project and the tab Connect a device and copy the project key. Then, in prj.conf:
CONFIG_PINSCOPE=y CONFIG_PINSCOPE_PROJECT_KEY="psk_..."
That is all. Pinscope switches on what it uses from Zephyr (IPv4, TCP, sockets, DNS, DHCP, settings on the flash storage partition, the cryptographic random generator) and starts by itself once the system is up. Anything it switches on you can still set differently in prj.conf. If the interface has no address yet, it starts DHCP; a static address is left alone.
Your application brings the network up as it always does: on a WiFi board, connect to the access point; on Ethernet, plug in the cable. The device needs to reach connect.pinscope.dev on TCP port 443, outbound only. Build, flash, and the device appears under Devices.
To start it yourself, with settings made in code, set CONFIG_PINSCOPE_AUTOSTART=n and call pinscope_start():
#include "pinscope.h"
pinscope_config_t cfg = { .ports = { 80, 8080 } };
pinscope_start(&cfg);
A storage partition: Pinscope keeps the device's credential in settings, on the flash partition labelled storage_partition. Some boards, the ST Nucleo boards among them, define none; the build then stops and shows the overlay to add in boards/<board>.overlay, in flash no image slot uses. The sample's boards/nucleo_f429zi.overlay is one.
RP2040 boards (Raspberry Pi Pico W, WIZnet W55RP20 and others) have no random generator Zephyr knows. Build with -S pinscope-rp2040; the build tells you so if you forget.
W55RP20 with MCUboot: the board w55rp20_pinscope has its random generator built in, and the build writes build/pinscope.uf2, MCUboot and the signed application in one file. Hold BOOTSEL while plugging the board in, then picotool load -v -x build/pinscope.uf2. Copying the file to the RPI-RP2 drive can leave it written incompletely on macOS, and MCUboot then refuses the image; picotool checks what it wrote.
2. Reach the device
Whatever listens on port 80 of the device is served at the device's own HTTPS address, exactly as on ESP32: pages, JSON APIs and WebSockets, share links, public pages and your own address. See Reach the device. To expose another port, set CONFIG_PINSCOPE_LOCAL_PORT, or list up to four in pinscope_config_t.ports.
3. Logs and metrics
Whatever your application and Zephyr log with LOG_INF, LOG_WRN and LOG_ERR reaches the Logs tab, as ESP_LOG does on ESP32. With CONFIG_LOG_PRINTK=y, printk too. This needs CONFIG_LOG=y in deferred mode, Zephyr's default.
Reported with no code, once a minute:
| Metric | Meaning |
|---|---|
heap_free, heap_min | Free heap of the system allocator now, and the lowest it has been |
stack/pinscope | Stack the Pinscope thread has never used |
rssi | WiFi signal, on WiFi boards |
uptime_s | Time since boot |
Send your own from any thread:
pinscope_metric("water_level_cm", 42.5);
4. Crashes
When the firmware hits a fatal error, the reason, the thread, the program counter and the code addresses found on the stack are kept in RAM across the reboot and sent on the next connection. They appear in the Crashes tab.
To read function names and line numbers, upload build/zephyr/zephyr.elf of that build (with sysbuild, build/<app>/zephyr/zephyr.elf) under the project's Firmware tab. Builds are matched by their GNU build ID, which Pinscope adds to the image. Pinscope provides k_sys_fatal_error_handler(); if your application defines its own, set CONFIG_PINSCOPE_CRASH_CAPTURE=n.
Boards
Any board Zephyr gives a network to works the same way. The sample is set up for these, and built for each of them on every change. Run on hardware, with updates, rollback and crash reports: the W55RP20 (Ethernet) and the ESP32-C6 (WiFi).
| Board | -b | Network | Before building |
|---|---|---|---|
| ST NUCLEO-F429ZI | nucleo_f429zi | Ethernet | |
| ST NUCLEO-H563ZI | nucleo_h563zi | Ethernet | |
| ST B-L475E-IOT01A | disco_l475_iot1 | WiFi | WiFi |
| Nordic nRF7002 DK | nrf7002dk/nrf5340/cpuapp | WiFi | west blobs fetch nrf_wifi, WiFi |
| Nordic nRF5340 DK + W5500 | nrf5340dk/nrf5340/cpuapp | Ethernet | -- -Dsample_SHIELD=wiznet_w5500 |
| Nordic nRF9151 DK | nrf9151dk/nrf9151/ns | LTE-M, NB-IoT | nRF Connect SDK, below |
| Espressif ESP32-S3-DevKitC | esp32s3_devkitc/esp32s3/procpu | WiFi | west blobs fetch hal_espressif, WiFi |
| Espressif ESP32-C3-DevKitM | esp32c3_devkitm | WiFi | west blobs fetch hal_espressif, WiFi |
| Espressif ESP32-C6-DevKitC | esp32c6_devkitc/esp32c6/hpcore | WiFi | west blobs fetch hal_espressif, WiFi |
| NXP FRDM-RW612 | frdm_rw612 | Ethernet | |
| Raspberry Pi Pico 2 W | rpi_pico2/rp2350a/m33_0/w/mcuboot | WiFi | west blobs fetch hal_infineon, WiFi |
| WIZnet W55RP20-EVB-Pico | w55rp20_pinscope | Ethernet | load build/pinscope.uf2 with picotool load -v -x |
| QEMU (Arm MPS2) | mps2/an385 | Ethernet | no --sysbuild |
WiFi boards take the network from the environment when the build directory is created: WIFI_SSID=... WIFI_PASSWORD=... west build ....
No board? Build for mps2/an385 and run it in QEMU from your system (brew install qemu, apt install qemu-system-arm): qemu-system-arm -M mps2-an385 -nographic -nic user,model=lan9118 -kernel build/zephyr/zephyr.bin. The emulated board has no random generator, so that build uses a predictable one: for trying, never for a device.
nRF91 (LTE). The modem library is part of Nordic's nRF Connect SDK. A workspace with nRF Connect SDK 3.4.2: west init -m https://github.com/pinscope-dev/pinscope-zephyr --mf west-ncs.yml pinscope-ncs, then west update and pip install -r nrf/scripts/requirements-build.txt. LTE comes up at boot; the board needs a SIM with an LTE-M or NB-IoT plan.
Modems that run the IP stack themselves (the nRF91, the B-L475E-IOT01A's WiFi module): logs, metrics, crash reports and updates work as on any board, but the device's own web page cannot be reached through Pinscope, which needs Zephyr's native IP stack.
5. Firmware updates
Updates use MCUboot, Zephyr's own bootloader. Build with sysbuild and MCUboot:
# sysbuild.conf SB_CONFIG_BOOTLOADER_MCUBOOT=y SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y
ECDSA P-256 keeps MCUboot inside the 32 KB many boards give it; with the default RSA it does not fit on the Nucleo boards.
west build -b <board> --sysbuild
The board needs MCUboot partitions (boot_partition, slot0_partition, slot1_partition); most boards with MCUboot support define them. Then upload build/<app>/zephyr/zephyr.signed.bin under Firmware and press Update firmware on a device. The image travels over the existing connection into the second slot, and MCUboot boots it on trial.
The new image is confirmed when it reaches Pinscope. If it has not after five minutes (CONFIG_PINSCOPE_OTA_TRIAL_S), the device restarts and MCUboot goes back to the previous one. The version shown is the one in your application's VERSION file; raise it with each build. Images for other platforms are refused.
Sign with your own key in production: SB_CONFIG_BOOT_SIGNATURE_KEY_FILE. The key Zephyr uses by default is public.
Memory
About 8.6 KB of RAM: the same 4.2 KB of frame buffers, queue and state as on ESP32, the Pinscope thread's stack (4 KB by default, CONFIG_PINSCOPE_STACK_SIZE; the stack/pinscope metric shows how much it uses), and the thread itself. No TLS library: the connection uses the Noise protocol (X25519, ChaCha20-Poly1305). The network buffers of the connection come from Zephyr's shared network pool.
Options
Under Pinscope in menuconfig (west build -t menuconfig):
| Option | Default | What it does |
|---|---|---|
CONFIG_PINSCOPE_PROJECT_KEY | empty | The project key from the dashboard |
CONFIG_PINSCOPE_DEVICE_NAME | empty | The name shown in the dashboard |
CONFIG_PINSCOPE_LOCAL_PORT | 80 | The port served on the device's address |
CONFIG_PINSCOPE_AUTOSTART | y | Start at boot, with no code |
CONFIG_PINSCOPE_START_DHCP | y | Start DHCP when the interface has no address |
CONFIG_PINSCOPE_OTA | y with MCUboot | Firmware updates from the dashboard |
CONFIG_PINSCOPE_LOG_BACKEND | y | Send Zephyr's log |
CONFIG_PINSCOPE_SYSTEM_METRICS | y | The built-in metrics |
CONFIG_PINSCOPE_STACK_SIZE | 4096 | Stack of the Pinscope thread |
The functions are the same as on ESP32: see Functions.
If it does not work
Most problems stop the build with the fix in the message. On the device, Pinscope logs under the module name pinscope; a device that connected says online at https://….
| What you see | What to check |
|---|---|
The build stops: this board has no storage_partition | Add a small storage partition in a board overlay (two flash sectors are enough), or choose another settings backend |
The build stops: The RP2040 has no random generator | Build with -S pinscope-rp2040 |
the project key is missing | Set CONFIG_PINSCOPE_PROJECT_KEY |
No online at line, no error | The interface has no address yet (WiFi not connected, no DHCP server, no cable), or the network does not let it reach connect.pinscope.dev on TCP port 443 |
| The device is online, its address shows an error page | Nothing listens on the exposed port. If the page says the device is out of connections, raise CONFIG_NET_MAX_CONN |
| There is no Update firmware button | The build has no MCUboot, or CONFIG_PINSCOPE_OTA is off |
| An update goes back to the old firmware | The new image did not reach Pinscope within five minutes, or it was signed with another key than the one MCUboot has |
Still stuck? Write to support@pinscope.dev with the lines the device logs under pinscope.