Add portal web simulator, SPI bridges, and Pico firmware updates.

Bring the five-panel hex portal online with a browser 3D/schematic preview, Pi SPI backends, and renamed multi-panel Pico UDP firmware.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-30 14:54:51 +12:00
parent d5cab2efdf
commit 5094c7bcee
78 changed files with 62251 additions and 832 deletions

View File

@@ -1,125 +0,0 @@
# Portal panel firmware (Pico SDK)
C firmware for a **Pico + W5500 + WS2812** panel adapter. Replaces the slow CircuitPython `adapter/code.py` path with native UDP receive and PIO WS2812 output.
## Hardware
Matches `adapter/code.py`:
| Signal | GPIO |
|--------|------|
| W5500 CS | GP9 |
| SPI1 SCK | GP10 |
| SPI1 MOSI | GP11 |
| SPI1 MISO | GP12 |
| W5500 RST | GP13 |
| Status LED | GP25 |
| WS2812 data | GP27 |
Default: **405 LEDs** (45×9 matrix chain), one data pin per panel.
WS2812 output uses [ForsakenNGS/Pico_WS2812](https://github.com/ForsakenNGS/Pico_WS2812) (FORMAT_GRB on GP27).
## Build
Requires [pico-sdk](https://github.com/raspberrypi/pico-sdk) and the ARM GCC toolchain (`arm-none-eabi-gcc`).
```bash
# From repo root
make deploy # build + flash over USB
make build # build only
# Or from this directory
make deploy
```
Manual cmake (one-time pico-sdk setup):
```bash
git clone https://github.com/raspberrypi/pico-sdk.git ~/pico/pico-sdk
cd ~/pico/pico-sdk && git submodule update --init
export PICO_SDK_PATH=~/pico/pico-sdk
mkdir -p build && cd build
cmake ..
make -j$(nproc)
```
UF2 output: `build/panel.uf2` — hold BOOTSEL and copy to the Pico.
### Options
```bash
# Panel 0 of 5 → 10.1.1.10, unique MAC, UDP panel_id filter
make deploy PANEL_ID=0 # 10.1.1.10
make deploy PANEL_ID=1 # 10.1.1.11
# DHCP instead of static 10.1.1.1014
make deploy PANEL_ID=0 NO_DHCP=0
# Fewer LEDs for bench test
make deploy NUM_LEDS=2
```
Static IP and MAC are derived from `PANEL_ID`:
| Panel | IP | MAC |
|-------|-----|-----|
| 0 | 10.1.1.10 | 02:50:52:54:4C:00 |
| 1 | 10.1.1.11 | 02:50:52:54:4C:01 |
| … | … | … |
| 4 | 10.1.1.14 | 02:50:52:54:4C:04 |
| 255 (bench) | 10.1.1.19 | 02:50:52:54:4C:FF |
Edit `board_config.h` for gateway, subnet, and pin changes.
## UDP protocol (port 50007)
| Payload | Action |
|---------|--------|
| **1215 bytes** | Raw RGB (`405 × 3`), show immediately |
| **1216 bytes** | `panel_id` (byte 0) + RGB; `255` = accept on any panel |
| **4 bytes `SHOW`** | Push buffered pixels to the strip (sync helper) |
Pi sends **RGB** order; firmware converts to WS2812 **GRB**.
Target throughput: ~3050 fps per panel (vs ~1020 fps on CircuitPython).
## Pi test sender
```bash
pipenv run python examples/panel_udp_send.py --host 192.168.2.111 --animation rainbow
```
## Flashing
Requires [picotool](https://github.com/raspberrypi/picotool) with libusb support:
```bash
make deploy
```
Or `./scripts/panel_deploy.sh` (same thing).
Or manually:
```bash
picotool load -x -f build/panel.uf2
```
`-f` forces a USB reboot into BOOTSEL when the panel is already running.
Legacy UF2 drag-and-drop also works: hold BOOTSEL, plug USB, copy `panel.uf2` to `RPI-RP2`.
Serial debug:
```bash
make monitor
# or: picocom /dev/ttyACM0
```
Ctrl+A then Ctrl+X to exit picocom.
## Multi-panel (5 Picos)
Flash each Pico with `PANEL_ID` 04 (`make deploy PANEL_ID=N`). Each gets `10.1.1.1N` (1014), a unique MAC, and UDP filtering. Send frames from the Pi to each panels IP, or use a leading `panel_id` byte with `255` broadcast filtering.

View File

@@ -1,219 +0,0 @@
/**
* Portal panel firmware — W5500 UDP + WS2812 (Pico SDK).
*
* UDP protocol (port 50007):
* - N×3 bytes: raw RGB (N = NUM_LEDS), show immediately
* - N×3+1 bytes: panel_id (byte 0) + RGB; panel_id 255 = any panel
* - 4 bytes "SHOW": refresh buffered pixels (for split frame/show sync)
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "board_config.h"
#include "wizchip_conf.h"
#include "dhcp.h"
#include "socket.h"
#include "timer.h"
#include "wizchip_spi.h"
#include "ws2812_led.h"
#include "hardware/gpio.h"
#include "pico/stdlib.h"
#define SOCKET_UDP 1
#define SOCKET_DHCP 0
#define ETH_BUF_SIZE 2048
#define DHCP_RETRY_MAX 10
static wiz_NetInfo g_net_info = {
.mac = MAC_ADDR,
.ip = STATIC_IP,
.sn = STATIC_SN,
.gw = STATIC_GW,
.dns = STATIC_DNS,
#if USE_DHCP
.dhcp = NETINFO_DHCP,
#else
.dhcp = NETINFO_STATIC,
#endif
};
static uint8_t g_eth_buf[ETH_BUF_SIZE];
static uint8_t g_pixel_buf[LED_RGB_BYTES];
static volatile uint16_t g_ms_tick;
static uint8_t g_dhcp_ready;
static void dhcp_timer_cb(void) {
g_ms_tick++;
if (g_ms_tick >= 999) {
g_ms_tick = 0;
DHCP_time_handler();
}
}
static void dhcp_assign(void) {
getIPfromDHCP(g_net_info.ip);
getGWfromDHCP(g_net_info.gw);
getSNfromDHCP(g_net_info.sn);
getDNSfromDHCP(g_net_info.dns);
g_net_info.dhcp = NETINFO_DHCP;
network_initialize(g_net_info);
print_network_information(g_net_info);
g_dhcp_ready = 1;
}
static void dhcp_conflict(void) {
printf("DHCP conflict\n");
}
static int network_bring_up(void) {
#if USE_DHCP
uint8_t retries = 0;
DHCP_init(SOCKET_DHCP, g_eth_buf);
reg_dhcp_cbfunc(dhcp_assign, dhcp_assign, dhcp_conflict);
wizchip_1ms_timer_initialize(dhcp_timer_cb);
while (!g_dhcp_ready && retries < DHCP_RETRY_MAX) {
int8_t rv = DHCP_run();
if (rv == DHCP_IP_LEASED) {
g_dhcp_ready = 1;
break;
}
if (rv == DHCP_FAILED) {
retries++;
}
wizchip_delay_ms(250);
}
if (!g_dhcp_ready) {
printf("DHCP failed, using static IP\n");
g_net_info.dhcp = NETINFO_STATIC;
network_initialize(g_net_info);
print_network_information(g_net_info);
}
#else
network_initialize(g_net_info);
print_network_information(g_net_info);
#endif
return 0;
}
static int udp_socket_open(void) {
int8_t sn = socket(SOCKET_UDP, Sn_MR_UDP, UDP_PORT, 0);
if (sn != SOCKET_UDP) {
printf("UDP socket open failed: %d\n", sn);
return -1;
}
printf("UDP listening on port %d\n", UDP_PORT);
return 0;
}
static void led_boot_test(void) {
printf("LED boot test on GP%d (%d pixels)\n", PIN_WS2812, NUM_LEDS);
ws2812_fill(255, 0, 0);
ws2812_show();
sleep_ms(500);
ws2812_fill(0, 255, 0);
ws2812_show();
sleep_ms(500);
ws2812_fill(0, 0, 0);
ws2812_show();
}
static int handle_frame(const uint8_t *rgb, uint16_t len) {
if (len != LED_RGB_BYTES) {
printf("frame reject len=%u want=%u\n", (unsigned)len, (unsigned)LED_RGB_BYTES);
return -1;
}
ws2812_set_rgb(rgb, NUM_LEDS);
ws2812_show();
return 0;
}
static void handle_packet(const uint8_t *data, int16_t len) {
if (len == 4 && memcmp(data, "SHOW", 4) == 0) {
ws2812_set_rgb(g_pixel_buf, NUM_LEDS);
ws2812_show();
return;
}
if (len == LED_RGB_BYTES) {
memcpy(g_pixel_buf, data, LED_RGB_BYTES);
handle_frame(data, (uint16_t)len);
return;
}
if (len == LED_RGB_BYTES + 1) {
uint8_t panel = data[0];
if (panel != 255 && panel != (uint8_t)PANEL_ID) {
return;
}
memcpy(g_pixel_buf, data + 1, LED_RGB_BYTES);
handle_frame(data + 1, LED_RGB_BYTES);
return;
}
}
static void poll_udp(void) {
while (1) {
uint16_t rx = getSn_RX_RSR(SOCKET_UDP);
if (rx == 0) {
return;
}
if (rx > ETH_BUF_SIZE) {
rx = ETH_BUF_SIZE;
}
uint8_t src_ip[4];
uint16_t src_port;
int16_t n = recvfrom(SOCKET_UDP, g_eth_buf, rx, src_ip, &src_port);
if (n <= 0) {
return;
}
handle_packet(g_eth_buf, n);
}
}
int main(void) {
stdio_init_all();
sleep_ms(2000);
gpio_init(PIN_LED_STATUS);
gpio_set_dir(PIN_LED_STATUS, GPIO_OUT);
printf("portal panel firmware (Pico SDK)\n");
printf("LEDs=%d panel_id=%d mac=02:50:52:54:4C:%02X ip=10.1.1.%u\n",
NUM_LEDS, PANEL_ID, (unsigned)PANEL_ID, (unsigned)STATIC_IP_OCT4);
wizchip_spi_initialize();
wizchip_cris_initialize();
wizchip_reset();
wizchip_initialize();
wizchip_check();
ws2812_init(PIN_WS2812, NUM_LEDS);
memset(g_pixel_buf, 0, sizeof(g_pixel_buf));
led_boot_test();
network_bring_up();
if (udp_socket_open() != 0) {
while (1) {
gpio_xor_mask(1u << PIN_LED_STATUS);
sleep_ms(100);
}
}
uint32_t heartbeat = 0;
while (1) {
#if USE_DHCP
DHCP_run();
#endif
poll_udp();
if (++heartbeat >= 500) {
gpio_xor_mask(1u << PIN_LED_STATUS);
heartbeat = 0;
}
sleep_ms(1);
}
}

View File

@@ -1,39 +0,0 @@
/* C API wrapper around ForsakenNGS/Pico_WS2812 */
#include "ws2812_led.h"
#include "WS2812.hpp"
#include "hardware/pio.h"
#include "pico/stdlib.h"
static WS2812 *s_strip;
extern "C" void ws2812_init(unsigned int pin, unsigned int num_leds) {
uint sm = pio_claim_unused_sm(pio0, true);
s_strip = new WS2812(pin, num_leds, pio0, sm, WS2812::FORMAT_GRB);
}
extern "C" void ws2812_set_rgb(const uint8_t *rgb, unsigned int count) {
if (s_strip == nullptr || rgb == nullptr) {
return;
}
for (unsigned int i = 0; i < count; i++) {
s_strip->setPixelColor(i, rgb[i * 3 + 0], rgb[i * 3 + 1], rgb[i * 3 + 2]);
}
}
extern "C" void ws2812_fill(uint8_t r, uint8_t g, uint8_t b) {
if (s_strip == nullptr) {
return;
}
s_strip->fill(WS2812::RGB(r, g, b));
}
extern "C" void ws2812_show(void) {
if (s_strip == nullptr) {
return;
}
s_strip->show();
sleep_us(300);
}

View File

@@ -1,19 +0,0 @@
#ifndef WS2812_LED_H
#define WS2812_LED_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
void ws2812_init(unsigned int pin, unsigned int num_leds);
void ws2812_set_rgb(const uint8_t *rgb, unsigned int count);
void ws2812_fill(uint8_t r, uint8_t g, uint8_t b);
void ws2812_show(void);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -47,14 +47,14 @@ if (DEFINED PANEL_ID_BUILD)
target_compile_definitions(panel PRIVATE PANEL_ID=${PANEL_ID_BUILD})
endif ()
if (DEFINED NUM_LEDS_BUILD)
target_compile_definitions(panel PRIVATE NUM_LEDS=${NUM_LEDS_BUILD})
endif ()
if (DEFINED WS2812_PIN_BUILD)
target_compile_definitions(panel PRIVATE PIN_WS2812=${WS2812_PIN_BUILD})
endif ()
if (DEFINED WS2812_PIN1_BUILD)
target_compile_definitions(panel PRIVATE PIN_WS2812_1=${WS2812_PIN1_BUILD})
endif ()
option(PORTAL_USE_DHCP "Use DHCP for W5500" OFF)
if (PORTAL_USE_DHCP)
target_compile_definitions(panel PRIVATE USE_DHCP=1)

View File

@@ -8,7 +8,7 @@
# Options (passed to cmake on configure/reconfigure):
# make deploy PANEL_ID=0
# make deploy NO_DHCP=1
# make deploy WS2812_PIN=28 # if data wire is on GP28 not GP27
# make deploy WS2812_PIN=26 WS2812_PIN1=27 # override defaults GP28 / GP27
#
# Environment:
# PICO_SDK_PATH default: ~/pico/pico-sdk
@@ -28,24 +28,13 @@ SERIAL_BAUD ?= 115200
CMAKE_ARGS :=
ifdef PANEL_ID
CMAKE_ARGS += -DPANEL_ID_BUILD=$(PANEL_ID)
# Panels 0 and 4 are 9×39 (351); others are 9×45 (405). Always pass NUM_LEDS so
# cmake cache cannot keep a stale count from a previous PANEL_ID flash.
ifeq ($(PANEL_ID),0)
NUM_LEDS ?= 351
else ifeq ($(PANEL_ID),4)
NUM_LEDS ?= 351
else
NUM_LEDS ?= 405
endif
CMAKE_ARGS += -DNUM_LEDS_BUILD=$(NUM_LEDS)
else
ifdef NUM_LEDS
CMAKE_ARGS += -DNUM_LEDS_BUILD=$(NUM_LEDS)
endif
endif
ifdef WS2812_PIN
CMAKE_ARGS += -DWS2812_PIN_BUILD=$(WS2812_PIN)
endif
ifdef WS2812_PIN1
CMAKE_ARGS += -DWS2812_PIN1_BUILD=$(WS2812_PIN1)
endif
ifeq ($(NO_DHCP),0)
CMAKE_ARGS += -DPORTAL_USE_DHCP=ON
else
@@ -56,7 +45,7 @@ CMAKE_STAMP_BODY := $(strip $(CMAKE_ARGS))
SOURCES := $(wildcard *.c *.cpp *.h) WS2812.pio CMakeLists.txt pico_sdk_import.cmake
.PHONY: all build configure reconfigure deploy flash upload monitor clean help
.PHONY: all build configure reconfigure deploy flash upload reset monitor clean help
all: build
@@ -65,9 +54,12 @@ help:
@echo " make build Build panel.uf2"
@echo " make deploy Build and flash over USB"
@echo " make flash Alias for deploy"
@echo " make reset USB reboot Pico (picotool) — run before monitor for boot log"
@echo " make monitor USB serial console (picocom, Ctrl+A Ctrl+X to quit)"
@echo " make clean Remove build directory"
@echo " make reconfigure Re-run cmake (e.g. after changing PANEL_ID)"
@echo ""
@echo "Typical: make reset && make monitor"
build: configure
@echo "==> Building panel firmware"
@@ -103,11 +95,32 @@ deploy: build
flash: deploy
upload: deploy
reset:
@command -v picotool >/dev/null || (echo "Error: picotool not found (https://github.com/raspberrypi/picotool)" && exit 1)
@echo "==> Resetting Pico over USB"
@picotool reboot -f
@echo "==> Waiting for USB serial..."
@i=0; \
while [ $$i -lt 40 ]; do \
if ls /dev/ttyACM* >/dev/null 2>&1; then \
echo "==> Ready: $$(ls /dev/ttyACM* | head -1)"; \
exit 0; \
fi; \
i=$$((i + 1)); \
sleep 0.25; \
done; \
echo "Warning: no /dev/ttyACM* yet — try make monitor anyway"; \
exit 0
monitor:
@command -v picocom >/dev/null || (echo "Error: picocom not found (sudo apt install picocom)" && exit 1)
@test -e "$(SERIAL_PORT)" || (echo "Error: $(SERIAL_PORT) not found (plug in Pico USB, or set SERIAL_PORT=...)" && exit 1)
@echo "==> Serial monitor on $(SERIAL_PORT) ($(SERIAL_BAUD) baud, Ctrl+A Ctrl+X to quit)"
picocom --baud $(SERIAL_BAUD) --flow n --echo $(SERIAL_PORT)
@SERIAL="$(SERIAL_PORT)"; \
if [ ! -e "$$SERIAL" ]; then \
SERIAL=$$(ls /dev/ttyACM* 2>/dev/null | head -1); \
fi; \
test -n "$$SERIAL" && test -e "$$SERIAL" || (echo "Error: no /dev/ttyACM* (plug in Pico USB, or set SERIAL_PORT=...)" && exit 1); \
echo "==> Serial monitor on $$SERIAL ($(SERIAL_BAUD) baud, Ctrl+A Ctrl+X to quit)"; \
picocom --baud $(SERIAL_BAUD) --flow n --echo "$$SERIAL"
clean:
rm -rf $(BUILD_DIR)

66
firmware/pico/README.md Normal file
View File

@@ -0,0 +1,66 @@
# Portal Pico firmware (Pico SDK)
C firmware for a **Pico + W5500 + WS2812** panel adapter under `firmware/pico/`.
Replaces the CircuitPython `adapter/code.py` path with native UDP + PIO WS2812.
## Hardware
| Signal | GPIO |
|--------|------|
| W5500 CS | GP9 |
| SPI1 SCK | GP10 |
| SPI1 MOSI | GP11 |
| SPI1 MISO | GP12 |
| W5500 RST | GP13 |
| Status LED | GP25 |
| WS2812 strip 0 | GP28 |
| WS2812 strip 1 | GP27 |
**Two strips** per Pico. Pixel count comes from each UDP frame.
`MAX_LEDS` (512) is capacity only — edit `board_config.h` to raise it.
## Build
```bash
# From repo root
make deploy PANEL_ID=0
make reset && make monitor
make build
```
Requires [pico-sdk](https://github.com/raspberrypi/pico-sdk) and `arm-none-eabi-gcc`.
UF2: `firmware/pico/build/panel.uf2`.
## Options
```bash
make deploy PANEL_ID=0 # 10.1.1.10
make deploy PANEL_ID=1 # 10.1.1.11
make deploy PANEL_ID=0 NO_DHCP=0
make deploy PANEL_ID=0 WS2812_PIN=26 WS2812_PIN1=28
```
| Panel | IP | MAC |
|-------|-----|-----|
| 0 | 10.1.1.10 | 02:50:52:54:4C:00 |
| 1 | 10.1.1.11 | 02:50:52:54:4C:01 |
| … | … | … |
| 4 | 10.1.1.14 | 02:50:52:54:4C:04 |
| 255 (bench) | 10.1.1.19 | 02:50:52:54:4C:FF |
## UDP protocol (port 50007)
| Payload | Action |
|---------|--------|
| **N×3 bytes** | Raw RGB → strip 0 |
| **1 + N×3 bytes** | `strip_id` + RGB (`0`/`1`, or `255` = all) |
| **5 bytes `PIN\\0` + gpio** | Bind strip 0 |
| **8 bytes `PIN\\0` + strip + gpio + len_lo + len_hi** | Bind strip (optional LED hint) |
| **4 bytes `SHOW`** | Reserved |
```bash
# Both strips on one Pico (strip0=GP28, strip1=GP27)
pipenv run python examples/panel_color_test.py --panel-index 0 --pins 28:405,27:351
```
Pi sends logical **RGB**. Firmware converts to **GRB** on the wire (`FORMAT_GRB`).

View File

@@ -4,6 +4,9 @@
#include "WS2812.pio.h"
#include "hardware/clocks.h"
#include "hardware/gpio.h"
#include <cstdlib>
WS2812::WS2812(uint pin, uint length, PIO pio, uint sm) {
@@ -39,9 +42,45 @@ void WS2812::initialize(uint pin, uint length, PIO pio, uint sm, DataByte b1, Da
this->bytes[1] = b2;
this->bytes[2] = b3;
this->bytes[3] = b4;
uint offset = pio_add_program(pio, &ws2812_program);
uint bits = (b1 == NONE ? 24 : 32);
ws2812_program_init(pio, sm, offset, pin, 800000, bits);
this->bits = (b1 == NONE ? 24 : 32);
/* One copy of the program per PIO block — shared by all strips on that PIO. */
static int pio0_offset = -1;
static int pio1_offset = -1;
int *cached = (pio == pio0) ? &pio0_offset : &pio1_offset;
if (*cached < 0) {
*cached = (int)pio_add_program(pio, &ws2812_program);
}
this->offset = (uint)*cached;
applyPinConfig();
}
void WS2812::applyPinConfig() {
pio_sm_set_enabled(pio, sm, false);
pio_gpio_init(pio, pin);
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
pio_sm_config c = ws2812_program_get_default_config(offset);
sm_config_set_sideset_pins(&c, pin);
sm_config_set_out_shift(&c, false, true, bits);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
int cycles_per_bit = ws2812_T1 + ws2812_T2 + ws2812_T3;
float div = (float)clock_get_hz(clk_sys) / (800000.f * (float)cycles_per_bit);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
void WS2812::setPin(uint new_pin) {
if (new_pin == pin) {
return;
}
pio_sm_set_enabled(pio, sm, false);
gpio_set_function(pin, GPIO_FUNC_SIO);
gpio_set_dir(pin, GPIO_IN);
pin = new_pin;
applyPinConfig();
}
uint32_t WS2812::convertData(uint32_t rgbw) {
@@ -86,7 +125,14 @@ void WS2812::fill(uint32_t color) {
}
void WS2812::show() {
for (uint i = 0; i < length; i++) {
show(length);
}
void WS2812::show(uint count) {
if (count > length) {
count = length;
}
for (uint i = 0; i < count; i++) {
pio_sm_put_blocking(pio, sm, data[i]);
}
}

View File

@@ -21,18 +21,24 @@ class WS2812 {
void setPixelColor(uint index, uint8_t red, uint8_t green, uint8_t blue);
void fill(uint32_t color);
void show();
void show(uint count);
void setPin(uint pin);
uint getPin() const { return pin; }
private:
uint pin;
uint length;
PIO pio;
uint sm;
uint offset;
uint bits;
DataByte bytes[4];
uint32_t *data;
void initialize(uint pin, uint length, PIO pio, uint sm, DataByte b1, DataByte b2,
DataByte b3, DataByte b4);
uint32_t convertData(uint32_t rgbw);
void applyPinConfig();
};
#endif

View File

@@ -1,7 +1,7 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H
/* Matches adapter/code.py — W5500 on SPI1, WS2812 on GP27 */
/* W5500 on SPI1; two WS2812 strips on GP27 + GP28 */
#define PIN_LED_STATUS 25
@@ -14,15 +14,25 @@
#define PIN_MISO 12
#define PIN_RST 13
/* Strip 0 / strip 1 data pins (override with -DPIN_WS2812=… / -DPIN_WS2812_1=…). */
#ifndef PIN_WS2812
#define PIN_WS2812 27
#define PIN_WS2812 28
#endif
#ifndef PIN_WS2812_1
#define PIN_WS2812_1 27
#endif
#ifndef NUM_LEDS
#define NUM_LEDS 405
/* Strip / buffer capacity. Pixel count comes from each UDP frame length. */
#ifndef MAX_LEDS
#define MAX_LEDS 512
#endif
#define LED_RGB_BYTES (NUM_LEDS * 3)
#define LED_RGB_BYTES (MAX_LEDS * 3)
/* Two WS2812 outputs on one Pico (separate GPIOs / PIO SMs). */
#ifndef MAX_STRIPS
#define MAX_STRIPS 2
#endif
/* W5500 socket assignments */
#define SOCKET_DHCP 0

362
firmware/pico/main.c Normal file
View File

@@ -0,0 +1,362 @@
/**
* Portal panel firmware — W5500 UDP + WS2812 (Pico SDK).
*
* UDP protocol (port 50007):
* - N×3 bytes: raw RGB → strip 0 (N = 1…MAX_LEDS)
* - 1 + N×3 bytes: strip_id (byte 0) + RGB
* strip_id 0…MAX_STRIPS-1 → that strip
* strip_id 255 → all active strips
* - 5 bytes "PIN\\0" + gpio: bind strip 0 to gpio
* - 8 bytes "PIN\\0" + strip + gpio + len_lo + len_hi: bind strip (len=0 → unset)
* - 4 bytes "SHOW": reserved (frames already show on receive)
*
* One Pico can drive up to MAX_STRIPS displays on separate GPIOs.
* Each strips pixel count comes from its own UDP frame length (strips may differ).
* MAX_LEDS is per-strip capacity.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "board_config.h"
#include "wizchip_conf.h"
#include "dhcp.h"
#include "socket.h"
#include "timer.h"
#include "wizchip_spi.h"
#include "ws2812_led.h"
#include "hardware/gpio.h"
#include "pico/stdlib.h"
#define SOCKET_UDP 1
#define SOCKET_DHCP 0
#define ETH_BUF_SIZE 2048
#define DHCP_RETRY_MAX 10
static wiz_NetInfo g_net_info = {
.mac = MAC_ADDR,
.ip = STATIC_IP,
.sn = STATIC_SN,
.gw = STATIC_GW,
.dns = STATIC_DNS,
#if USE_DHCP
.dhcp = NETINFO_DHCP,
#else
.dhcp = NETINFO_STATIC,
#endif
};
static uint8_t g_eth_buf[ETH_BUF_SIZE];
static uint8_t g_pixel_buf[MAX_STRIPS][LED_RGB_BYTES];
static uint16_t g_frame_pixels[MAX_STRIPS];
static uint16_t g_strip_leds[MAX_STRIPS];
static uint8_t g_frame_dirty[MAX_STRIPS];
static volatile uint16_t g_ms_tick;
static uint8_t g_dhcp_ready;
static void dhcp_timer_cb(void) {
g_ms_tick++;
if (g_ms_tick >= 999) {
g_ms_tick = 0;
DHCP_time_handler();
}
}
static void dhcp_assign(void) {
getIPfromDHCP(g_net_info.ip);
getGWfromDHCP(g_net_info.gw);
getSNfromDHCP(g_net_info.sn);
getDNSfromDHCP(g_net_info.dns);
g_net_info.dhcp = NETINFO_DHCP;
network_initialize(g_net_info);
print_network_information(g_net_info);
g_dhcp_ready = 1;
}
static void dhcp_conflict(void) {
printf("DHCP conflict\n");
}
static int network_bring_up(void) {
#if USE_DHCP
uint8_t retries = 0;
DHCP_init(SOCKET_DHCP, g_eth_buf);
reg_dhcp_cbfunc(dhcp_assign, dhcp_assign, dhcp_conflict);
wizchip_1ms_timer_initialize(dhcp_timer_cb);
while (!g_dhcp_ready && retries < DHCP_RETRY_MAX) {
int8_t rv = DHCP_run();
if (rv == DHCP_IP_LEASED) {
g_dhcp_ready = 1;
break;
}
if (rv == DHCP_FAILED) {
retries++;
}
wizchip_delay_ms(250);
}
if (!g_dhcp_ready) {
printf("DHCP failed, using static IP\n");
g_net_info.dhcp = NETINFO_STATIC;
network_initialize(g_net_info);
print_network_information(g_net_info);
}
#else
network_initialize(g_net_info);
print_network_information(g_net_info);
#endif
return 0;
}
static int udp_socket_open(void) {
int8_t sn = socket(SOCKET_UDP, Sn_MR_UDP, UDP_PORT, 0);
if (sn != SOCKET_UDP) {
printf("UDP socket open failed: %d\n", sn);
return -1;
}
printf("UDP listening on port %d\n", UDP_PORT);
return 0;
}
static void led_boot_test(unsigned int strip) {
if (!ws2812_strip_active(strip)) {
return;
}
uint8_t one[3] = {255, 0, 0};
ws2812_set_rgb(strip, one, 1);
ws2812_show(strip);
sleep_ms(150);
one[0] = 0;
one[1] = 255;
ws2812_set_rgb(strip, one, 1);
ws2812_show(strip);
sleep_ms(150);
one[1] = 0;
one[2] = 255;
ws2812_set_rgb(strip, one, 1);
ws2812_show(strip);
sleep_ms(150);
one[2] = 0;
ws2812_set_rgb(strip, one, 1);
ws2812_show(strip);
}
static int buffer_frame(unsigned int strip, const uint8_t *rgb, uint16_t nbytes) {
if (strip >= MAX_STRIPS || !ws2812_strip_active(strip)) {
return -1;
}
if (nbytes == 0 || (nbytes % 3u) != 0) {
return -1;
}
uint16_t pixels = nbytes / 3u;
if (pixels > MAX_LEDS) {
pixels = MAX_LEDS;
nbytes = (uint16_t)(pixels * 3u);
}
memcpy(g_pixel_buf[strip], rgb, nbytes);
if (nbytes < LED_RGB_BYTES) {
memset(g_pixel_buf[strip] + nbytes, 0, LED_RGB_BYTES - nbytes);
}
g_frame_pixels[strip] = pixels;
g_frame_dirty[strip] = 1;
return 0;
}
static void buffer_frame_all(const uint8_t *rgb, uint16_t nbytes) {
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (ws2812_strip_active(s)) {
buffer_frame(s, rgb, nbytes);
}
}
}
static void show_dirty_frames(void) {
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (!g_frame_dirty[s]) {
continue;
}
ws2812_set_rgb(s, g_pixel_buf[s], g_frame_pixels[s]);
ws2812_show(s);
g_frame_dirty[s] = 0;
}
}
static int pin_is_reserved(uint8_t pin) {
switch (pin) {
case PIN_CS:
case PIN_SCK:
case PIN_MOSI:
case PIN_MISO:
case PIN_RST:
case PIN_LED_STATUS:
return 1;
default:
return pin > 29;
}
}
static void handle_pin_command(uint8_t strip, uint8_t pin, uint16_t leds) {
if (strip >= MAX_STRIPS) {
printf("PIN strip %u out of range (max %u)\n", (unsigned)strip, MAX_STRIPS);
return;
}
if (pin_is_reserved(pin)) {
printf("PIN GP%u rejected (reserved)\n", (unsigned)pin);
return;
}
if (leds > MAX_LEDS) {
printf("PIN leds %u clamped to max_leds=%u\n", (unsigned)leds, MAX_LEDS);
leds = MAX_LEDS;
}
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (s != strip && ws2812_strip_active(s) && ws2812_get_pin(s) == pin) {
printf("PIN GP%u: releasing strip %u\n", (unsigned)pin, s);
ws2812_release_strip(s);
g_strip_leds[s] = 0;
g_frame_pixels[s] = 0;
g_frame_dirty[s] = 0;
}
}
if (ws2812_set_pin(strip, pin) != 0) {
printf("PIN strip %u GP%u failed (no PIO SM?)\n", (unsigned)strip, (unsigned)pin);
return;
}
g_strip_leds[strip] = leds;
if (leds) {
printf("strip %u ws2812=GP%u leds=%u\n", (unsigned)strip,
(unsigned)ws2812_get_pin(strip), (unsigned)leds);
} else {
printf("strip %u ws2812=GP%u\n", (unsigned)strip, (unsigned)ws2812_get_pin(strip));
}
}
static void handle_packet(const uint8_t *data, int16_t len) {
if (len == 4 && memcmp(data, "SHOW", 4) == 0) {
return;
}
if (len == 5 && data[0] == 'P' && data[1] == 'I' && data[2] == 'N' && data[3] == 0) {
handle_pin_command(0, data[4], 0);
return;
}
if (len == 8 && data[0] == 'P' && data[1] == 'I' && data[2] == 'N' && data[3] == 0) {
uint16_t leds = (uint16_t)data[6] | ((uint16_t)data[7] << 8);
handle_pin_command(data[4], data[5], leds);
return;
}
if (len >= 3 && (len % 3) == 0) {
buffer_frame(0, data, (uint16_t)len);
return;
}
if (len >= 4 && ((len - 1) % 3) == 0) {
uint8_t id = data[0];
uint16_t nbytes = (uint16_t)(len - 1);
if (id == 255) {
buffer_frame_all(data + 1, nbytes);
return;
}
if (id < MAX_STRIPS) {
buffer_frame(id, data + 1, nbytes);
return;
}
if (id == (uint8_t)PANEL_ID) {
buffer_frame(0, data + 1, nbytes);
return;
}
}
}
static void poll_udp(void) {
while (1) {
uint16_t rx = getSn_RX_RSR(SOCKET_UDP);
if (rx == 0) {
return;
}
if (rx > ETH_BUF_SIZE) {
rx = ETH_BUF_SIZE;
}
uint8_t src_ip[4];
uint16_t src_port;
int16_t n = recvfrom(SOCKET_UDP, g_eth_buf, rx, src_ip, &src_port);
if (n <= 0) {
return;
}
handle_packet(g_eth_buf, n);
}
}
static void print_strip_map(void) {
printf("strips=%d", MAX_STRIPS);
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (ws2812_strip_active(s)) {
if (g_strip_leds[s]) {
printf(" [%u]=GP%u/%u", s, ws2812_get_pin(s), g_strip_leds[s]);
} else {
printf(" [%u]=GP%u", s, ws2812_get_pin(s));
}
}
}
printf("\n");
}
int main(void) {
stdio_init_all();
sleep_ms(2000);
gpio_init(PIN_LED_STATUS);
gpio_set_dir(PIN_LED_STATUS, GPIO_OUT);
printf("portal panel firmware (Pico SDK)\n");
printf("max_leds=%d max_strips=%d panel_id=%d mac=02:50:52:54:4C:%02X ip=10.1.1.%u\n",
MAX_LEDS, MAX_STRIPS, PANEL_ID, (unsigned)PANEL_ID, (unsigned)STATIC_IP_OCT4);
wizchip_spi_initialize();
wizchip_cris_initialize();
wizchip_reset();
wizchip_initialize();
wizchip_check();
ws2812_init(MAX_LEDS);
memset(g_pixel_buf, 0, sizeof(g_pixel_buf));
memset(g_frame_pixels, 0, sizeof(g_frame_pixels));
memset(g_strip_leds, 0, sizeof(g_strip_leds));
memset(g_frame_dirty, 0, sizeof(g_frame_dirty));
static const unsigned int default_pins[MAX_STRIPS] = {PIN_WS2812, PIN_WS2812_1};
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (ws2812_set_pin(s, default_pins[s]) == 0) {
led_boot_test(s);
} else {
printf("default strip %u GP%u failed\n", s, default_pins[s]);
}
}
print_strip_map();
network_bring_up();
if (udp_socket_open() != 0) {
while (1) {
gpio_xor_mask(1u << PIN_LED_STATUS);
sleep_ms(100);
}
}
uint32_t heartbeat = 0;
while (1) {
#if USE_DHCP
DHCP_run();
#endif
poll_udp();
show_dirty_frames();
if (++heartbeat >= 500) {
gpio_xor_mask(1u << PIN_LED_STATUS);
heartbeat = 0;
}
sleep_ms(1);
}
}

View File

@@ -0,0 +1,176 @@
/* Multi-strip WS2812 wrapper around ForsakenNGS/Pico_WS2812 */
#include "ws2812_led.h"
#include "board_config.h"
#include "WS2812.hpp"
#include "hardware/pio.h"
#include "pico/stdlib.h"
typedef struct {
WS2812 *strip;
PIO pio;
uint sm;
unsigned int pin;
unsigned int lit;
unsigned int show_n;
uint8_t active;
} strip_slot_t;
static strip_slot_t s_slots[MAX_STRIPS];
static unsigned int s_max_leds;
static int claim_sm(PIO *pio_out, uint *sm_out) {
int sm = pio_claim_unused_sm(pio0, false);
if (sm >= 0) {
*pio_out = pio0;
*sm_out = (uint)sm;
return 0;
}
sm = pio_claim_unused_sm(pio1, false);
if (sm >= 0) {
*pio_out = pio1;
*sm_out = (uint)sm;
return 0;
}
return -1;
}
extern "C" void ws2812_init(unsigned int max_leds) {
s_max_leds = max_leds;
for (unsigned int i = 0; i < MAX_STRIPS; i++) {
s_slots[i].strip = nullptr;
s_slots[i].pin = 0;
s_slots[i].lit = 0;
s_slots[i].show_n = 0;
s_slots[i].active = 0;
}
}
extern "C" void ws2812_release_strip(unsigned int strip) {
if (strip >= MAX_STRIPS) {
return;
}
strip_slot_t *slot = &s_slots[strip];
if (!slot->active) {
return;
}
pio_sm_set_enabled(slot->pio, slot->sm, false);
gpio_set_function(slot->pin, GPIO_FUNC_SIO);
gpio_set_dir(slot->pin, GPIO_IN);
delete slot->strip;
slot->strip = nullptr;
pio_sm_unclaim(slot->pio, slot->sm);
slot->pin = 0;
slot->lit = 0;
slot->show_n = 0;
slot->active = 0;
}
extern "C" int ws2812_set_pin(unsigned int strip, unsigned int pin) {
if (strip >= MAX_STRIPS || pin > 29) {
return -1;
}
strip_slot_t *slot = &s_slots[strip];
if (slot->active && slot->pin == pin) {
return 0;
}
/* Steal pin from any other strip that already owns it. */
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (s != strip && s_slots[s].active && s_slots[s].pin == pin) {
ws2812_release_strip(s);
}
}
if (slot->active) {
/* Tear down and rebuild on the same PIO SM — more reliable than setPin alone. */
PIO pio = slot->pio;
uint sm = slot->sm;
unsigned int old_pin = slot->pin;
pio_sm_set_enabled(pio, sm, false);
gpio_set_function(old_pin, GPIO_FUNC_SIO);
gpio_set_dir(old_pin, GPIO_IN);
delete slot->strip;
slot->strip = new WS2812(pin, s_max_leds, pio, sm, WS2812::FORMAT_GRB);
slot->pin = pin;
slot->lit = 0;
slot->show_n = 0;
return 0;
}
PIO pio;
uint sm;
if (claim_sm(&pio, &sm) != 0) {
return -1;
}
slot->strip = new WS2812(pin, s_max_leds, pio, sm, WS2812::FORMAT_GRB);
slot->pio = pio;
slot->sm = sm;
slot->pin = pin;
slot->lit = 0;
slot->show_n = 0;
slot->active = 1;
return 0;
}
extern "C" unsigned int ws2812_get_pin(unsigned int strip) {
if (strip >= MAX_STRIPS || !s_slots[strip].active) {
return 0;
}
return s_slots[strip].pin;
}
extern "C" int ws2812_strip_active(unsigned int strip) {
return strip < MAX_STRIPS && s_slots[strip].active;
}
extern "C" void ws2812_set_rgb(unsigned int strip, const uint8_t *rgb, unsigned int count) {
if (strip >= MAX_STRIPS || !s_slots[strip].active || rgb == nullptr) {
return;
}
strip_slot_t *slot = &s_slots[strip];
if (count > s_max_leds) {
count = s_max_leds;
}
unsigned int old = slot->lit;
unsigned int i = 0;
for (; i < count; i++) {
slot->strip->setPixelColor(i, rgb[i * 3 + 0], rgb[i * 3 + 1], rgb[i * 3 + 2]);
}
for (; i < old; i++) {
slot->strip->setPixelColor(i, 0, 0, 0);
}
slot->lit = count;
slot->show_n = count > old ? count : old;
}
extern "C" void ws2812_fill(unsigned int strip, uint8_t r, uint8_t g, uint8_t b) {
if (strip >= MAX_STRIPS || !s_slots[strip].active) {
return;
}
strip_slot_t *slot = &s_slots[strip];
slot->strip->fill(WS2812::RGB(r, g, b));
slot->lit = s_max_leds;
slot->show_n = s_max_leds;
}
extern "C" void ws2812_show(unsigned int strip) {
if (strip >= MAX_STRIPS || !s_slots[strip].active) {
return;
}
strip_slot_t *slot = &s_slots[strip];
unsigned int n = slot->show_n;
if (n == 0) {
n = 1;
}
slot->strip->show(n);
sleep_us(300);
slot->show_n = slot->lit;
}
extern "C" void ws2812_show_all(void) {
for (unsigned int i = 0; i < MAX_STRIPS; i++) {
if (s_slots[i].active) {
ws2812_show(i);
}
}
}

View File

@@ -0,0 +1,33 @@
#ifndef WS2812_LED_H
#define WS2812_LED_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef MAX_STRIPS
#define MAX_STRIPS 4
#endif
/* Create strip slots (none active until ws2812_set_pin). max_leds = capacity each. */
void ws2812_init(unsigned int max_leds);
/* Bind strip to gpio (creates/rebinds PIO SM). strip = 0 … MAX_STRIPS-1. */
int ws2812_set_pin(unsigned int strip, unsigned int pin);
/* Free PIO SM + GPIO so another strip can take the pin. */
void ws2812_release_strip(unsigned int strip);
unsigned int ws2812_get_pin(unsigned int strip);
int ws2812_strip_active(unsigned int strip);
void ws2812_set_rgb(unsigned int strip, const uint8_t *rgb, unsigned int count);
void ws2812_fill(unsigned int strip, uint8_t r, uint8_t g, uint8_t b);
void ws2812_show(unsigned int strip);
void ws2812_show_all(void);
#ifdef __cplusplus
}
#endif
#endif