Pinscope on the Raspberry Pi Pico 2 W
Remote access, logs, metrics, crash reports and firmware updates for the Pico 2 W with the Pico SDK. One line in CMake, one line of 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: pico.md.
# Add Pinscope to a Raspberry Pi Pico 2 W project
Pinscope gives the device's web server a secure address, sends printf output, metrics and HardFaults to the Pinscope dashboard, and updates the firmware with the RP2350's A/B partitions. You are adding it to an existing Pico SDK 2.x project for the Pico 2 W that uses FreeRTOS and lwIP. Full docs: https://pinscope.dev/docs/pico
## First, look at the project
- Find `CMakeLists.txt`, `lwipopts.h` and where WiFi connects (`cyw43_arch_wifi_connect_*`).
- Pinscope runs in a FreeRTOS task and uses lwIP sockets, so the project must use `pico_cyw43_arch_lwip_sys_freertos`. If it uses `NO_SYS=1` (no FreeRTOS), stop and tell the user: moving to FreeRTOS is a bigger change they should decide on.
## Steps
1. Get the library next to the project: `git clone https://github.com/pinscope-dev/pinscope-pico` (or as a git submodule).
2. In `CMakeLists.txt`, after `pico_sdk_init()`:
```cmake
include(pinscope-pico/pinscope.cmake)
target_link_libraries(app
pinscope
pico_cyw43_arch_lwip_sys_freertos
FreeRTOS-Kernel-Heap4
pico_stdlib)
```
using the project's own target name instead of `app`, and keeping the libraries it already links.
3. In `lwipopts.h`, make sure these are set (the build stops with the setting to change if one is missing):
```c
#define NO_SYS 0
#define LWIP_SOCKET 1
#define LWIP_NETIF_LOOPBACK 1
#define MEMP_NUM_NETCONN 12
```
4. The project key. The user copies it from the dashboard: their project, tab **Connect a device**. Put it in a header that is not committed, for example `secrets.h` added to `.gitignore`: `#define PINSCOPE_KEY "psk_..."`.
5. Once WiFi is connected, start it once; it reconnects by itself:
```c
#include "pinscope.h"
#include "secrets.h"
...
pinscope_start(&(pinscope_config_t){
.project_key = PINSCOPE_KEY,
.firmware_version = "1.0.0", // shown in the dashboard
});
```
6. For updates, each board needs a partition table with two application partitions, loaded once: `picotool partition create partitions.json pt.uf2`, then `picotool load pt.uf2 && picotool reboot -u`, with a `partitions.json` like the one in `pinscope-pico/sample`. Tell the user this is a one-time step per board.
## Check
- The project builds and produces a `.uf2`.
- After flashing, the device appears in the dashboard under Devices.
## 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
You need CMake, Ninja and the Arm GCC (arm-none-eabi-gcc). Without PICO_SDK_PATH, the Pico SDK and the FreeRTOS kernel are downloaded the first time:
git clone https://github.com/pinscope-dev/pinscope-pico && cd pinscope-pico PINSCOPE_KEY=psk_... WIFI_SSID=... WIFI_PASSWORD=... cmake -S sample -B build -G Ninja cmake --build build
Hold BOOTSEL while plugging the board in and copy build/pinscope_pico.uf2 to it.
1. Connect
Add it to the build
Clone pinscope-pico next to your project (or add it as a git submodule). In your CMakeLists.txt, after pico_sdk_init():
include(pinscope-pico/pinscope.cmake)
target_link_libraries(app
pinscope
pico_cyw43_arch_lwip_sys_freertos
FreeRTOS-Kernel-Heap4
pico_stdlib)
Pinscope runs in a FreeRTOS task of its own and uses lwIP's sockets, so the project uses the SDK's FreeRTOS WiFi architecture, pico_cyw43_arch_lwip_sys_freertos. In your lwipopts.h:
#define NO_SYS 0 #define LWIP_SOCKET 1 #define LWIP_NETIF_LOOPBACK 1 #define MEMP_NUM_NETCONN 12
The build stops with the setting to change if one is missing.
Start it
In the dashboard, open your project and the tab Connect a device and copy the project key. Then, once WiFi is connected:
#include "pinscope.h"
pinscope_start(&(pinscope_config_t){
.project_key = "psk_...",
.firmware_version = "1.0.0", // shown in the dashboard
});
Call it once. It reconnects by itself. The device needs to reach connect.pinscope.dev on TCP port 443, outbound only. Flash the UF2 and the device appears under Devices.
2. Reach the device
Whatever listens on port 80 of the Pico 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.
3. Logs and metrics
Everything the firmware prints with printf reaches the Logs tab, a line at a time. The USB or UART output is unchanged.
| Metric | Meaning |
|---|---|
heap_free, heap_min | Free FreeRTOS heap now, and the lowest it has been |
stack/pinscope | Stack the Pinscope task has never used |
rssi | WiFi signal |
chip_temp_c, uptime_s | The RP2350's temperature sensor, and time since boot |
pinscope_metric("water_level_cm", 42.5);
4. Crashes
On a HardFault, the program counter, the return address and the code addresses found on the stack are kept in RAM across the reset and sent on the next connection, under the Crashes tab. Upload the .elf of that build under Firmware to read function names and line numbers; builds are matched by their GNU build ID. Pinscope provides isr_hardfault; an application that defines its own replaces it.
5. Firmware updates
Updates use the RP2350's own A/B partitions: the new firmware is written to the partition not in use, and the bootrom boots it on trial. Once per board, load a partition table with two partitions for the application:
picotool partition create partitions.json pt.uf2 picotool load pt.uf2 && picotool reboot -u
with a partitions.json like the one in ports/pico/sample. Then flash your firmware as usual. To update it, upload the build's .uf2 under Firmware and press Update firmware on the device.
The new firmware is confirmed when it reaches Pinscope; if it does not within five minutes, the device restarts and the bootrom goes back to the previous one. Without the partition table the device simply shows no Update firmware button. Pinscope keeps its device credential in the last 4 KB sector of flash, outside both partitions.
Memory
About 9 KB of RAM: 4.2 KB of frame buffers, queue and state, as on ESP32, a 3 KB task stack, and the task, lock and socket. No TLS library: the connection uses the Noise protocol, with keys from the RP2350's hardware random generator.
If it does not work
Pinscope's lines carry pinscope:; a device that connected says online at https://….
| What you see | What to check |
|---|---|
The build stops: Pinscope needs lwIP sockets | Set NO_SYS 0 and LWIP_SOCKET 1 in lwipopts.h, and link pico_cyw43_arch_lwip_sys_freertos |
The build stops: LWIP_NETIF_LOOPBACK | Set it to 1: Pinscope reaches the Pico's own web server through it |
No online at line, no error | WiFi is not connected yet, or the network does not let the Pico reach connect.pinscope.dev on TCP port 443 |
| There is no Update firmware button | The board has no A/B partition table yet |
Still stuck? Write to support@pinscope.dev with the lines that carry pinscope:.