Add ArtNetDMX Pico W firmware with dual DMX and docs.
Art-Net to two RS-485 outputs over Wi-Fi (AP or STA with SoftAP fallback), web UI, mDNS, and QLC+/ArtPoll notes for unicast on STA. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
/build/
|
||||||
|
*.uf2
|
||||||
|
*.elf
|
||||||
|
*.bin
|
||||||
|
*.hex
|
||||||
|
*.dis
|
||||||
|
*.map
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.o
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
|
||||||
|
set(PICO_BOARD pico_w CACHE STRING "Board type")
|
||||||
|
set(PICO_CYW43_ARCH_THREADSAFE_BACKGROUND ON)
|
||||||
|
|
||||||
|
include(${CMAKE_CURRENT_LIST_DIR}/pico_sdk_import.cmake)
|
||||||
|
|
||||||
|
project(artnetdmx C CXX ASM)
|
||||||
|
set(CMAKE_C_STANDARD 11)
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
|
||||||
|
pico_sdk_init()
|
||||||
|
|
||||||
|
add_executable(artnetdmx
|
||||||
|
src/main.c
|
||||||
|
src/config.c
|
||||||
|
src/channels.c
|
||||||
|
src/dmx_out.c
|
||||||
|
src/artnet.c
|
||||||
|
src/wifi_mgr.c
|
||||||
|
src/http_server.c
|
||||||
|
third_party/dhcpserver/dhcpserver.c
|
||||||
|
)
|
||||||
|
|
||||||
|
pico_generate_pio_header(artnetdmx ${CMAKE_CURRENT_LIST_DIR}/src/dmx_out.pio)
|
||||||
|
|
||||||
|
target_include_directories(artnetdmx PRIVATE
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/include
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/third_party/dhcpserver
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(artnetdmx PRIVATE
|
||||||
|
PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(artnetdmx
|
||||||
|
pico_stdlib
|
||||||
|
pico_cyw43_arch_lwip_threadsafe_background
|
||||||
|
pico_lwip_mdns
|
||||||
|
hardware_pio
|
||||||
|
hardware_dma
|
||||||
|
hardware_flash
|
||||||
|
hardware_sync
|
||||||
|
)
|
||||||
|
|
||||||
|
pico_configure_ip4_address(artnetdmx PRIVATE
|
||||||
|
CYW43_DEFAULT_IP_AP_ADDRESS 192.168.4.1
|
||||||
|
)
|
||||||
|
|
||||||
|
pico_enable_stdio_usb(artnetdmx 1)
|
||||||
|
pico_enable_stdio_uart(artnetdmx 0)
|
||||||
|
|
||||||
|
pico_add_extra_outputs(artnetdmx)
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# ArtNetDMX — Pico W Art-Net → Dual DMX
|
||||||
|
|
||||||
|
Firmware for **Raspberry Pi Pico W** that receives Art-Net over Wi‑Fi and outputs **two DMX512 universes** through RS‑485, using **PIO + DMA** at **40 Hz**. A web UI configures Wi‑Fi (STA / AP) and shows / sets all 512 channels per universe.
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
| Signal | Pico W | RS‑485 module |
|
||||||
|
|--------|--------|----------------|
|
||||||
|
| DMX A data | GP0 | DI (universe A) |
|
||||||
|
| DMX B data | GP1 | DI (universe B) |
|
||||||
|
| GND | GND | GND |
|
||||||
|
| Power | 3V3 or 5V | VCC (many MAX485 boards want 5V) |
|
||||||
|
|
||||||
|
TX‑only: tie **DE** and **/RE** high (to VCC) on each module. XLR pinout is typically 1=GND, 2=Data− (B), 3=Data+ (A) — match your module labels.
|
||||||
|
|
||||||
|
Pins are defined in [`include/board.h`](include/board.h).
|
||||||
|
|
||||||
|
```
|
||||||
|
Pico W GP0 ──► MAX485 A DI
|
||||||
|
Pico W GP1 ──► MAX485 B DI
|
||||||
|
Pico W GND ──► both module GND
|
||||||
|
VCC ──► both module VCC
|
||||||
|
DE,/RE ──► VCC (TX enable)
|
||||||
|
A/B ──► XLR / DMX cable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Requires [Pico SDK](https://github.com/raspberrypi/pico-sdk) and an ARM GCC toolchain.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PICO_SDK_PATH=/path/to/pico-sdk
|
||||||
|
mkdir -p build && cd build
|
||||||
|
cmake -DPICO_BOARD=pico_w ..
|
||||||
|
make -j$(nproc)
|
||||||
|
```
|
||||||
|
|
||||||
|
Flash with BOOTSEL + drag-and-drop, or:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picotool load -f build/artnetdmx.uf2
|
||||||
|
```
|
||||||
|
|
||||||
|
USB serial (115200) prints boot status, Wi‑Fi mode, and IP addresses.
|
||||||
|
|
||||||
|
Do not `cp` the UF2 onto a path that is not the mounted `RPI-RP2` volume (a failed copy can leave a plain file instead of flashing).
|
||||||
|
|
||||||
|
## First boot (AP)
|
||||||
|
|
||||||
|
1. Join Wi‑Fi **ArtNetDMX** / password **artnetdmx**
|
||||||
|
2. Open **http://192.168.4.1/** (or **http://artnetdmx.local/** once mDNS is up)
|
||||||
|
3. **WiFi** page: choose mode, credentials, Art‑Net universe numbers, mDNS hostname
|
||||||
|
4. Save — device reboots
|
||||||
|
|
||||||
|
### Wi‑Fi modes
|
||||||
|
|
||||||
|
| Mode | Behaviour |
|
||||||
|
|------|-----------|
|
||||||
|
| **Access Point** | SoftAP only (default `ArtNetDMX` / `artnetdmx` @ `192.168.4.1`) |
|
||||||
|
| **Station** | Joins your LAN only. If join fails, SoftAP starts for recovery |
|
||||||
|
|
||||||
|
There is no concurrent AP+STA mode (legacy “AP+Station” configs are treated as Station). SoftAP is paused while STA obtains DHCP, then left off if join succeeds.
|
||||||
|
|
||||||
|
mDNS hostname defaults to **`artnetdmx`** → **http://artnetdmx.local/** (configurable on the WiFi page).
|
||||||
|
|
||||||
|
## Art-Net
|
||||||
|
|
||||||
|
- UDP port **6454**
|
||||||
|
- Replies to **ArtPoll** / sends **ArtPollReply** (QLC+ and similar discovery)
|
||||||
|
- Maps configured universes to physical ports A (GP0) and B (GP1)
|
||||||
|
- Defaults: universe **0** → A, universe **1** → B
|
||||||
|
|
||||||
|
### SoftAP vs Station (important)
|
||||||
|
|
||||||
|
| Mode | Art-Net transport |
|
||||||
|
|------|-------------------|
|
||||||
|
| **SoftAP** | Controller joins the Pico’s AP. **Broadcast** ArtDmx usually works. |
|
||||||
|
| **Station** | Pico and controller share a home/office AP. Many APs **drop Wi‑Fi broadcast**, so ArtDmx broadcast never arrives. Use **unicast** to the Pico’s STA IP. |
|
||||||
|
|
||||||
|
Unicast ArtDmx on STA has been verified end-to-end; subnet broadcast often is not delivered by the upstream AP.
|
||||||
|
|
||||||
|
### QLC+ setup
|
||||||
|
|
||||||
|
1. Put the Pico in **Station** mode and note its IP (web UI, serial, or mDNS).
|
||||||
|
2. **Inputs/Outputs** → Art-Net plugin.
|
||||||
|
3. You will typically see **two nodes**: **QLC+ itself** and **ArtNetDMX**. Select **ArtNetDMX** (not the QLC+ loopback node).
|
||||||
|
4. Set output to **unicast** → Pico STA IP (e.g. `10.1.1.190`), not broadcast and not `192.168.4.1` unless you are on SoftAP.
|
||||||
|
5. Enable the Art-Net **output** line (icon lit).
|
||||||
|
6. Map universes: **0** → DMX A, **1** → DMX B (or match the WiFi page).
|
||||||
|
7. Patch fixtures to those universes.
|
||||||
|
|
||||||
|
Confirm on **http://<pico-ip>/** (Channels): Art-Net packet count should rise and channel values should follow faders. If the web UI updates but fixtures do not, check RS‑485 wiring / DE·/RE / cabling.
|
||||||
|
|
||||||
|
## Web UI
|
||||||
|
|
||||||
|
- **Channels** — tabs for universe A/B; all 512 values; edit live; Clear / All 255
|
||||||
|
- **WiFi** — AP or Station, credentials, Art‑Net universes, mDNS hostname, live debug line
|
||||||
|
- Status strip: mode, IP, Art‑Net packet count, age since last packet per port
|
||||||
|
|
||||||
|
Manual channel edits drive the same buffers as Art‑Net; new Art‑Net packets overwrite those channels.
|
||||||
|
|
||||||
|
Embedded pages live in `src/http_server.c`. The `www/` tree is a reference copy of the HTML structure.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
| Path | Role |
|
||||||
|
|------|------|
|
||||||
|
| `src/main.c` | Boot, DMX timer, poll loops |
|
||||||
|
| `src/wifi_mgr.c` | SoftAP / STA, DHCP, mDNS hooks |
|
||||||
|
| `src/artnet.c` | ArtDmx + ArtPoll/ArtPollReply |
|
||||||
|
| `src/dmx_out.c` + `.pio` | PIO + DMA DMX TX |
|
||||||
|
| `src/http_server.c` | Web UI + JSON API |
|
||||||
|
| `src/config.c` | Flash config (CRC, versioned) |
|
||||||
|
| `lwipopts.h` | lwIP tuning (UDP PCBs, SoftAP DHCP, broadcast) |
|
||||||
|
|
||||||
|
## License notes
|
||||||
|
|
||||||
|
DMX PIO timing is adapted from [jostlowe/Pico-DMX](https://github.com/jostlowe/Pico-DMX) (BSD‑3‑Clause). DHCP server is from MicroPython / pico‑examples (MIT).
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#ifndef ARTNET_H
|
||||||
|
#define ARTNET_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
bool artnet_start(void);
|
||||||
|
/** Re-bind listeners to current AP/STA IPs (call after SoftAP re-assert or STA join). */
|
||||||
|
void artnet_refresh(void);
|
||||||
|
/** Broadcast ArtPollReply (call after interface comes up). */
|
||||||
|
void artnet_announce(void);
|
||||||
|
/** Periodic discovery announces (call from main loop). */
|
||||||
|
void artnet_poll(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#ifndef BOARD_H
|
||||||
|
#define BOARD_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/** DMX universe A (port 0) TX pin → RS-485 DI */
|
||||||
|
#define BOARD_DMX_A_PIN 0
|
||||||
|
/** DMX universe B (port 1) TX pin → RS-485 DI */
|
||||||
|
#define BOARD_DMX_B_PIN 1
|
||||||
|
|
||||||
|
#define BOARD_NUM_UNIVERSES 2
|
||||||
|
#define BOARD_DMX_CHANNELS 512
|
||||||
|
#define BOARD_DMX_FRAME_LEN (1 + BOARD_DMX_CHANNELS) /* start code + 512 */
|
||||||
|
|
||||||
|
#define BOARD_DMX_HZ 40
|
||||||
|
#define BOARD_DMX_PERIOD_MS (1000 / BOARD_DMX_HZ)
|
||||||
|
|
||||||
|
#define BOARD_DEFAULT_UNIVERSE_A 0
|
||||||
|
#define BOARD_DEFAULT_UNIVERSE_B 1
|
||||||
|
|
||||||
|
#define BOARD_AP_SSID_DEFAULT "ArtNetDMX"
|
||||||
|
#define BOARD_AP_PASS_DEFAULT "artnetdmx"
|
||||||
|
#define BOARD_HOSTNAME "artnetdmx"
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#ifndef CHANNELS_H
|
||||||
|
#define CHANNELS_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "board.h"
|
||||||
|
|
||||||
|
void channels_init(void);
|
||||||
|
|
||||||
|
/** Returns pointer to 512 channel values for universe index 0 or 1 (no start code). */
|
||||||
|
uint8_t *channels_get(unsigned universe);
|
||||||
|
|
||||||
|
/** Build a 513-byte DMX frame (start code 0 + 512 channels) into out. */
|
||||||
|
void channels_build_frame(unsigned universe, uint8_t out[BOARD_DMX_FRAME_LEN]);
|
||||||
|
|
||||||
|
void channels_set(unsigned universe, unsigned channel, uint8_t value);
|
||||||
|
void channels_set_range(unsigned universe, const uint8_t *values, size_t len);
|
||||||
|
void channels_clear(unsigned universe);
|
||||||
|
|
||||||
|
void channels_note_artnet(unsigned universe);
|
||||||
|
uint32_t channels_artnet_packets(void);
|
||||||
|
uint32_t channels_ms_since_artnet(unsigned universe);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#ifndef CONFIG_H
|
||||||
|
#define CONFIG_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "board.h"
|
||||||
|
|
||||||
|
#define CONFIG_MAGIC 0x414E4458u /* 'ANDX' */
|
||||||
|
#define CONFIG_VERSION 3
|
||||||
|
|
||||||
|
#define CONFIG_SSID_MAX 32
|
||||||
|
#define CONFIG_PASS_MAX 64
|
||||||
|
#define CONFIG_HOSTNAME_MAX 32
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
WIFI_MODE_AP = 0,
|
||||||
|
WIFI_MODE_STA = 1,
|
||||||
|
/** Legacy flash value; treated as STA (with AP fallback on failure). */
|
||||||
|
WIFI_MODE_AP_STA = 2,
|
||||||
|
} wifi_mode_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t magic;
|
||||||
|
uint32_t version;
|
||||||
|
uint32_t crc;
|
||||||
|
wifi_mode_t wifi_mode;
|
||||||
|
char sta_ssid[CONFIG_SSID_MAX];
|
||||||
|
char sta_pass[CONFIG_PASS_MAX];
|
||||||
|
char ap_ssid[CONFIG_SSID_MAX];
|
||||||
|
char ap_pass[CONFIG_PASS_MAX];
|
||||||
|
uint16_t universe_a;
|
||||||
|
uint16_t universe_b;
|
||||||
|
/* v3: mDNS hostname without ".local" (e.g. "artnetdmx") */
|
||||||
|
char mdns_host[CONFIG_HOSTNAME_MAX];
|
||||||
|
} config_t;
|
||||||
|
|
||||||
|
void config_set_defaults(config_t *cfg);
|
||||||
|
bool config_load(config_t *cfg);
|
||||||
|
bool config_save(const config_t *cfg);
|
||||||
|
const config_t *config_get(void);
|
||||||
|
void config_init(void);
|
||||||
|
/** Lowercase DNS-label sanitize into out; returns false if empty/invalid → uses default. */
|
||||||
|
bool config_sanitize_hostname(const char *in, char *out, size_t out_sz);
|
||||||
|
|
||||||
|
static inline const char *wifi_mode_name(wifi_mode_t m) {
|
||||||
|
switch (m) {
|
||||||
|
case WIFI_MODE_STA:
|
||||||
|
case WIFI_MODE_AP_STA:
|
||||||
|
return "STA";
|
||||||
|
case WIFI_MODE_AP:
|
||||||
|
default:
|
||||||
|
return "AP";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#ifndef DMX_OUT_H
|
||||||
|
#define DMX_OUT_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "hardware/pio.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
PIO pio;
|
||||||
|
uint sm;
|
||||||
|
uint dma;
|
||||||
|
uint pin;
|
||||||
|
uint prgm_offset;
|
||||||
|
bool inited;
|
||||||
|
} dmx_out_t;
|
||||||
|
|
||||||
|
bool dmx_out_init(dmx_out_t *out, uint pin, PIO pio);
|
||||||
|
void dmx_out_write(dmx_out_t *out, const uint8_t *frame, uint length);
|
||||||
|
bool dmx_out_busy(const dmx_out_t *out);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef HTTP_SERVER_H
|
||||||
|
#define HTTP_SERVER_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
bool http_server_start(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#ifndef WIFI_MGR_H
|
||||||
|
#define WIFI_MGR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AP mode: SoftAP only.
|
||||||
|
* STA mode: join LAN; if that fails, SoftAP comes up for recovery.
|
||||||
|
*/
|
||||||
|
bool wifi_mgr_start(const config_t *cfg);
|
||||||
|
/** Call from main loop — runs deferred STA join (STA mode only). */
|
||||||
|
void wifi_mgr_poll(void);
|
||||||
|
wifi_mode_t wifi_mgr_active_mode(void);
|
||||||
|
/** Primary IP for UI (STA if up, else AP). */
|
||||||
|
const char *wifi_mgr_ip_str(void);
|
||||||
|
const char *wifi_mgr_sta_ip_str(void);
|
||||||
|
const char *wifi_mgr_ap_ip_str(void);
|
||||||
|
const char *wifi_mgr_mdns_name(void);
|
||||||
|
bool wifi_mgr_sta_up(void);
|
||||||
|
bool wifi_mgr_ap_up(void);
|
||||||
|
bool wifi_mgr_is_up(void);
|
||||||
|
const char *wifi_mgr_debug(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef _LWIPOPTS_H
|
||||||
|
#define _LWIPOPTS_H
|
||||||
|
|
||||||
|
#include "lwipopts_examples_common.h"
|
||||||
|
|
||||||
|
#undef MEM_SIZE
|
||||||
|
#define MEM_SIZE 16000
|
||||||
|
#undef MEMP_NUM_TCP_SEG
|
||||||
|
#define MEMP_NUM_TCP_SEG 48
|
||||||
|
#undef PBUF_POOL_SIZE
|
||||||
|
#define PBUF_POOL_SIZE 32
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Default MEMP_NUM_UDP_PCB is 4 — not enough for concurrent SoftAP DHCP
|
||||||
|
* server + STA DHCP client + Art-Net (ANY/AP/STA) + mDNS. STA then sticks
|
||||||
|
* at CYW43_LINK_NOIP with dhcp_start() returning ERR_MEM (-1).
|
||||||
|
*/
|
||||||
|
#undef MEMP_NUM_UDP_PCB
|
||||||
|
#define MEMP_NUM_UDP_PCB 12
|
||||||
|
|
||||||
|
/* mDNS + DHCP server + dual netif (AP+STA) need extra sys_timeout slots.
|
||||||
|
* Too-small MEMP_NUM_SYS_TIMEOUT panics when a SoftAP client joins (DHCP). */
|
||||||
|
#define LWIP_MDNS_RESPONDER 1
|
||||||
|
#define LWIP_IGMP 1
|
||||||
|
#define LWIP_NUM_NETIF_CLIENT_DATA 1
|
||||||
|
#define MDNS_RESP_USENETIF_EXTCALLBACK 1
|
||||||
|
#undef MEMP_NUM_SYS_TIMEOUT
|
||||||
|
#define MEMP_NUM_SYS_TIMEOUT (LWIP_NUM_SYS_TIMEOUT_INTERNAL + 16)
|
||||||
|
|
||||||
|
/* Allow Art-Net UDP bind on AP IP + STA IP + ANY (same port). */
|
||||||
|
#define SO_REUSE 1
|
||||||
|
|
||||||
|
/* Art-Net discovery uses directed / limited broadcast. */
|
||||||
|
#define IP_SOF_BROADCAST 1
|
||||||
|
#define IP_SOF_BROADCAST_RECV 1
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#ifndef _LWIPOPTS_EXAMPLE_COMMONH_H
|
||||||
|
#define _LWIPOPTS_EXAMPLE_COMMONH_H
|
||||||
|
|
||||||
|
|
||||||
|
// Common settings used in most of the pico_w examples
|
||||||
|
// (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html for details)
|
||||||
|
|
||||||
|
// allow override in some examples
|
||||||
|
#ifndef NO_SYS
|
||||||
|
#define NO_SYS 1
|
||||||
|
#endif
|
||||||
|
// allow override in some examples
|
||||||
|
#ifndef LWIP_SOCKET
|
||||||
|
#define LWIP_SOCKET 0
|
||||||
|
#endif
|
||||||
|
#if PICO_CYW43_ARCH_POLL
|
||||||
|
#define MEM_LIBC_MALLOC 1
|
||||||
|
#else
|
||||||
|
// MEM_LIBC_MALLOC is incompatible with non polling versions
|
||||||
|
#define MEM_LIBC_MALLOC 0
|
||||||
|
#endif
|
||||||
|
#define MEM_ALIGNMENT 4
|
||||||
|
#ifndef MEM_SIZE
|
||||||
|
#define MEM_SIZE 4000
|
||||||
|
#endif
|
||||||
|
#define MEMP_NUM_TCP_SEG 32
|
||||||
|
#define MEMP_NUM_ARP_QUEUE 10
|
||||||
|
#define PBUF_POOL_SIZE 24
|
||||||
|
#define LWIP_ARP 1
|
||||||
|
#define LWIP_ETHERNET 1
|
||||||
|
#define LWIP_ICMP 1
|
||||||
|
#define LWIP_RAW 1
|
||||||
|
#define TCP_WND (8 * TCP_MSS)
|
||||||
|
#define TCP_MSS 1460
|
||||||
|
#define TCP_SND_BUF (8 * TCP_MSS)
|
||||||
|
#define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS))
|
||||||
|
#define LWIP_NETIF_STATUS_CALLBACK 1
|
||||||
|
#define LWIP_NETIF_LINK_CALLBACK 1
|
||||||
|
#define LWIP_NETIF_HOSTNAME 1
|
||||||
|
#define LWIP_NETCONN 0
|
||||||
|
#define MEM_STATS 0
|
||||||
|
#define SYS_STATS 0
|
||||||
|
#define MEMP_STATS 0
|
||||||
|
#define LINK_STATS 0
|
||||||
|
// #define ETH_PAD_SIZE 2
|
||||||
|
#define LWIP_CHKSUM_ALGORITHM 3
|
||||||
|
#define LWIP_DHCP 1
|
||||||
|
#define LWIP_IPV4 1
|
||||||
|
#define LWIP_TCP 1
|
||||||
|
#define LWIP_UDP 1
|
||||||
|
#define LWIP_DNS 1
|
||||||
|
#define LWIP_TCP_KEEPALIVE 1
|
||||||
|
#define LWIP_NETIF_TX_SINGLE_PBUF 1
|
||||||
|
#define DHCP_DOES_ARP_CHECK 0
|
||||||
|
#define LWIP_DHCP_DOES_ACD_CHECK 0
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
#define LWIP_DEBUG 1
|
||||||
|
#define LWIP_STATS 1
|
||||||
|
#define LWIP_STATS_DISPLAY 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define ETHARP_DEBUG LWIP_DBG_OFF
|
||||||
|
#define NETIF_DEBUG LWIP_DBG_OFF
|
||||||
|
#define PBUF_DEBUG LWIP_DBG_OFF
|
||||||
|
#define API_LIB_DEBUG LWIP_DBG_OFF
|
||||||
|
#define API_MSG_DEBUG LWIP_DBG_OFF
|
||||||
|
#define SOCKETS_DEBUG LWIP_DBG_OFF
|
||||||
|
#define ICMP_DEBUG LWIP_DBG_OFF
|
||||||
|
#define INET_DEBUG LWIP_DBG_OFF
|
||||||
|
#define IP_DEBUG LWIP_DBG_OFF
|
||||||
|
#define IP_REASS_DEBUG LWIP_DBG_OFF
|
||||||
|
#define RAW_DEBUG LWIP_DBG_OFF
|
||||||
|
#define MEM_DEBUG LWIP_DBG_OFF
|
||||||
|
#define MEMP_DEBUG LWIP_DBG_OFF
|
||||||
|
#define SYS_DEBUG LWIP_DBG_OFF
|
||||||
|
#define TCP_DEBUG LWIP_DBG_OFF
|
||||||
|
#define TCP_INPUT_DEBUG LWIP_DBG_OFF
|
||||||
|
#define TCP_OUTPUT_DEBUG LWIP_DBG_OFF
|
||||||
|
#define TCP_RTO_DEBUG LWIP_DBG_OFF
|
||||||
|
#define TCP_CWND_DEBUG LWIP_DBG_OFF
|
||||||
|
#define TCP_WND_DEBUG LWIP_DBG_OFF
|
||||||
|
#define TCP_FR_DEBUG LWIP_DBG_OFF
|
||||||
|
#define TCP_QLEN_DEBUG LWIP_DBG_OFF
|
||||||
|
#define TCP_RST_DEBUG LWIP_DBG_OFF
|
||||||
|
#define UDP_DEBUG LWIP_DBG_OFF
|
||||||
|
#define TCPIP_DEBUG LWIP_DBG_OFF
|
||||||
|
#define PPP_DEBUG LWIP_DBG_OFF
|
||||||
|
#define SLIP_DEBUG LWIP_DBG_OFF
|
||||||
|
#define DHCP_DEBUG LWIP_DBG_OFF
|
||||||
|
|
||||||
|
#endif /* __LWIPOPTS_H__ */
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
|
||||||
|
|
||||||
|
# This can be dropped into an external project to help locate this SDK
|
||||||
|
# It should be include()ed prior to project()
|
||||||
|
|
||||||
|
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
||||||
|
# following conditions are met:
|
||||||
|
#
|
||||||
|
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
||||||
|
# disclaimer.
|
||||||
|
#
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
||||||
|
# disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||||
|
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||||
|
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||||
|
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
|
||||||
|
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
|
||||||
|
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
|
||||||
|
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
|
||||||
|
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
|
||||||
|
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
|
||||||
|
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
|
||||||
|
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
|
||||||
|
|
||||||
|
if (NOT PICO_SDK_PATH)
|
||||||
|
if (PICO_SDK_FETCH_FROM_GIT)
|
||||||
|
include(FetchContent)
|
||||||
|
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
|
||||||
|
if (PICO_SDK_FETCH_FROM_GIT_PATH)
|
||||||
|
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
|
||||||
|
endif ()
|
||||||
|
FetchContent_Declare(
|
||||||
|
pico_sdk
|
||||||
|
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||||
|
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (NOT pico_sdk)
|
||||||
|
message("Downloading Raspberry Pi Pico SDK")
|
||||||
|
# GIT_SUBMODULES_RECURSE was added in 3.17
|
||||||
|
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
|
||||||
|
FetchContent_Populate(
|
||||||
|
pico_sdk
|
||||||
|
QUIET
|
||||||
|
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||||
|
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||||
|
GIT_SUBMODULES_RECURSE FALSE
|
||||||
|
|
||||||
|
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
|
||||||
|
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
|
||||||
|
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
|
||||||
|
)
|
||||||
|
else ()
|
||||||
|
FetchContent_Populate(
|
||||||
|
pico_sdk
|
||||||
|
QUIET
|
||||||
|
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||||
|
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||||
|
|
||||||
|
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
|
||||||
|
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
|
||||||
|
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
|
||||||
|
endif ()
|
||||||
|
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
|
||||||
|
else ()
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
|
||||||
|
if (NOT EXISTS ${PICO_SDK_PATH})
|
||||||
|
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
|
||||||
|
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
|
||||||
|
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
|
||||||
|
|
||||||
|
include(${PICO_SDK_INIT_CMAKE_FILE})
|
||||||
+339
@@ -0,0 +1,339 @@
|
|||||||
|
#include "artnet.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "lwip/pbuf.h"
|
||||||
|
#include "lwip/udp.h"
|
||||||
|
#include "lwip/netif.h"
|
||||||
|
#include "pico/cyw43_arch.h"
|
||||||
|
#include "pico/stdlib.h"
|
||||||
|
|
||||||
|
#include "board.h"
|
||||||
|
#include "channels.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "wifi_mgr.h"
|
||||||
|
|
||||||
|
#define ARTNET_PORT 6454
|
||||||
|
#define ARTNET_OPCODE_POLL 0x2000
|
||||||
|
#define ARTNET_OPCODE_POLLREPLY 0x2100
|
||||||
|
#define ARTNET_OPCODE_DMX 0x5000
|
||||||
|
|
||||||
|
/* Classic ArtPollReply length (Art-Net 3 / widely accepted by QLC+). */
|
||||||
|
#define ARTNET_POLLREPLY_LEN 239
|
||||||
|
|
||||||
|
static struct udp_pcb *g_pcb;
|
||||||
|
static uint32_t g_rx_count;
|
||||||
|
static uint32_t g_poll_count;
|
||||||
|
static absolute_time_t g_announce_until;
|
||||||
|
static absolute_time_t g_next_announce;
|
||||||
|
static absolute_time_t g_last_poll_reply;
|
||||||
|
|
||||||
|
static uint16_t rd_le16(const uint8_t *p) {
|
||||||
|
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wr_le16(uint8_t *p, uint16_t v) {
|
||||||
|
p[0] = (uint8_t)(v & 0xff);
|
||||||
|
p[1] = (uint8_t)(v >> 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fill_ip4(uint8_t out[4], const ip4_addr_t *ip) {
|
||||||
|
uint32_t a = ip4_addr_get_u32(ip);
|
||||||
|
out[0] = (uint8_t)(a & 0xff);
|
||||||
|
out[1] = (uint8_t)((a >> 8) & 0xff);
|
||||||
|
out[2] = (uint8_t)((a >> 16) & 0xff);
|
||||||
|
out[3] = (uint8_t)((a >> 24) & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool our_ipv4(ip4_addr_t *out) {
|
||||||
|
if (wifi_mgr_sta_up()) {
|
||||||
|
*out = *netif_ip4_addr(&cyw43_state.netif[CYW43_ITF_STA]);
|
||||||
|
if (!ip4_addr_isany_val(*out)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wifi_mgr_ap_up()) {
|
||||||
|
*out = *netif_ip4_addr(&cyw43_state.netif[CYW43_ITF_AP]);
|
||||||
|
if (!ip4_addr_isany_val(*out)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
IP4_ADDR(out, 192, 168, 4, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct udp_pcb *send_pcb(void) {
|
||||||
|
return g_pcb;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void build_poll_reply(uint8_t *pkt) {
|
||||||
|
const config_t *cfg = config_get();
|
||||||
|
memset(pkt, 0, ARTNET_POLLREPLY_LEN);
|
||||||
|
|
||||||
|
memcpy(pkt, "Art-Net", 8);
|
||||||
|
wr_le16(&pkt[8], ARTNET_OPCODE_POLLREPLY);
|
||||||
|
|
||||||
|
ip4_addr_t ip;
|
||||||
|
if (our_ipv4(&ip)) {
|
||||||
|
fill_ip4(&pkt[10], &ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
wr_le16(&pkt[14], ARTNET_PORT); /* Protocol port 0x1936 */
|
||||||
|
|
||||||
|
pkt[16] = 1; /* VersInfo Hi */
|
||||||
|
pkt[17] = 0; /* VersInfo Lo — firmware 1.0 */
|
||||||
|
|
||||||
|
/* 15-bit Port-Address split across Net / Sub / Universe nibble */
|
||||||
|
uint16_t pa = cfg->universe_a & 0x7fff;
|
||||||
|
pkt[18] = (uint8_t)((pa >> 8) & 0x7f); /* NetSwitch */
|
||||||
|
pkt[19] = (uint8_t)((pa >> 4) & 0x0f); /* SubSwitch */
|
||||||
|
|
||||||
|
pkt[20] = 0x00; /* Oem Hi — unregistered / prototype */
|
||||||
|
pkt[21] = 0xff; /* Oem Lo */
|
||||||
|
pkt[22] = 0; /* UbeaVersion */
|
||||||
|
pkt[23] = 0xe0; /* Status1: indicators normal, Port-Address Authority set */
|
||||||
|
|
||||||
|
pkt[24] = 0x00; /* ESTA Man Lo */
|
||||||
|
pkt[25] = 0x00; /* ESTA Man Hi */
|
||||||
|
|
||||||
|
strncpy((char *)&pkt[26], "ArtNetDMX", 17);
|
||||||
|
strncpy((char *)&pkt[44], "ArtNetDMX Pico W dual DMX", 63);
|
||||||
|
|
||||||
|
snprintf((char *)&pkt[108], 64, "#0001 [%04u] OK", (unsigned)(g_rx_count & 0xffff));
|
||||||
|
|
||||||
|
pkt[172] = 0;
|
||||||
|
pkt[173] = 2; /* NumPorts = 2 */
|
||||||
|
|
||||||
|
/* PortTypes: Art-Net capable of output (DMX out from network) */
|
||||||
|
pkt[174] = 0x80;
|
||||||
|
pkt[175] = 0x80;
|
||||||
|
|
||||||
|
/* GoodInput unused for TX-only */
|
||||||
|
pkt[178] = 0;
|
||||||
|
pkt[179] = 0;
|
||||||
|
|
||||||
|
/* GoodOutputA: data is being transmitted on channel */
|
||||||
|
pkt[182] = 0x80;
|
||||||
|
pkt[183] = 0x80;
|
||||||
|
|
||||||
|
/* SwIn unused */
|
||||||
|
/* SwOut: low 4 bits of each port's Port-Address */
|
||||||
|
pkt[190] = (uint8_t)(cfg->universe_a & 0x0f);
|
||||||
|
pkt[191] = (uint8_t)(cfg->universe_b & 0x0f);
|
||||||
|
|
||||||
|
pkt[200] = 0x00; /* Style = StNode */
|
||||||
|
|
||||||
|
uint8_t mac[6] = {0};
|
||||||
|
int itf = wifi_mgr_sta_up() ? CYW43_ITF_STA : CYW43_ITF_AP;
|
||||||
|
cyw43_wifi_get_mac(&cyw43_state, itf, mac);
|
||||||
|
memcpy(&pkt[201], mac, 6);
|
||||||
|
|
||||||
|
if (our_ipv4(&ip)) {
|
||||||
|
fill_ip4(&pkt[207], &ip); /* BindIp */
|
||||||
|
}
|
||||||
|
pkt[211] = 1; /* BindIndex */
|
||||||
|
pkt[212] = 0x08; /* Status2: DHCP capable / 15-bit Port-Address support */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_poll_reply(const ip_addr_t *dst, u16_t port) {
|
||||||
|
struct udp_pcb *pcb = send_pcb();
|
||||||
|
if (!pcb || !dst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t pkt[ARTNET_POLLREPLY_LEN];
|
||||||
|
build_poll_reply(pkt);
|
||||||
|
|
||||||
|
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, ARTNET_POLLREPLY_LEN, PBUF_RAM);
|
||||||
|
if (!p) {
|
||||||
|
printf("[artnet] poll reply alloc failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memcpy(p->payload, pkt, ARTNET_POLLREPLY_LEN);
|
||||||
|
|
||||||
|
err_t err = udp_sendto(pcb, p, dst, port);
|
||||||
|
pbuf_free(p);
|
||||||
|
if (err != ERR_OK) {
|
||||||
|
printf("[artnet] poll reply send failed %d\n", (int)err);
|
||||||
|
} else if ((g_poll_count <= 4) || ((g_poll_count & 0x1f) == 0)) {
|
||||||
|
printf("[artnet] ArtPollReply #%lu -> %s:%u\n",
|
||||||
|
(unsigned long)g_poll_count, ipaddr_ntoa(dst), port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void artnet_announce(void) {
|
||||||
|
ip4_addr_t local;
|
||||||
|
if (!our_ipv4(&local)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* One subnet-directed broadcast — dual announces looked like two nodes in QLC+. */
|
||||||
|
ip_addr_t bcast;
|
||||||
|
ip4_addr_t mask;
|
||||||
|
if (wifi_mgr_sta_up()) {
|
||||||
|
mask = *netif_ip4_netmask(&cyw43_state.netif[CYW43_ITF_STA]);
|
||||||
|
} else {
|
||||||
|
IP4_ADDR(&mask, 255, 255, 255, 0);
|
||||||
|
}
|
||||||
|
ip4_addr_t b4;
|
||||||
|
b4.addr = (local.addr & mask.addr) | ~mask.addr;
|
||||||
|
ip_addr_set_ip4_u32_val(bcast, b4.addr);
|
||||||
|
|
||||||
|
cyw43_arch_lwip_begin();
|
||||||
|
send_poll_reply(&bcast, ARTNET_PORT);
|
||||||
|
cyw43_arch_lwip_end();
|
||||||
|
g_poll_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void artnet_poll(void) {
|
||||||
|
if (is_nil_time(g_announce_until) || time_reached(g_announce_until)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!time_reached(g_next_announce)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_next_announce = make_timeout_time_ms(2500);
|
||||||
|
artnet_announce();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void artnet_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p,
|
||||||
|
const ip_addr_t *addr, u16_t port) {
|
||||||
|
(void)pcb;
|
||||||
|
const char *via = (const char *)arg;
|
||||||
|
|
||||||
|
if (!p) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p->tot_len < 12) {
|
||||||
|
pbuf_free(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t hdr[18];
|
||||||
|
size_t want = p->tot_len < sizeof(hdr) ? p->tot_len : sizeof(hdr);
|
||||||
|
if (pbuf_copy_partial(p, hdr, want, 0) != want) {
|
||||||
|
pbuf_free(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memcmp(hdr, "Art-Net", 8) != 0) {
|
||||||
|
pbuf_free(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t opcode = rd_le16(&hdr[8]);
|
||||||
|
|
||||||
|
if (opcode == ARTNET_OPCODE_POLL) {
|
||||||
|
g_poll_count++;
|
||||||
|
if ((g_poll_count <= 8) || ((g_poll_count & 0x1f) == 0)) {
|
||||||
|
printf("[artnet] ArtPoll #%lu via=%s from=%s:%u\n",
|
||||||
|
(unsigned long)g_poll_count, via ? via : "?",
|
||||||
|
addr ? ipaddr_ntoa(addr) : "?", port);
|
||||||
|
}
|
||||||
|
/* One reply per poll burst — dual PCBs used to create a ghost second node. */
|
||||||
|
if (addr && (is_nil_time(g_last_poll_reply) ||
|
||||||
|
absolute_time_diff_us(g_last_poll_reply, get_absolute_time()) > 200000)) {
|
||||||
|
g_last_poll_reply = get_absolute_time();
|
||||||
|
send_poll_reply(addr, port ? port : ARTNET_PORT);
|
||||||
|
}
|
||||||
|
pbuf_free(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opcode != ARTNET_OPCODE_DMX) {
|
||||||
|
pbuf_free(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p->tot_len < 18) {
|
||||||
|
pbuf_free(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ArtDmx: sequence(12), physical(13), universe LE(14-15), length BE(16-17), data... */
|
||||||
|
uint16_t universe = rd_le16(&hdr[14]);
|
||||||
|
uint16_t length = ((uint16_t)hdr[16] << 8) | hdr[17];
|
||||||
|
if (length > BOARD_DMX_CHANNELS) {
|
||||||
|
length = BOARD_DMX_CHANNELS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config_t *cfg = config_get();
|
||||||
|
int port_idx = -1;
|
||||||
|
if (universe == cfg->universe_a) {
|
||||||
|
port_idx = 0;
|
||||||
|
} else if (universe == cfg->universe_b) {
|
||||||
|
port_idx = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_rx_count++;
|
||||||
|
if ((g_rx_count <= 8) || ((g_rx_count & 0x3f) == 0)) {
|
||||||
|
printf("[artnet] #%lu via=%s from=%s uni=%u len=%u -> %s\n",
|
||||||
|
(unsigned long)g_rx_count,
|
||||||
|
via ? via : "?",
|
||||||
|
addr ? ipaddr_ntoa(addr) : "?",
|
||||||
|
universe, length,
|
||||||
|
port_idx == 0 ? "DMX A" : port_idx == 1 ? "DMX B" : "ignored");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port_idx >= 0 && length > 0) {
|
||||||
|
uint8_t data[BOARD_DMX_CHANNELS];
|
||||||
|
memset(data, 0, sizeof(data));
|
||||||
|
uint16_t copied = pbuf_copy_partial(p, data, length, 18);
|
||||||
|
if (copied > 0) {
|
||||||
|
channels_set_range((unsigned)port_idx, data, copied);
|
||||||
|
channels_note_artnet((unsigned)port_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pbuf_free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove_pcb(void) {
|
||||||
|
if (g_pcb) {
|
||||||
|
udp_remove(g_pcb);
|
||||||
|
g_pcb = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct udp_pcb *bind_listener(void) {
|
||||||
|
struct udp_pcb *pcb = udp_new_ip_type(IPADDR_TYPE_V4);
|
||||||
|
if (!pcb) {
|
||||||
|
printf("[artnet] udp_new failed\n");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
ip_set_option(pcb, SOF_BROADCAST);
|
||||||
|
err_t err = udp_bind(pcb, IP4_ADDR_ANY, ARTNET_PORT);
|
||||||
|
if (err != ERR_OK) {
|
||||||
|
printf("[artnet] bind :%u failed (%d)\n", ARTNET_PORT, (int)err);
|
||||||
|
udp_remove(pcb);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
udp_recv(pcb, artnet_recv, (void *)"UDP");
|
||||||
|
printf("[artnet] listening on 0.0.0.0:%u\n", ARTNET_PORT);
|
||||||
|
return pcb;
|
||||||
|
}
|
||||||
|
|
||||||
|
void artnet_refresh(void) {
|
||||||
|
cyw43_arch_lwip_begin();
|
||||||
|
remove_pcb();
|
||||||
|
g_pcb = bind_listener();
|
||||||
|
if (wifi_mgr_sta_up()) {
|
||||||
|
netif_set_default(&cyw43_state.netif[CYW43_ITF_STA]);
|
||||||
|
}
|
||||||
|
cyw43_arch_lwip_end();
|
||||||
|
|
||||||
|
if (wifi_mgr_sta_up() || wifi_mgr_ap_up()) {
|
||||||
|
artnet_announce();
|
||||||
|
/* Short burst only — long announce spam confused QLC+ discovery. */
|
||||||
|
g_announce_until = make_timeout_time_ms(8000);
|
||||||
|
g_next_announce = make_timeout_time_ms(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool artnet_start(void) {
|
||||||
|
printf("[artnet] start (port %u)\n", ARTNET_PORT);
|
||||||
|
artnet_refresh();
|
||||||
|
return g_pcb != NULL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#include "channels.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include "pico/stdlib.h"
|
||||||
|
#include "pico/time.h"
|
||||||
|
|
||||||
|
static uint8_t g_channels[BOARD_NUM_UNIVERSES][BOARD_DMX_CHANNELS];
|
||||||
|
static absolute_time_t g_last_artnet[BOARD_NUM_UNIVERSES];
|
||||||
|
static volatile uint32_t g_artnet_packets;
|
||||||
|
|
||||||
|
void channels_init(void) {
|
||||||
|
memset(g_channels, 0, sizeof(g_channels));
|
||||||
|
for (unsigned u = 0; u < BOARD_NUM_UNIVERSES; u++) {
|
||||||
|
g_last_artnet[u] = nil_time;
|
||||||
|
}
|
||||||
|
g_artnet_packets = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *channels_get(unsigned universe) {
|
||||||
|
if (universe >= BOARD_NUM_UNIVERSES) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return g_channels[universe];
|
||||||
|
}
|
||||||
|
|
||||||
|
void channels_build_frame(unsigned universe, uint8_t out[BOARD_DMX_FRAME_LEN]) {
|
||||||
|
if (!out || universe >= BOARD_NUM_UNIVERSES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out[0] = 0; /* start code */
|
||||||
|
memcpy(&out[1], g_channels[universe], BOARD_DMX_CHANNELS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void channels_set(unsigned universe, unsigned channel, uint8_t value) {
|
||||||
|
if (universe >= BOARD_NUM_UNIVERSES || channel >= BOARD_DMX_CHANNELS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_channels[universe][channel] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void channels_set_range(unsigned universe, const uint8_t *values, size_t len) {
|
||||||
|
if (universe >= BOARD_NUM_UNIVERSES || !values) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (len > BOARD_DMX_CHANNELS) {
|
||||||
|
len = BOARD_DMX_CHANNELS;
|
||||||
|
}
|
||||||
|
memcpy(g_channels[universe], values, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void channels_clear(unsigned universe) {
|
||||||
|
if (universe >= BOARD_NUM_UNIVERSES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memset(g_channels[universe], 0, BOARD_DMX_CHANNELS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void channels_note_artnet(unsigned universe) {
|
||||||
|
if (universe >= BOARD_NUM_UNIVERSES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_last_artnet[universe] = get_absolute_time();
|
||||||
|
g_artnet_packets++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t channels_artnet_packets(void) {
|
||||||
|
return g_artnet_packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t channels_ms_since_artnet(unsigned universe) {
|
||||||
|
if (universe >= BOARD_NUM_UNIVERSES) {
|
||||||
|
return UINT32_MAX;
|
||||||
|
}
|
||||||
|
if (is_nil_time(g_last_artnet[universe])) {
|
||||||
|
return UINT32_MAX;
|
||||||
|
}
|
||||||
|
return (uint32_t)absolute_time_diff_us(g_last_artnet[universe], get_absolute_time()) / 1000u;
|
||||||
|
}
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "hardware/flash.h"
|
||||||
|
#include "hardware/sync.h"
|
||||||
|
#include "pico/stdlib.h"
|
||||||
|
|
||||||
|
/* Store config in the last flash sector. */
|
||||||
|
#define CONFIG_FLASH_OFFSET (PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE)
|
||||||
|
|
||||||
|
/* Layout before mdns_host was added (v1/v2). */
|
||||||
|
#define CONFIG_SIZE_V2 offsetof(config_t, mdns_host)
|
||||||
|
|
||||||
|
static config_t g_config;
|
||||||
|
|
||||||
|
static uint32_t crc32_calc(const uint8_t *data, size_t len) {
|
||||||
|
uint32_t crc = 0xFFFFFFFFu;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
crc ^= data[i];
|
||||||
|
for (int b = 0; b < 8; b++) {
|
||||||
|
uint32_t mask = -(crc & 1u);
|
||||||
|
crc = (crc >> 1) ^ (0xEDB88320u & mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ~crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t config_payload_crc(const config_t *cfg) {
|
||||||
|
config_t tmp = *cfg;
|
||||||
|
tmp.crc = 0;
|
||||||
|
return crc32_calc((const uint8_t *)&tmp, sizeof(tmp));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool config_sanitize_hostname(const char *in, char *out, size_t out_sz) {
|
||||||
|
if (!out || out_sz < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t j = 0;
|
||||||
|
for (size_t i = 0; in && in[i] && j + 1 < out_sz && j < 63; i++) {
|
||||||
|
unsigned char c = (unsigned char)in[i];
|
||||||
|
if (c == '.') {
|
||||||
|
continue; /* strip .local if pasted */
|
||||||
|
}
|
||||||
|
if (isalnum(c)) {
|
||||||
|
out[j++] = (char)tolower(c);
|
||||||
|
} else if (c == '-' && j > 0) {
|
||||||
|
out[j++] = '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (j > 0 && out[j - 1] == '-') {
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
out[j] = 0;
|
||||||
|
if (j == 0) {
|
||||||
|
strncpy(out, BOARD_HOSTNAME, out_sz - 1);
|
||||||
|
out[out_sz - 1] = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void config_set_defaults(config_t *cfg) {
|
||||||
|
memset(cfg, 0, sizeof(*cfg));
|
||||||
|
cfg->magic = CONFIG_MAGIC;
|
||||||
|
cfg->version = CONFIG_VERSION;
|
||||||
|
cfg->wifi_mode = WIFI_MODE_AP;
|
||||||
|
strncpy(cfg->ap_ssid, BOARD_AP_SSID_DEFAULT, CONFIG_SSID_MAX - 1);
|
||||||
|
strncpy(cfg->ap_pass, BOARD_AP_PASS_DEFAULT, CONFIG_PASS_MAX - 1);
|
||||||
|
cfg->universe_a = BOARD_DEFAULT_UNIVERSE_A;
|
||||||
|
cfg->universe_b = BOARD_DEFAULT_UNIVERSE_B;
|
||||||
|
strncpy(cfg->mdns_host, BOARD_HOSTNAME, CONFIG_HOSTNAME_MAX - 1);
|
||||||
|
cfg->crc = config_payload_crc(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool config_load(config_t *cfg) {
|
||||||
|
const config_t *flash_cfg = (const config_t *)(XIP_BASE + CONFIG_FLASH_OFFSET);
|
||||||
|
if (flash_cfg->magic != CONFIG_MAGIC) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t ver = flash_cfg->version;
|
||||||
|
if (ver < 1 || ver > CONFIG_VERSION) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
config_t tmp;
|
||||||
|
memset(&tmp, 0, sizeof(tmp));
|
||||||
|
size_t copy_n = (ver < 3) ? CONFIG_SIZE_V2 : sizeof(config_t);
|
||||||
|
memcpy(&tmp, flash_cfg, copy_n);
|
||||||
|
|
||||||
|
uint32_t expect = tmp.crc;
|
||||||
|
tmp.crc = 0;
|
||||||
|
size_t crc_len = (ver < 3) ? CONFIG_SIZE_V2 : sizeof(config_t);
|
||||||
|
if (crc32_calc((const uint8_t *)&tmp, crc_len) != expect) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*cfg = tmp;
|
||||||
|
cfg->version = CONFIG_VERSION;
|
||||||
|
/* AP+STA is gone — legacy value 2 becomes STA (AP fallback on failure). */
|
||||||
|
if (cfg->wifi_mode == WIFI_MODE_AP_STA || cfg->wifi_mode > WIFI_MODE_STA) {
|
||||||
|
cfg->wifi_mode = (cfg->wifi_mode == WIFI_MODE_AP_STA) ? WIFI_MODE_STA : WIFI_MODE_AP;
|
||||||
|
}
|
||||||
|
if (!cfg->mdns_host[0]) {
|
||||||
|
strncpy(cfg->mdns_host, BOARD_HOSTNAME, CONFIG_HOSTNAME_MAX - 1);
|
||||||
|
} else {
|
||||||
|
char cleaned[CONFIG_HOSTNAME_MAX];
|
||||||
|
config_sanitize_hostname(cfg->mdns_host, cleaned, sizeof(cleaned));
|
||||||
|
strncpy(cfg->mdns_host, cleaned, CONFIG_HOSTNAME_MAX - 1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool config_save(const config_t *cfg) {
|
||||||
|
config_t to_write = *cfg;
|
||||||
|
to_write.magic = CONFIG_MAGIC;
|
||||||
|
to_write.version = CONFIG_VERSION;
|
||||||
|
char cleaned[CONFIG_HOSTNAME_MAX];
|
||||||
|
config_sanitize_hostname(to_write.mdns_host, cleaned, sizeof(cleaned));
|
||||||
|
strncpy(to_write.mdns_host, cleaned, CONFIG_HOSTNAME_MAX - 1);
|
||||||
|
to_write.mdns_host[CONFIG_HOSTNAME_MAX - 1] = 0;
|
||||||
|
to_write.crc = config_payload_crc(&to_write);
|
||||||
|
|
||||||
|
uint8_t sector[FLASH_SECTOR_SIZE];
|
||||||
|
memset(sector, 0xFF, sizeof(sector));
|
||||||
|
memcpy(sector, &to_write, sizeof(to_write));
|
||||||
|
|
||||||
|
uint32_t ints = save_and_disable_interrupts();
|
||||||
|
flash_range_erase(CONFIG_FLASH_OFFSET, FLASH_SECTOR_SIZE);
|
||||||
|
flash_range_program(CONFIG_FLASH_OFFSET, sector, FLASH_SECTOR_SIZE);
|
||||||
|
restore_interrupts(ints);
|
||||||
|
|
||||||
|
g_config = to_write;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config_t *config_get(void) {
|
||||||
|
return &g_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
void config_init(void) {
|
||||||
|
if (!config_load(&g_config)) {
|
||||||
|
printf("[config] no valid flash config — using defaults (AP)\n");
|
||||||
|
config_set_defaults(&g_config);
|
||||||
|
} else {
|
||||||
|
printf("[config] loaded from flash: mode=%d sta='%s' mdns='%s'\n",
|
||||||
|
(int)g_config.wifi_mode, g_config.sta_ssid, g_config.mdns_host);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#include "dmx_out.h"
|
||||||
|
|
||||||
|
#include "hardware/clocks.h"
|
||||||
|
#include "hardware/dma.h"
|
||||||
|
#include "hardware/irq.h"
|
||||||
|
#include "dmx_out.pio.h"
|
||||||
|
|
||||||
|
#define DMX_SM_FREQ 1000000u
|
||||||
|
|
||||||
|
bool dmx_out_init(dmx_out_t *out, uint pin, PIO pio) {
|
||||||
|
if (!out) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pio_can_add_program(pio, &dmx_out_program)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint offset = pio_add_program(pio, &dmx_out_program);
|
||||||
|
|
||||||
|
int sm = pio_claim_unused_sm(pio, false);
|
||||||
|
if (sm < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pio_sm_set_pins_with_mask(pio, (uint)sm, 1u << pin, 1u << pin);
|
||||||
|
pio_sm_set_pindirs_with_mask(pio, (uint)sm, 1u << pin, 1u << pin);
|
||||||
|
pio_gpio_init(pio, pin);
|
||||||
|
|
||||||
|
pio_sm_config conf = dmx_out_program_get_default_config(offset);
|
||||||
|
sm_config_set_out_pins(&conf, pin, 1);
|
||||||
|
sm_config_set_sideset_pins(&conf, pin);
|
||||||
|
sm_config_set_clkdiv(&conf, (float)clock_get_hz(clk_sys) / (float)DMX_SM_FREQ);
|
||||||
|
sm_config_set_out_shift(&conf, true, false, 8);
|
||||||
|
|
||||||
|
pio_sm_init(pio, (uint)sm, offset, &conf);
|
||||||
|
pio_sm_set_enabled(pio, (uint)sm, true);
|
||||||
|
|
||||||
|
int dma = dma_claim_unused_channel(false);
|
||||||
|
if (dma < 0) {
|
||||||
|
pio_sm_set_enabled(pio, (uint)sm, false);
|
||||||
|
pio_sm_unclaim(pio, (uint)sm);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dma_channel_config dma_conf = dma_channel_get_default_config((uint)dma);
|
||||||
|
channel_config_set_transfer_data_size(&dma_conf, DMA_SIZE_8);
|
||||||
|
channel_config_set_read_increment(&dma_conf, true);
|
||||||
|
channel_config_set_write_increment(&dma_conf, false);
|
||||||
|
channel_config_set_dreq(&dma_conf, pio_get_dreq(pio, (uint)sm, true));
|
||||||
|
|
||||||
|
dma_channel_configure((uint)dma, &dma_conf, &pio->txf[sm], NULL, 0, false);
|
||||||
|
|
||||||
|
out->pio = pio;
|
||||||
|
out->sm = (uint)sm;
|
||||||
|
out->dma = (uint)dma;
|
||||||
|
out->pin = pin;
|
||||||
|
out->prgm_offset = offset;
|
||||||
|
out->inited = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dmx_out_write(dmx_out_t *out, const uint8_t *frame, uint length) {
|
||||||
|
if (!out || !out->inited || !frame || length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pio_sm_set_enabled(out->pio, out->sm, false);
|
||||||
|
pio_sm_clear_fifos(out->pio, out->sm);
|
||||||
|
pio_sm_restart(out->pio, out->sm);
|
||||||
|
pio_sm_exec(out->pio, out->sm, pio_encode_jmp(out->prgm_offset));
|
||||||
|
pio_sm_set_enabled(out->pio, out->sm, true);
|
||||||
|
|
||||||
|
dma_channel_transfer_from_buffer_now(out->dma, frame, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool dmx_out_busy(const dmx_out_t *out) {
|
||||||
|
if (!out || !out->inited) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dma_channel_is_busy(out->dma)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !pio_sm_is_tx_fifo_empty(out->pio, out->sm);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
;
|
||||||
|
; DMX512 TX via PIO at 1 MHz (adapted from jostlowe/Pico-DMX, BSD-3-Clause)
|
||||||
|
;
|
||||||
|
; BREAK ~176 us, MAB ~8 us, then 8 data bits with start/stop timing via side-set.
|
||||||
|
;
|
||||||
|
|
||||||
|
.program dmx_out
|
||||||
|
.side_set 1 opt
|
||||||
|
|
||||||
|
set x, 21 side 0 ; break (line low)
|
||||||
|
break_loop:
|
||||||
|
jmp x-- break_loop [7]
|
||||||
|
nop side 1 [7] ; mark-after-break
|
||||||
|
|
||||||
|
.wrap_target
|
||||||
|
pull block side 1 [7] ; stop / idle high between bytes
|
||||||
|
set x, 7 side 0 [3] ; start bit
|
||||||
|
bitloop:
|
||||||
|
out pins, 1
|
||||||
|
jmp x-- bitloop [2]
|
||||||
|
.wrap
|
||||||
@@ -0,0 +1,645 @@
|
|||||||
|
#include "http_server.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
#include "pico/cyw43_arch.h"
|
||||||
|
#include "pico/stdlib.h"
|
||||||
|
#include "hardware/watchdog.h"
|
||||||
|
#include "lwip/pbuf.h"
|
||||||
|
#include "lwip/tcp.h"
|
||||||
|
|
||||||
|
#include "board.h"
|
||||||
|
#include "channels.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "wifi_mgr.h"
|
||||||
|
|
||||||
|
#define HTTP_PORT 80
|
||||||
|
#define REQ_BUF 2048
|
||||||
|
#define RESP_BUF 4096
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
struct tcp_pcb *pcb;
|
||||||
|
char req[REQ_BUF];
|
||||||
|
int req_len;
|
||||||
|
char *resp;
|
||||||
|
int resp_len;
|
||||||
|
int sent;
|
||||||
|
bool close_after;
|
||||||
|
} http_conn_t;
|
||||||
|
|
||||||
|
static struct tcp_pcb *g_server;
|
||||||
|
|
||||||
|
static const char PAGE_INDEX[] =
|
||||||
|
"<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\">"
|
||||||
|
"<title>ArtNetDMX</title><link rel=stylesheet href=/style.css></head><body>"
|
||||||
|
"<header><h1>ArtNetDMX</h1><nav><a href=/ class=active>Channels</a><a href=/wifi>WiFi</a></nav>"
|
||||||
|
"<div id=status class=status>…</div></header>"
|
||||||
|
"<main><div class=tabs>"
|
||||||
|
"<button type=button id=tab0 class=active data-u=0>Universe A</button>"
|
||||||
|
"<button type=button id=tab1 data-u=1>Universe B</button>"
|
||||||
|
"<button type=button id=clear>Clear</button>"
|
||||||
|
"<button type=button id=full>All 255</button>"
|
||||||
|
"</div><div id=grid class=grid></div></main>"
|
||||||
|
"<script src=/app.js></script></body></html>";
|
||||||
|
|
||||||
|
static const char PAGE_WIFI[] =
|
||||||
|
"<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\">"
|
||||||
|
"<title>WiFi — ArtNetDMX</title><link rel=stylesheet href=/style.css></head><body>"
|
||||||
|
"<header><h1>ArtNetDMX</h1><nav><a href=/>Channels</a><a href=/wifi class=active>WiFi</a></nav>"
|
||||||
|
"<div id=status class=status>…</div></header>"
|
||||||
|
"<main class=form-wrap>"
|
||||||
|
"<section class=ip-box><div class=ip-label>Current IP</div><div id=currentIp class=ip-value>—</div>"
|
||||||
|
"<div id=currentMode class=ip-meta>—</div>"
|
||||||
|
"<div id=addrDetail class=ip-meta></div>"
|
||||||
|
"<div id=mdnsName class=ip-meta></div>"
|
||||||
|
"<div id=wifiDebug class=ip-debug>—</div>"
|
||||||
|
"<p class=hint id=artnetHint></p></section>"
|
||||||
|
"<form id=wifiForm>"
|
||||||
|
"<label>Mode<select name=mode id=mode>"
|
||||||
|
"<option value=0>Access Point</option>"
|
||||||
|
"<option value=1>Station (AP fallback if join fails)</option>"
|
||||||
|
"</select></label>"
|
||||||
|
"<p class=hint>Station joins your LAN only. If join fails, SoftAP starts for recovery. mDNS name is set below.</p>"
|
||||||
|
"<fieldset><legend>Station</legend>"
|
||||||
|
"<label>SSID<input name=sta_ssid id=sta_ssid maxlength=31></label>"
|
||||||
|
"<label>Password<input name=sta_pass id=sta_pass type=password maxlength=63></label>"
|
||||||
|
"</fieldset>"
|
||||||
|
"<fieldset><legend>Access Point</legend>"
|
||||||
|
"<label>SSID<input name=ap_ssid id=ap_ssid maxlength=31></label>"
|
||||||
|
"<label>Password<input name=ap_pass id=ap_pass type=password maxlength=63></label>"
|
||||||
|
"</fieldset>"
|
||||||
|
"<fieldset><legend>Art-Net universes</legend>"
|
||||||
|
"<label>Port A<input name=universe_a id=universe_a type=number min=0 max=32767></label>"
|
||||||
|
"<label>Port B<input name=universe_b id=universe_b type=number min=0 max=32767></label>"
|
||||||
|
"</fieldset>"
|
||||||
|
"<fieldset><legend>mDNS</legend>"
|
||||||
|
"<label>Hostname<input name=mdns_host id=mdns_host maxlength=31 pattern=\"[A-Za-z0-9-]+\" placeholder=artnetdmx></label>"
|
||||||
|
"<p class=hint>Resolves as <code>http://<hostname>.local/</code> on the LAN (and SoftAP). Letters, digits, hyphen. Reboot required.</p>"
|
||||||
|
"</fieldset>"
|
||||||
|
"<button type=submit>Save & reboot</button>"
|
||||||
|
"<p class=hint>After save the device reboots into the selected mode. AP default: ArtNetDMX / artnetdmx @ 192.168.4.1</p>"
|
||||||
|
"</form></main>"
|
||||||
|
"<script src=/wifi.js></script></body></html>";
|
||||||
|
|
||||||
|
static const char PAGE_CSS[] =
|
||||||
|
":root{--bg:#0e1116;--panel:#171b22;--text:#e8ecf1;--muted:#8b95a5;--accent:#f0a202;--line:#2a3140}"
|
||||||
|
"*{box-sizing:border-box}body{margin:0;font-family:\"IBM Plex Sans\",Segoe UI,sans-serif;background:radial-gradient(1200px 600px at 10% -10%,#1a2230,var(--bg));color:var(--text);min-height:100vh}"
|
||||||
|
"header{padding:1rem 1.25rem;border-bottom:1px solid var(--line);display:flex;flex-wrap:wrap;gap:.75rem 1.5rem;align-items:center}"
|
||||||
|
"h1{margin:0;font-family:\"IBM Plex Mono\",ui-monospace,monospace;font-size:1.25rem;letter-spacing:.04em;color:var(--accent)}"
|
||||||
|
"nav{display:flex;gap:.75rem}nav a{color:var(--muted);text-decoration:none;padding:.35rem .6rem;border-radius:4px}nav a.active,nav a:hover{color:var(--text);background:var(--panel)}"
|
||||||
|
".status{margin-left:auto;font-size:.85rem;color:var(--muted);font-family:\"IBM Plex Mono\",monospace}"
|
||||||
|
"main{padding:1rem}"
|
||||||
|
".tabs{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem}"
|
||||||
|
"button,.tabs button{background:var(--panel);color:var(--text);border:1px solid var(--line);padding:.45rem .8rem;border-radius:4px;cursor:pointer}"
|
||||||
|
"button:hover,.tabs button.active{border-color:var(--accent);color:var(--accent)}"
|
||||||
|
".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(3.4rem,1fr));gap:4px}"
|
||||||
|
".cell{background:var(--panel);border:1px solid var(--line);border-radius:3px;padding:2px;text-align:center}"
|
||||||
|
".cell label{display:block;font-size:.65rem;color:var(--muted);font-family:monospace}"
|
||||||
|
".cell input{width:100%;background:#0a0d12;border:none;color:var(--text);text-align:center;font-family:monospace;font-size:.85rem;padding:.2rem 0}"
|
||||||
|
".form-wrap{max-width:32rem}form{display:flex;flex-direction:column;gap:1rem}"
|
||||||
|
"label{display:flex;flex-direction:column;gap:.35rem;font-size:.9rem;color:var(--muted)}"
|
||||||
|
"input,select{background:var(--panel);border:1px solid var(--line);color:var(--text);padding:.55rem .65rem;border-radius:4px;font:inherit}"
|
||||||
|
"fieldset{border:1px solid var(--line);border-radius:6px;padding:1rem;display:flex;flex-direction:column;gap:.75rem}"
|
||||||
|
"legend{padding:0 .4rem;color:var(--accent)}"
|
||||||
|
".hint{color:var(--muted);font-size:.85rem;line-height:1.4}"
|
||||||
|
"button[type=submit]{background:var(--accent);color:#111;border:none;font-weight:600;padding:.7rem 1rem}"
|
||||||
|
".ip-box{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:1rem 1.1rem;margin-bottom:1.25rem}"
|
||||||
|
".ip-label{font-size:.75rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:.35rem}"
|
||||||
|
".ip-value{font-family:\"IBM Plex Mono\",ui-monospace,monospace;font-size:1.6rem;color:var(--accent);letter-spacing:.02em}"
|
||||||
|
".ip-meta{margin-top:.35rem;font-size:.85rem;color:var(--muted);font-family:\"IBM Plex Mono\",monospace}"
|
||||||
|
".ip-debug{margin-top:.55rem;font-size:.8rem;color:#c9d1d9;font-family:\"IBM Plex Mono\",monospace;line-height:1.35;word-break:break-word}";
|
||||||
|
|
||||||
|
static const char PAGE_APP_JS[] =
|
||||||
|
"let uni=0;const grid=document.getElementById('grid');"
|
||||||
|
"function mkGrid(){grid.innerHTML='';for(let i=0;i<512;i++){const d=document.createElement('div');d.className='cell';"
|
||||||
|
"d.innerHTML=`<label>${i+1}</label><input data-ch=${i} type=number min=0 max=255 value=0>`;"
|
||||||
|
"d.querySelector('input').addEventListener('change',e=>{const ch=+e.target.dataset.ch;const val=Math.max(0,Math.min(255,+e.target.value||0));"
|
||||||
|
"e.target.value=val;fetch('/api/channels',{method:'POST',headers:{'Content-Type':'application/json'},"
|
||||||
|
"body:JSON.stringify({u:uni,ch,val})});});grid.appendChild(d);}}"
|
||||||
|
"async function refresh(){try{const r=await fetch('/api/channels?u='+uni);const j=await r.json();"
|
||||||
|
"const inputs=grid.querySelectorAll('input');j.values.forEach((v,i)=>{if(document.activeElement!==inputs[i])inputs[i].value=v;});"
|
||||||
|
"const s=await fetch('/api/status');const st=await s.json();"
|
||||||
|
"document.getElementById('status').textContent=`${st.mode} ${st.ip}${st.mdns?' · '+st.mdns:''} | ArtNet ${st.artnet_packets} pkts | A ${st.age_a}ms B ${st.age_b}ms`;"
|
||||||
|
"}catch(e){}}"
|
||||||
|
"document.querySelectorAll('[data-u]').forEach(b=>b.addEventListener('click',()=>{"
|
||||||
|
"document.querySelectorAll('[data-u]').forEach(x=>x.classList.remove('active'));b.classList.add('active');uni=+b.dataset.u;refresh();}));"
|
||||||
|
"document.getElementById('clear').onclick=()=>fetch('/api/channels',{method:'POST',headers:{'Content-Type':'application/json'},"
|
||||||
|
"body:JSON.stringify({u:uni,clear:true})}).then(refresh);"
|
||||||
|
"document.getElementById('full').onclick=()=>{const values=Array(512).fill(255);"
|
||||||
|
"fetch('/api/channels',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({u:uni,values})}).then(refresh);};"
|
||||||
|
"mkGrid();refresh();setInterval(refresh,2000);";
|
||||||
|
|
||||||
|
static const char PAGE_WIFI_JS[] =
|
||||||
|
"function showSt(st){document.getElementById('status').textContent=st.mode+' '+st.ip;"
|
||||||
|
"document.getElementById('currentIp').textContent=st.ip;"
|
||||||
|
"document.getElementById('currentMode').textContent='Mode: '+st.mode;"
|
||||||
|
"let d=[];if(st.sta_up)d.push('STA '+st.sta_ip);if(st.ap_up)d.push('AP '+st.ap_ip);"
|
||||||
|
"document.getElementById('addrDetail').textContent=d.join(' · ');"
|
||||||
|
"document.getElementById('mdnsName').textContent=st.mdns?'mDNS: http://'+st.mdns+'/':'';"
|
||||||
|
"document.getElementById('wifiDebug').textContent=st.debug||'';"
|
||||||
|
"const h=document.getElementById('artnetHint');"
|
||||||
|
"if(h){if(st.sta_up&&st.sta_ip&&st.sta_ip!=='0.0.0.0'){"
|
||||||
|
"h.textContent='QLC+/Art-Net: unicast to '+st.sta_ip+' (home Wi‑Fi drops Art-Net broadcast; SoftAP did not).';"
|
||||||
|
"}else if(st.ap_up){h.textContent='QLC+/Art-Net: join SoftAP and send to '+st.ap_ip+' (broadcast OK on SoftAP).';"
|
||||||
|
"}else{h.textContent='';}}"
|
||||||
|
"}"
|
||||||
|
"async function load(){const s=await fetch('/api/status');const st=await s.json();showSt(st);"
|
||||||
|
"const c=await fetch('/api/config');const cfg=await c.json();"
|
||||||
|
"mode.value=cfg.wifi_mode;sta_ssid.value=cfg.sta_ssid;ap_ssid.value=cfg.ap_ssid;"
|
||||||
|
"sta_pass.placeholder='(unchanged if left blank)';ap_pass.placeholder='(unchanged if left blank)';"
|
||||||
|
"universe_a.value=cfg.universe_a;universe_b.value=cfg.universe_b;"
|
||||||
|
"mdns_host.value=cfg.mdns_host||'artnetdmx';}"
|
||||||
|
"wifiForm.onsubmit=async e=>{e.preventDefault();"
|
||||||
|
"const body={wifi_mode:+mode.value,sta_ssid:sta_ssid.value,"
|
||||||
|
"ap_ssid:ap_ssid.value,universe_a:+universe_a.value,universe_b:+universe_b.value,"
|
||||||
|
"mdns_host:mdns_host.value.trim()};"
|
||||||
|
"if(sta_pass.value)body.sta_pass=sta_pass.value;if(ap_pass.value)body.ap_pass=ap_pass.value;"
|
||||||
|
"const r=await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});"
|
||||||
|
"document.getElementById('status').textContent=r.ok?'Saved — rebooting…':'Save failed';};"
|
||||||
|
"load();setInterval(async()=>{try{const s=await fetch('/api/status');showSt(await s.json());}catch(e){}},2000);";
|
||||||
|
|
||||||
|
static void http_conn_free(http_conn_t *c) {
|
||||||
|
if (!c) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
free(c->resp);
|
||||||
|
free(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
static err_t http_close(http_conn_t *c, struct tcp_pcb *tpcb, err_t err) {
|
||||||
|
if (tpcb) {
|
||||||
|
tcp_arg(tpcb, NULL);
|
||||||
|
tcp_recv(tpcb, NULL);
|
||||||
|
tcp_sent(tpcb, NULL);
|
||||||
|
tcp_err(tpcb, NULL);
|
||||||
|
tcp_poll(tpcb, NULL, 0);
|
||||||
|
tcp_close(tpcb);
|
||||||
|
}
|
||||||
|
http_conn_free(c);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
static err_t http_send_more(http_conn_t *c, struct tcp_pcb *tpcb) {
|
||||||
|
while (c->sent < c->resp_len) {
|
||||||
|
u16_t space = tcp_sndbuf(tpcb);
|
||||||
|
if (space == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
u16_t chunk = (u16_t)(c->resp_len - c->sent);
|
||||||
|
if (chunk > space) {
|
||||||
|
chunk = space;
|
||||||
|
}
|
||||||
|
if (chunk > 1024) {
|
||||||
|
chunk = 1024;
|
||||||
|
}
|
||||||
|
err_t e = tcp_write(tpcb, c->resp + c->sent, chunk, TCP_WRITE_FLAG_COPY);
|
||||||
|
if (e == ERR_MEM) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (e != ERR_OK) {
|
||||||
|
printf("[http] tcp_write err %d\n", (int)e);
|
||||||
|
return http_close(c, tpcb, e);
|
||||||
|
}
|
||||||
|
c->sent += chunk;
|
||||||
|
}
|
||||||
|
tcp_output(tpcb);
|
||||||
|
if (c->sent >= c->resp_len && c->close_after) {
|
||||||
|
return http_close(c, tpcb, ERR_OK);
|
||||||
|
}
|
||||||
|
return ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static err_t http_poll(void *arg, struct tcp_pcb *tpcb) {
|
||||||
|
http_conn_t *c = (http_conn_t *)arg;
|
||||||
|
if (!c) {
|
||||||
|
return ERR_OK;
|
||||||
|
}
|
||||||
|
if (c->resp && c->sent < c->resp_len) {
|
||||||
|
return http_send_more(c, tpcb);
|
||||||
|
}
|
||||||
|
return ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static err_t http_sent(void *arg, struct tcp_pcb *tpcb, u16_t len) {
|
||||||
|
(void)len;
|
||||||
|
return http_send_more((http_conn_t *)arg, tpcb);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *build_response(const char *status, const char *ctype, const char *body, int body_len, int *out_len) {
|
||||||
|
int hdr_max = 160;
|
||||||
|
char *buf = malloc((size_t)hdr_max + (size_t)body_len + 1);
|
||||||
|
if (!buf) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
int n = snprintf(buf, (size_t)hdr_max,
|
||||||
|
"HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n",
|
||||||
|
status, ctype, body_len);
|
||||||
|
if (n < 0 || n >= hdr_max) {
|
||||||
|
free(buf);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
memcpy(buf + n, body, (size_t)body_len);
|
||||||
|
*out_len = n + body_len;
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *json_escape(const char *s, char *out, size_t out_sz) {
|
||||||
|
size_t j = 0;
|
||||||
|
out[j++] = '"';
|
||||||
|
for (size_t i = 0; s[i] && j + 2 < out_sz; i++) {
|
||||||
|
char c = s[i];
|
||||||
|
if (c == '"' || c == '\\') {
|
||||||
|
if (j + 3 >= out_sz) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out[j++] = '\\';
|
||||||
|
out[j++] = c;
|
||||||
|
} else if ((unsigned char)c < 0x20) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
out[j++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (j + 1 < out_sz) {
|
||||||
|
out[j++] = '"';
|
||||||
|
}
|
||||||
|
out[j] = 0;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *find_body(const char *req) {
|
||||||
|
const char *p = strstr(req, "\r\n\r\n");
|
||||||
|
return p ? p + 4 : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int query_int(const char *req, const char *key, int def) {
|
||||||
|
char pat[32];
|
||||||
|
snprintf(pat, sizeof(pat), "%s=", key);
|
||||||
|
const char *q = strchr(req, '?');
|
||||||
|
if (!q) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
const char *p = strstr(q, pat);
|
||||||
|
if (!p) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
return atoi(p + strlen(pat));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool json_get_str(const char *json, const char *key, char *out, size_t out_sz) {
|
||||||
|
char pat[48];
|
||||||
|
snprintf(pat, sizeof(pat), "\"%s\"", key);
|
||||||
|
const char *p = strstr(json, pat);
|
||||||
|
if (!p) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
p = strchr(p + strlen(pat), ':');
|
||||||
|
if (!p) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
while (*p && isspace((unsigned char)*p)) {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (*p != '"') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
size_t i = 0;
|
||||||
|
while (*p && *p != '"' && i + 1 < out_sz) {
|
||||||
|
if (*p == '\\' && p[1]) {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
out[i++] = *p++;
|
||||||
|
}
|
||||||
|
out[i] = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool json_get_int(const char *json, const char *key, int *out) {
|
||||||
|
char pat[48];
|
||||||
|
snprintf(pat, sizeof(pat), "\"%s\"", key);
|
||||||
|
const char *p = strstr(json, pat);
|
||||||
|
if (!p) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
p = strchr(p + strlen(pat), ':');
|
||||||
|
if (!p) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*out = atoi(p + 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *handle_api_channels_get(const char *req, int *resp_len) {
|
||||||
|
int u = query_int(req, "u", 0);
|
||||||
|
if (u < 0 || u >= BOARD_NUM_UNIVERSES) {
|
||||||
|
u = 0;
|
||||||
|
}
|
||||||
|
uint8_t *ch = channels_get((unsigned)u);
|
||||||
|
char *body = malloc(4096);
|
||||||
|
if (!body || !ch) {
|
||||||
|
free(body);
|
||||||
|
return build_response("500 Internal Server Error", "text/plain", "err", 3, resp_len);
|
||||||
|
}
|
||||||
|
int n = snprintf(body, 4096, "{\"u\":%d,\"values\":[", u);
|
||||||
|
for (int i = 0; i < BOARD_DMX_CHANNELS; i++) {
|
||||||
|
n += snprintf(body + n, (size_t)(4096 - n), "%s%u", i ? "," : "", ch[i]);
|
||||||
|
if (n >= 4000) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n += snprintf(body + n, (size_t)(4096 - n), "]}");
|
||||||
|
char *resp = build_response("200 OK", "application/json", body, n, resp_len);
|
||||||
|
free(body);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *handle_api_status(int *resp_len) {
|
||||||
|
char body[640];
|
||||||
|
char dbg_esc[220];
|
||||||
|
const char *mode = wifi_mode_name(wifi_mgr_active_mode());
|
||||||
|
uint32_t age_a = channels_ms_since_artnet(0);
|
||||||
|
uint32_t age_b = channels_ms_since_artnet(1);
|
||||||
|
json_escape(wifi_mgr_debug(), dbg_esc, sizeof(dbg_esc));
|
||||||
|
int n = snprintf(body, sizeof(body),
|
||||||
|
"{\"mode\":\"%s\",\"ip\":\"%s\",\"sta_ip\":\"%s\",\"ap_ip\":\"%s\","
|
||||||
|
"\"sta_up\":%s,\"ap_up\":%s,\"mdns\":\"%s\","
|
||||||
|
"\"artnet_packets\":%lu,\"age_a\":%lu,\"age_b\":%lu,\"uni_a\":%u,\"uni_b\":%u,\"debug\":%s}",
|
||||||
|
mode, wifi_mgr_ip_str(),
|
||||||
|
wifi_mgr_sta_ip_str(), wifi_mgr_ap_ip_str(),
|
||||||
|
wifi_mgr_sta_up() ? "true" : "false",
|
||||||
|
wifi_mgr_ap_up() ? "true" : "false",
|
||||||
|
wifi_mgr_mdns_name(),
|
||||||
|
(unsigned long)channels_artnet_packets(),
|
||||||
|
(unsigned long)age_a, (unsigned long)age_b,
|
||||||
|
config_get()->universe_a, config_get()->universe_b,
|
||||||
|
dbg_esc);
|
||||||
|
return build_response("200 OK", "application/json", body, n, resp_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *handle_api_config_get(int *resp_len) {
|
||||||
|
const config_t *c = config_get();
|
||||||
|
char ss[80], ap[80], md[80];
|
||||||
|
char body[640];
|
||||||
|
int n = snprintf(body, sizeof(body),
|
||||||
|
"{\"wifi_mode\":%d,\"sta_ssid\":%s,\"ap_ssid\":%s,\"universe_a\":%u,\"universe_b\":%u,\"mdns_host\":%s}",
|
||||||
|
(int)c->wifi_mode,
|
||||||
|
json_escape(c->sta_ssid, ss, sizeof(ss)),
|
||||||
|
json_escape(c->ap_ssid, ap, sizeof(ap)),
|
||||||
|
c->universe_a, c->universe_b,
|
||||||
|
json_escape(c->mdns_host[0] ? c->mdns_host : BOARD_HOSTNAME, md, sizeof(md)));
|
||||||
|
return build_response("200 OK", "application/json", body, n, resp_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parse_channel_values(const char *json, unsigned u) {
|
||||||
|
const char *p = strstr(json, "\"values\"");
|
||||||
|
if (!p) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
p = strchr(p, '[');
|
||||||
|
if (!p) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
uint8_t vals[BOARD_DMX_CHANNELS];
|
||||||
|
memset(vals, 0, sizeof(vals));
|
||||||
|
int idx = 0;
|
||||||
|
while (*p && *p != ']' && idx < BOARD_DMX_CHANNELS) {
|
||||||
|
while (*p && (isspace((unsigned char)*p) || *p == ',')) {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (*p == ']' || !*p) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
vals[idx++] = (uint8_t)atoi(p);
|
||||||
|
while (*p && *p != ',' && *p != ']') {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channels_set_range(u, vals, (size_t)idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *handle_api_channels_post(const char *req, int *resp_len) {
|
||||||
|
const char *body = find_body(req);
|
||||||
|
if (!body) {
|
||||||
|
return build_response("400 Bad Request", "text/plain", "no body", 7, resp_len);
|
||||||
|
}
|
||||||
|
int u = 0;
|
||||||
|
json_get_int(body, "u", &u);
|
||||||
|
if (u < 0 || u >= BOARD_NUM_UNIVERSES) {
|
||||||
|
u = 0;
|
||||||
|
}
|
||||||
|
if (strstr(body, "\"clear\":true") || strstr(body, "\"clear\": true")) {
|
||||||
|
channels_clear((unsigned)u);
|
||||||
|
} else if (strstr(body, "\"values\"")) {
|
||||||
|
parse_channel_values(body, (unsigned)u);
|
||||||
|
} else {
|
||||||
|
int ch = 0, val = 0;
|
||||||
|
json_get_int(body, "ch", &ch);
|
||||||
|
json_get_int(body, "val", &val);
|
||||||
|
if (ch >= 0 && ch < BOARD_DMX_CHANNELS) {
|
||||||
|
channels_set((unsigned)u, (unsigned)ch, (uint8_t)val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return build_response("200 OK", "application/json", "{\"ok\":true}", 11, resp_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *handle_api_config_post(const char *req, int *resp_len) {
|
||||||
|
const char *body = find_body(req);
|
||||||
|
if (!body) {
|
||||||
|
return build_response("400 Bad Request", "text/plain", "no body", 7, resp_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
config_t cfg = *config_get();
|
||||||
|
int mode = (int)cfg.wifi_mode;
|
||||||
|
int ua = cfg.universe_a, ub = cfg.universe_b;
|
||||||
|
json_get_int(body, "wifi_mode", &mode);
|
||||||
|
json_get_int(body, "universe_a", &ua);
|
||||||
|
json_get_int(body, "universe_b", &ub);
|
||||||
|
if (mode != (int)WIFI_MODE_STA) {
|
||||||
|
mode = (int)WIFI_MODE_AP;
|
||||||
|
}
|
||||||
|
cfg.wifi_mode = (wifi_mode_t)mode;
|
||||||
|
cfg.universe_a = (uint16_t)ua;
|
||||||
|
cfg.universe_b = (uint16_t)ub;
|
||||||
|
json_get_str(body, "sta_ssid", cfg.sta_ssid, sizeof(cfg.sta_ssid));
|
||||||
|
json_get_str(body, "ap_ssid", cfg.ap_ssid, sizeof(cfg.ap_ssid));
|
||||||
|
{
|
||||||
|
char host[CONFIG_HOSTNAME_MAX];
|
||||||
|
if (json_get_str(body, "mdns_host", host, sizeof(host))) {
|
||||||
|
config_sanitize_hostname(host, cfg.mdns_host, sizeof(cfg.mdns_host));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Password fields are blank in the UI on load — only overwrite if a new value was sent. */
|
||||||
|
{
|
||||||
|
char pass[CONFIG_PASS_MAX];
|
||||||
|
if (json_get_str(body, "sta_pass", pass, sizeof(pass)) && pass[0]) {
|
||||||
|
strncpy(cfg.sta_pass, pass, sizeof(cfg.sta_pass) - 1);
|
||||||
|
cfg.sta_pass[sizeof(cfg.sta_pass) - 1] = 0;
|
||||||
|
}
|
||||||
|
if (json_get_str(body, "ap_pass", pass, sizeof(pass)) && pass[0]) {
|
||||||
|
strncpy(cfg.ap_pass, pass, sizeof(cfg.ap_pass) - 1);
|
||||||
|
cfg.ap_pass[sizeof(cfg.ap_pass) - 1] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Config save: mode=%s sta_ssid='%s' sta_pass_len=%u ap_ssid='%s' mdns='%s'\n",
|
||||||
|
wifi_mode_name(cfg.wifi_mode),
|
||||||
|
cfg.sta_ssid, (unsigned)strlen(cfg.sta_pass), cfg.ap_ssid, cfg.mdns_host);
|
||||||
|
|
||||||
|
config_save(&cfg);
|
||||||
|
char *resp = build_response("200 OK", "application/json", "{\"ok\":true,\"reboot\":true}", 25, resp_len);
|
||||||
|
/* Reboot shortly after response is queued — caller sets flag via returning special; do it after delay in main isn't available.
|
||||||
|
Schedule reboot from here with a short sleep in a deferred way: use alarm. */
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int64_t reboot_cb(alarm_id_t id, void *user) {
|
||||||
|
(void)id;
|
||||||
|
(void)user;
|
||||||
|
watchdog_reboot(0, 0, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *dispatch(const char *req, int *resp_len, bool *reboot) {
|
||||||
|
*reboot = false;
|
||||||
|
if (strncmp(req, "GET /api/channels", 17) == 0) {
|
||||||
|
return handle_api_channels_get(req, resp_len);
|
||||||
|
}
|
||||||
|
if (strncmp(req, "GET /api/status", 15) == 0) {
|
||||||
|
return handle_api_status(resp_len);
|
||||||
|
}
|
||||||
|
if (strncmp(req, "GET /api/config", 15) == 0) {
|
||||||
|
return handle_api_config_get(resp_len);
|
||||||
|
}
|
||||||
|
if (strncmp(req, "POST /api/channels", 18) == 0) {
|
||||||
|
return handle_api_channels_post(req, resp_len);
|
||||||
|
}
|
||||||
|
if (strncmp(req, "POST /api/config", 16) == 0) {
|
||||||
|
*reboot = true;
|
||||||
|
return handle_api_config_post(req, resp_len);
|
||||||
|
}
|
||||||
|
if (strncmp(req, "GET /style.css", 14) == 0) {
|
||||||
|
return build_response("200 OK", "text/css", PAGE_CSS, (int)strlen(PAGE_CSS), resp_len);
|
||||||
|
}
|
||||||
|
if (strncmp(req, "GET /app.js", 11) == 0) {
|
||||||
|
return build_response("200 OK", "application/javascript", PAGE_APP_JS, (int)strlen(PAGE_APP_JS), resp_len);
|
||||||
|
}
|
||||||
|
if (strncmp(req, "GET /wifi.js", 12) == 0) {
|
||||||
|
return build_response("200 OK", "application/javascript", PAGE_WIFI_JS, (int)strlen(PAGE_WIFI_JS), resp_len);
|
||||||
|
}
|
||||||
|
if (strncmp(req, "GET /wifi", 9) == 0) {
|
||||||
|
return build_response("200 OK", "text/html", PAGE_WIFI, (int)strlen(PAGE_WIFI), resp_len);
|
||||||
|
}
|
||||||
|
/* Root: "GET / ", "GET /?", "GET /\r", "GET /HTTP" edge cases */
|
||||||
|
if (strncmp(req, "GET /", 5) == 0) {
|
||||||
|
char n = req[5];
|
||||||
|
if (n == ' ' || n == '?' || n == '\r' || n == '\n') {
|
||||||
|
return build_response("200 OK", "text/html", PAGE_INDEX, (int)strlen(PAGE_INDEX), resp_len);
|
||||||
|
}
|
||||||
|
if (strncmp(req, "GET /index.html", 15) == 0) {
|
||||||
|
return build_response("200 OK", "text/html", PAGE_INDEX, (int)strlen(PAGE_INDEX), resp_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (strncmp(req, "GET /favicon.ico", 16) == 0) {
|
||||||
|
return build_response("204 No Content", "text/plain", "", 0, resp_len);
|
||||||
|
}
|
||||||
|
printf("[http] 404: %.40s\n", req);
|
||||||
|
return build_response("404 Not Found", "text/plain", "not found", 9, resp_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static err_t http_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) {
|
||||||
|
http_conn_t *c = (http_conn_t *)arg;
|
||||||
|
if (!p) {
|
||||||
|
return http_close(c, tpcb, ERR_OK);
|
||||||
|
}
|
||||||
|
if (err != ERR_OK) {
|
||||||
|
pbuf_free(p);
|
||||||
|
return http_close(c, tpcb, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p->tot_len + c->req_len < REQ_BUF) {
|
||||||
|
pbuf_copy_partial(p, c->req + c->req_len, p->tot_len, 0);
|
||||||
|
c->req_len += p->tot_len;
|
||||||
|
c->req[c->req_len] = 0;
|
||||||
|
}
|
||||||
|
tcp_recved(tpcb, p->tot_len);
|
||||||
|
pbuf_free(p);
|
||||||
|
|
||||||
|
if (!strstr(c->req, "\r\n\r\n")) {
|
||||||
|
return ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For POST, wait until Content-Length bytes arrived */
|
||||||
|
if (strncmp(c->req, "POST", 4) == 0) {
|
||||||
|
const char *cl = strstr(c->req, "Content-Length:");
|
||||||
|
if (cl) {
|
||||||
|
int need = atoi(cl + 15);
|
||||||
|
const char *body = find_body(c->req);
|
||||||
|
if (!body || (int)strlen(body) < need) {
|
||||||
|
return ERR_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reboot = false;
|
||||||
|
printf("[http] %.48s\n", c->req);
|
||||||
|
c->resp = dispatch(c->req, &c->resp_len, &reboot);
|
||||||
|
c->sent = 0;
|
||||||
|
c->close_after = true;
|
||||||
|
if (!c->resp) {
|
||||||
|
return http_close(c, tpcb, ERR_MEM);
|
||||||
|
}
|
||||||
|
if (reboot) {
|
||||||
|
add_alarm_in_ms(500, reboot_cb, NULL, true);
|
||||||
|
}
|
||||||
|
return http_send_more(c, tpcb);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void http_err(void *arg, err_t err) {
|
||||||
|
(void)err;
|
||||||
|
http_conn_free((http_conn_t *)arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static err_t http_accept(void *arg, struct tcp_pcb *newpcb, err_t err) {
|
||||||
|
(void)arg;
|
||||||
|
if (err != ERR_OK || !newpcb) {
|
||||||
|
return ERR_VAL;
|
||||||
|
}
|
||||||
|
http_conn_t *c = calloc(1, sizeof(*c));
|
||||||
|
if (!c) {
|
||||||
|
return ERR_MEM;
|
||||||
|
}
|
||||||
|
c->pcb = newpcb;
|
||||||
|
tcp_arg(newpcb, c);
|
||||||
|
tcp_recv(newpcb, http_recv);
|
||||||
|
tcp_sent(newpcb, http_sent);
|
||||||
|
tcp_err(newpcb, http_err);
|
||||||
|
tcp_poll(newpcb, http_poll, 2);
|
||||||
|
printf("[http] client accept\n");
|
||||||
|
return ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool http_server_start(void) {
|
||||||
|
cyw43_arch_lwip_begin();
|
||||||
|
g_server = tcp_new_ip_type(IPADDR_TYPE_ANY);
|
||||||
|
if (!g_server) {
|
||||||
|
cyw43_arch_lwip_end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tcp_bind(g_server, IP_ANY_TYPE, HTTP_PORT) != ERR_OK) {
|
||||||
|
tcp_close(g_server);
|
||||||
|
g_server = NULL;
|
||||||
|
cyw43_arch_lwip_end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_server = tcp_listen_with_backlog(g_server, 2);
|
||||||
|
tcp_accept(g_server, http_accept);
|
||||||
|
cyw43_arch_lwip_end();
|
||||||
|
printf("HTTP server on port %d\n", HTTP_PORT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "pico/stdlib.h"
|
||||||
|
#include "pico/stdio_usb.h"
|
||||||
|
#include "pico/cyw43_arch.h"
|
||||||
|
#include "hardware/pio.h"
|
||||||
|
|
||||||
|
#include "board.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "channels.h"
|
||||||
|
#include "dmx_out.h"
|
||||||
|
#include "artnet.h"
|
||||||
|
#include "wifi_mgr.h"
|
||||||
|
#include "http_server.h"
|
||||||
|
|
||||||
|
static dmx_out_t g_dmx[BOARD_NUM_UNIVERSES];
|
||||||
|
static uint8_t g_frames[BOARD_NUM_UNIVERSES][BOARD_DMX_FRAME_LEN];
|
||||||
|
|
||||||
|
static bool dmx_timer_cb(repeating_timer_t *rt) {
|
||||||
|
(void)rt;
|
||||||
|
for (unsigned u = 0; u < BOARD_NUM_UNIVERSES; u++) {
|
||||||
|
if (dmx_out_busy(&g_dmx[u])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
channels_build_frame(u, g_frames[u]);
|
||||||
|
dmx_out_write(&g_dmx[u], g_frames[u], BOARD_DMX_FRAME_LEN);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
stdio_init_all();
|
||||||
|
/* Give USB CDC a moment so early boot logs are visible after flash/reboot. */
|
||||||
|
for (int i = 0; i < 40 && !stdio_usb_connected(); i++) {
|
||||||
|
sleep_ms(50);
|
||||||
|
}
|
||||||
|
sleep_ms(200);
|
||||||
|
printf("\nArtNetDMX starting\n");
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
config_init();
|
||||||
|
channels_init();
|
||||||
|
|
||||||
|
if (!dmx_out_init(&g_dmx[0], BOARD_DMX_A_PIN, pio0)) {
|
||||||
|
printf("DMX A init failed\n");
|
||||||
|
}
|
||||||
|
if (!dmx_out_init(&g_dmx[1], BOARD_DMX_B_PIN, pio1)) {
|
||||||
|
printf("DMX B init failed\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wifi_mgr_start(config_get())) {
|
||||||
|
printf("WiFi start failed\n");
|
||||||
|
while (true) {
|
||||||
|
tight_loop_contents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!http_server_start()) {
|
||||||
|
printf("HTTP start failed\n");
|
||||||
|
}
|
||||||
|
if (!artnet_start()) {
|
||||||
|
printf("Art-Net start failed\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static repeating_timer_t dmx_timer;
|
||||||
|
if (!add_repeating_timer_ms(-BOARD_DMX_PERIOD_MS, dmx_timer_cb, NULL, &dmx_timer)) {
|
||||||
|
printf("DMX timer failed\n");
|
||||||
|
} else {
|
||||||
|
printf("DMX output at %d Hz\n", BOARD_DMX_HZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Ready — mode=%s primary=%s mDNS http://%s/\n",
|
||||||
|
wifi_mode_name(wifi_mgr_active_mode()),
|
||||||
|
wifi_mgr_ip_str(),
|
||||||
|
wifi_mgr_mdns_name());
|
||||||
|
if (wifi_mgr_ap_up()) {
|
||||||
|
printf("SoftAP http://%s/ (ssid ArtNetDMX)\n", wifi_mgr_ap_ip_str());
|
||||||
|
} else {
|
||||||
|
printf("Joining STA (SoftAP starts only if join fails)...\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
wifi_mgr_poll();
|
||||||
|
artnet_poll();
|
||||||
|
tight_loop_contents();
|
||||||
|
sleep_ms(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
+569
@@ -0,0 +1,569 @@
|
|||||||
|
#include "artnet.h"
|
||||||
|
#include "wifi_mgr.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "pico/cyw43_arch.h"
|
||||||
|
#include "pico/stdlib.h"
|
||||||
|
#include "pico/error.h"
|
||||||
|
#include "lwip/netif.h"
|
||||||
|
#include "lwip/dhcp.h"
|
||||||
|
#include "lwip/apps/mdns.h"
|
||||||
|
#include "dhcpserver.h"
|
||||||
|
|
||||||
|
#include "board.h"
|
||||||
|
|
||||||
|
#ifndef BOARD_WIFI_COUNTRY
|
||||||
|
#define BOARD_WIFI_COUNTRY CYW43_COUNTRY_NEW_ZEALAND
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static wifi_mode_t g_mode = WIFI_MODE_AP;
|
||||||
|
static char g_ip[16] = "0.0.0.0";
|
||||||
|
static char g_sta_ip[16] = "0.0.0.0";
|
||||||
|
static char g_ap_ip[16] = "0.0.0.0";
|
||||||
|
static bool g_sta_up;
|
||||||
|
static bool g_ap_up;
|
||||||
|
static bool g_up;
|
||||||
|
static dhcp_server_t g_dhcp;
|
||||||
|
static bool g_dhcp_active;
|
||||||
|
static bool g_mdns_ok;
|
||||||
|
|
||||||
|
static char g_debug[192] = "boot";
|
||||||
|
static bool g_scan_found_target;
|
||||||
|
static int g_scan_rssi;
|
||||||
|
static int g_scan_chan;
|
||||||
|
static uint8_t g_scan_auth;
|
||||||
|
static char g_target_ssid[CONFIG_SSID_MAX];
|
||||||
|
|
||||||
|
static config_t g_cfg;
|
||||||
|
static bool g_sta_pending;
|
||||||
|
static absolute_time_t g_sta_deadline;
|
||||||
|
|
||||||
|
static void set_debug(const char *msg) {
|
||||||
|
snprintf(g_debug, sizeof(g_debug), "%s", msg);
|
||||||
|
printf("[wifi] %s\n", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *wifi_mgr_debug(void) {
|
||||||
|
return g_debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifi_disable_powersave(void) {
|
||||||
|
/* Keep radio fully awake — important for Art-Net / DMX timing reliability. */
|
||||||
|
cyw43_wifi_pm(&cyw43_state, CYW43_NONE_PM);
|
||||||
|
printf("[wifi] powersave OFF (CYW43_NONE_PM)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void refresh_sta_ip(void) {
|
||||||
|
snprintf(g_sta_ip, sizeof(g_sta_ip), "%s",
|
||||||
|
ip4addr_ntoa(netif_ip4_addr(&cyw43_state.netif[CYW43_ITF_STA])));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void update_primary_ip(void) {
|
||||||
|
if (g_sta_up) {
|
||||||
|
refresh_sta_ip();
|
||||||
|
snprintf(g_ip, sizeof(g_ip), "%s", g_sta_ip);
|
||||||
|
} else if (g_ap_up) {
|
||||||
|
snprintf(g_ip, sizeof(g_ip), "%s", g_ap_ip);
|
||||||
|
} else {
|
||||||
|
snprintf(g_ip, sizeof(g_ip), "0.0.0.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *link_name(int status) {
|
||||||
|
switch (status) {
|
||||||
|
case CYW43_LINK_DOWN: return "DOWN";
|
||||||
|
case CYW43_LINK_JOIN: return "JOIN";
|
||||||
|
case CYW43_LINK_NOIP: return "NOIP";
|
||||||
|
case CYW43_LINK_UP: return "UP";
|
||||||
|
case CYW43_LINK_FAIL: return "FAIL";
|
||||||
|
case CYW43_LINK_NONET: return "NONET";
|
||||||
|
case CYW43_LINK_BADAUTH: return "BADAUTH";
|
||||||
|
default: return "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *err_name(int rc) {
|
||||||
|
switch (rc) {
|
||||||
|
case PICO_OK: return "OK";
|
||||||
|
case PICO_ERROR_TIMEOUT: return "TIMEOUT";
|
||||||
|
case PICO_ERROR_BADAUTH: return "BADAUTH";
|
||||||
|
case PICO_ERROR_CONNECT_FAILED: return "CONNECT_FAILED";
|
||||||
|
case PICO_ERROR_GENERIC: return "GENERIC";
|
||||||
|
default: return "ERR";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dump_ssid_debug(const char *label, const char *ssid) {
|
||||||
|
size_t n = strlen(ssid);
|
||||||
|
printf("[wifi] %s ssid='%s' len=%u hex:", label, ssid, (unsigned)n);
|
||||||
|
for (size_t i = 0; i < n && i < 32; i++) {
|
||||||
|
printf(" %02x", (unsigned char)ssid[i]);
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dump_pass_debug(const char *pass) {
|
||||||
|
size_t n = pass ? strlen(pass) : 0;
|
||||||
|
if (n == 0) {
|
||||||
|
printf("[wifi] password: (empty / open)\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printf("[wifi] password: len=%u first='%c' last='%c'\n",
|
||||||
|
(unsigned)n, pass[0], pass[n - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int scan_result_cb(void *env, const cyw43_ev_scan_result_t *result) {
|
||||||
|
(void)env;
|
||||||
|
if (!result || result->ssid_len == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char ssid[33];
|
||||||
|
size_t n = result->ssid_len < 32 ? result->ssid_len : 32;
|
||||||
|
memcpy(ssid, result->ssid, n);
|
||||||
|
ssid[n] = 0;
|
||||||
|
|
||||||
|
bool match = g_target_ssid[0] && strcmp(ssid, g_target_ssid) == 0;
|
||||||
|
printf("[wifi] scan %sssid='%-32s' rssi=%4d chan=%3u auth=0x%02x mac=%02x:%02x:%02x:%02x:%02x:%02x\n",
|
||||||
|
match ? "* " : " ",
|
||||||
|
ssid, result->rssi, result->channel, result->auth_mode,
|
||||||
|
result->bssid[0], result->bssid[1], result->bssid[2],
|
||||||
|
result->bssid[3], result->bssid[4], result->bssid[5]);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
g_scan_found_target = true;
|
||||||
|
g_scan_rssi = result->rssi;
|
||||||
|
g_scan_chan = result->channel;
|
||||||
|
g_scan_auth = result->auth_mode;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifi_scan_for_target(const char *ssid) {
|
||||||
|
memset(g_target_ssid, 0, sizeof(g_target_ssid));
|
||||||
|
if (ssid) {
|
||||||
|
strncpy(g_target_ssid, ssid, sizeof(g_target_ssid) - 1);
|
||||||
|
}
|
||||||
|
g_scan_found_target = false;
|
||||||
|
|
||||||
|
set_debug("scanning 2.4GHz networks...");
|
||||||
|
cyw43_wifi_scan_options_t opts;
|
||||||
|
memset(&opts, 0, sizeof(opts));
|
||||||
|
int err = cyw43_wifi_scan(&cyw43_state, &opts, NULL, scan_result_cb);
|
||||||
|
if (err) {
|
||||||
|
printf("[wifi] scan start failed: %d\n", err);
|
||||||
|
set_debug("scan start failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
absolute_time_t deadline = make_timeout_time_ms(10000);
|
||||||
|
while (cyw43_wifi_scan_active(&cyw43_state)) {
|
||||||
|
if (time_reached(deadline)) {
|
||||||
|
printf("[wifi] scan timeout\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sleep_ms(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_scan_found_target) {
|
||||||
|
char buf[160];
|
||||||
|
snprintf(buf, sizeof(buf), "SSID found rssi=%d chan=%d auth=0x%02x",
|
||||||
|
g_scan_rssi, g_scan_chan, g_scan_auth);
|
||||||
|
set_debug(buf);
|
||||||
|
} else {
|
||||||
|
set_debug("SSID NOT SEEN in scan (wrong name, 5GHz-only, or out of range)");
|
||||||
|
printf("[wifi] *** target SSID '%s' not seen — Pico W is 2.4GHz only ***\n",
|
||||||
|
g_target_ssid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *mdns_hostname(void) {
|
||||||
|
const config_t *c = config_get();
|
||||||
|
if (c && c->mdns_host[0]) {
|
||||||
|
return c->mdns_host;
|
||||||
|
}
|
||||||
|
return BOARD_HOSTNAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mdns_txt(struct mdns_service *service, void *txt_userdata) {
|
||||||
|
(void)txt_userdata;
|
||||||
|
mdns_resp_add_service_txtitem(service, "path=/", 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mdns_add_netif(struct netif *netif, const char *label) {
|
||||||
|
if (!netif) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char *host = mdns_hostname();
|
||||||
|
err_t e = mdns_resp_add_netif(netif, host);
|
||||||
|
if (e != ERR_OK) {
|
||||||
|
printf("[mdns] add_netif %s failed %d\n", label, (int)e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s8_t slot = mdns_resp_add_service(netif, host, "_http", DNSSD_PROTO_TCP, 80,
|
||||||
|
mdns_txt, NULL);
|
||||||
|
if (slot < 0) {
|
||||||
|
printf("[mdns] add_service %s failed %d\n", label, (int)slot);
|
||||||
|
} else {
|
||||||
|
printf("[mdns] %s -> %s.local (_http._tcp)\n", label, host);
|
||||||
|
g_mdns_ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mdns_start(void) {
|
||||||
|
cyw43_arch_lwip_begin();
|
||||||
|
mdns_resp_init();
|
||||||
|
if (g_sta_up) {
|
||||||
|
mdns_add_netif(&cyw43_state.netif[CYW43_ITF_STA], "STA");
|
||||||
|
}
|
||||||
|
if (g_ap_up) {
|
||||||
|
mdns_add_netif(&cyw43_state.netif[CYW43_ITF_AP], "AP");
|
||||||
|
}
|
||||||
|
cyw43_arch_lwip_end();
|
||||||
|
if (g_mdns_ok) {
|
||||||
|
printf("[mdns] browse http://%s.local/\n", mdns_hostname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remember SoftAP credentials so we can re-assert after STA/scan. */
|
||||||
|
static char g_ap_ssid_active[CONFIG_SSID_MAX];
|
||||||
|
static char g_ap_pass_active[CONFIG_PASS_MAX];
|
||||||
|
|
||||||
|
static void resolve_ap_credentials(const config_t *cfg, const char **ssid, const char **pass) {
|
||||||
|
*ssid = (cfg->ap_ssid[0]) ? cfg->ap_ssid : BOARD_AP_SSID_DEFAULT;
|
||||||
|
*pass = (cfg->ap_pass[0]) ? cfg->ap_pass : BOARD_AP_PASS_DEFAULT;
|
||||||
|
/* WPA2-PSK requires 8–63 character passphrase; fall back to defaults. */
|
||||||
|
size_t plen = strlen(*pass);
|
||||||
|
if (plen < 8 || plen > 63) {
|
||||||
|
printf("[wifi] AP pass len=%u invalid for WPA2 — using defaults\n", (unsigned)plen);
|
||||||
|
*ssid = BOARD_AP_SSID_DEFAULT;
|
||||||
|
*pass = BOARD_AP_PASS_DEFAULT;
|
||||||
|
}
|
||||||
|
if (!(*ssid)[0]) {
|
||||||
|
*ssid = BOARD_AP_SSID_DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool start_ap_iface(const config_t *cfg) {
|
||||||
|
const char *ssid;
|
||||||
|
const char *pass;
|
||||||
|
resolve_ap_credentials(cfg, &ssid, &pass);
|
||||||
|
|
||||||
|
strncpy(g_ap_ssid_active, ssid, sizeof(g_ap_ssid_active) - 1);
|
||||||
|
g_ap_ssid_active[sizeof(g_ap_ssid_active) - 1] = 0;
|
||||||
|
strncpy(g_ap_pass_active, pass, sizeof(g_ap_pass_active) - 1);
|
||||||
|
g_ap_pass_active[sizeof(g_ap_pass_active) - 1] = 0;
|
||||||
|
|
||||||
|
dump_ssid_debug("AP", g_ap_ssid_active);
|
||||||
|
dump_pass_debug(g_ap_pass_active);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SoftAP must come up before STA. On CYW43, later STA join/scan can stop
|
||||||
|
* beaconing — callers re-assert via this same path after STA work.
|
||||||
|
*/
|
||||||
|
cyw43_arch_enable_ap_mode(g_ap_ssid_active, g_ap_pass_active, CYW43_AUTH_WPA2_AES_PSK);
|
||||||
|
wifi_disable_powersave();
|
||||||
|
|
||||||
|
/* Let SoftAP beacon before we touch STA / scan. */
|
||||||
|
sleep_ms(300);
|
||||||
|
|
||||||
|
ip4_addr_t gw, mask;
|
||||||
|
IP4_ADDR(&gw, 192, 168, 4, 1);
|
||||||
|
IP4_ADDR(&mask, 255, 255, 255, 0);
|
||||||
|
if (!g_dhcp_active) {
|
||||||
|
dhcp_server_init(&g_dhcp, (ip_addr_t *)&gw, (ip_addr_t *)&mask);
|
||||||
|
g_dhcp_active = true;
|
||||||
|
printf("[wifi] DHCP server on 192.168.4.0/24 (gw 192.168.4.1)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
g_ap_up = true;
|
||||||
|
snprintf(g_ap_ip, sizeof(g_ap_ip), "%s",
|
||||||
|
ip4addr_ntoa(netif_ip4_addr(&cyw43_state.netif[CYW43_ITF_AP])));
|
||||||
|
if (strcmp(g_ap_ip, "0.0.0.0") == 0) {
|
||||||
|
snprintf(g_ap_ip, sizeof(g_ap_ip), "192.168.4.1");
|
||||||
|
}
|
||||||
|
printf("[wifi] AP '%s' up at %s (netif)\n", g_ap_ssid_active, g_ap_ip);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void softap_pause_for_sta(void) {
|
||||||
|
/* SoftAP DHCP binds IP_ANY:67 and can steal STA discovers / UDP PCBs. */
|
||||||
|
if (g_dhcp_active) {
|
||||||
|
printf("[wifi] stopping SoftAP DHCP server for STA join\n");
|
||||||
|
dhcp_server_deinit(&g_dhcp);
|
||||||
|
g_dhcp_active = false;
|
||||||
|
}
|
||||||
|
if (!g_ap_up) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printf("[wifi] pausing SoftAP so STA can get DHCP\n");
|
||||||
|
cyw43_arch_disable_ap_mode();
|
||||||
|
g_ap_up = false;
|
||||||
|
sleep_ms(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int connect_with_status(const char *ssid, const char *pass, uint32_t auth,
|
||||||
|
const char *label, uint32_t timeout_ms) {
|
||||||
|
printf("[wifi] join %s auth=0x%08lx timeout=%lums\n",
|
||||||
|
label, (unsigned long)auth, (unsigned long)timeout_ms);
|
||||||
|
|
||||||
|
int err = cyw43_arch_wifi_connect_async(ssid, pass, auth);
|
||||||
|
if (err) {
|
||||||
|
printf("[wifi] join async failed: %d (%s)\n", err, err_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
absolute_time_t until = make_timeout_time_ms(timeout_ms);
|
||||||
|
int status = 999;
|
||||||
|
absolute_time_t last_print = get_absolute_time();
|
||||||
|
absolute_time_t last_dhcp = get_absolute_time();
|
||||||
|
|
||||||
|
while (!time_reached(until)) {
|
||||||
|
int st = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA);
|
||||||
|
if (st != status || absolute_time_diff_us(last_print, get_absolute_time()) > 2000000) {
|
||||||
|
status = st;
|
||||||
|
last_print = get_absolute_time();
|
||||||
|
printf("[wifi] link=%s (%d)\n", link_name(st), st);
|
||||||
|
char buf[96];
|
||||||
|
snprintf(buf, sizeof(buf), "STA %s link=%s", label, link_name(st));
|
||||||
|
set_debug(buf);
|
||||||
|
}
|
||||||
|
if (st == CYW43_LINK_UP) {
|
||||||
|
return PICO_OK;
|
||||||
|
}
|
||||||
|
if (st == CYW43_LINK_BADAUTH) {
|
||||||
|
return PICO_ERROR_BADAUTH;
|
||||||
|
}
|
||||||
|
if (st == CYW43_LINK_FAIL) {
|
||||||
|
return PICO_ERROR_CONNECT_FAILED;
|
||||||
|
}
|
||||||
|
/* Associated but no lease — restart DHCP (free UDP PCB pool if SoftAP ate it). */
|
||||||
|
if (st == CYW43_LINK_NOIP || st == CYW43_LINK_JOIN) {
|
||||||
|
if (absolute_time_diff_us(last_dhcp, get_absolute_time()) > 4000000) {
|
||||||
|
last_dhcp = get_absolute_time();
|
||||||
|
cyw43_arch_lwip_begin();
|
||||||
|
dhcp_release_and_stop(&cyw43_state.netif[CYW43_ITF_STA]);
|
||||||
|
err_t de = dhcp_start(&cyw43_state.netif[CYW43_ITF_STA]);
|
||||||
|
cyw43_arch_lwip_end();
|
||||||
|
printf("[wifi] dhcp_start on STA => %d\n", (int)de);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (st == CYW43_LINK_NONET) {
|
||||||
|
cyw43_arch_wifi_connect_async(ssid, pass, auth);
|
||||||
|
}
|
||||||
|
sleep_ms(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
int final = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA);
|
||||||
|
printf("[wifi] %s timed out, final link=%s\n", label, link_name(final));
|
||||||
|
if (final == CYW43_LINK_BADAUTH) {
|
||||||
|
return PICO_ERROR_BADAUTH;
|
||||||
|
}
|
||||||
|
if (final == CYW43_LINK_UP) {
|
||||||
|
return PICO_OK;
|
||||||
|
}
|
||||||
|
return PICO_ERROR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool start_sta_iface(const config_t *cfg) {
|
||||||
|
if (!cfg->sta_ssid[0]) {
|
||||||
|
set_debug("STA aborted: empty SSID in flash");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SoftAP (if any) must be down — concurrent SoftAP breaks STA DHCP on CYW43. */
|
||||||
|
softap_pause_for_sta();
|
||||||
|
|
||||||
|
cyw43_arch_enable_sta_mode();
|
||||||
|
wifi_disable_powersave();
|
||||||
|
netif_set_default(&cyw43_state.netif[CYW43_ITF_STA]);
|
||||||
|
|
||||||
|
dump_ssid_debug("STA target", cfg->sta_ssid);
|
||||||
|
dump_pass_debug(cfg->sta_pass);
|
||||||
|
|
||||||
|
const char *pass = cfg->sta_pass[0] ? cfg->sta_pass : NULL;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
uint32_t auth;
|
||||||
|
const char *label;
|
||||||
|
} attempts[] = {
|
||||||
|
{CYW43_AUTH_WPA2_AES_PSK, "WPA2_AES"},
|
||||||
|
{CYW43_AUTH_WPA2_MIXED_PSK, "WPA2_MIXED"},
|
||||||
|
{CYW43_AUTH_WPA_TKIP_PSK, "WPA_TKIP"},
|
||||||
|
};
|
||||||
|
|
||||||
|
int rc = PICO_ERROR_CONNECT_FAILED;
|
||||||
|
for (unsigned i = 0; i < count_of(attempts); i++) {
|
||||||
|
if (!pass) {
|
||||||
|
rc = connect_with_status(cfg->sta_ssid, NULL, CYW43_AUTH_OPEN, "OPEN", 25000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
char buf[96];
|
||||||
|
snprintf(buf, sizeof(buf), "trying %s...", attempts[i].label);
|
||||||
|
set_debug(buf);
|
||||||
|
rc = connect_with_status(cfg->sta_ssid, pass, attempts[i].auth, attempts[i].label, 25000);
|
||||||
|
printf("[wifi] attempt %s => %d (%s)\n", attempts[i].label, rc, err_name(rc));
|
||||||
|
if (rc == PICO_OK) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (rc == PICO_ERROR_BADAUTH) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* Timeout/fail after association — other auth modes won't help. */
|
||||||
|
if (rc == PICO_ERROR_TIMEOUT) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rc != PICO_OK) {
|
||||||
|
char buf[160];
|
||||||
|
snprintf(buf, sizeof(buf), "STA FAILED %s", err_name(rc));
|
||||||
|
set_debug(buf);
|
||||||
|
cyw43_arch_disable_sta_mode();
|
||||||
|
g_sta_up = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
wifi_disable_powersave();
|
||||||
|
g_sta_up = true;
|
||||||
|
refresh_sta_ip();
|
||||||
|
printf("[wifi] STA connected IP=%s\n", g_sta_ip);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wifi_mgr_start(const config_t *cfg) {
|
||||||
|
printf("[wifi] cyw43 init country=NZ...\n");
|
||||||
|
if (cyw43_arch_init_with_country(BOARD_WIFI_COUNTRY)) {
|
||||||
|
set_debug("cyw43 init FAILED");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1);
|
||||||
|
wifi_disable_powersave();
|
||||||
|
|
||||||
|
g_cfg = *cfg;
|
||||||
|
wifi_mode_t want = g_cfg.wifi_mode;
|
||||||
|
/* Legacy AP+STA → STA with SoftAP fallback on failure. */
|
||||||
|
if (want == WIFI_MODE_AP_STA) {
|
||||||
|
want = WIFI_MODE_STA;
|
||||||
|
}
|
||||||
|
if (want != WIFI_MODE_STA) {
|
||||||
|
want = WIFI_MODE_AP;
|
||||||
|
}
|
||||||
|
g_cfg.wifi_mode = want;
|
||||||
|
|
||||||
|
printf("[wifi] configured mode=%s sta='%s' ap='%s'\n",
|
||||||
|
wifi_mode_name(want), g_cfg.sta_ssid, g_cfg.ap_ssid);
|
||||||
|
|
||||||
|
if (want == WIFI_MODE_AP) {
|
||||||
|
set_debug("starting SoftAP...");
|
||||||
|
if (!start_ap_iface(&g_cfg)) {
|
||||||
|
set_debug("AP start FAILED");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sleep_ms(300);
|
||||||
|
g_mode = WIFI_MODE_AP;
|
||||||
|
g_up = true;
|
||||||
|
update_primary_ip();
|
||||||
|
mdns_start();
|
||||||
|
char buf[160];
|
||||||
|
snprintf(buf, sizeof(buf), "AP ready ssid='%s' ip=%s",
|
||||||
|
g_ap_ssid_active, g_ap_ip);
|
||||||
|
set_debug(buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STA: join after HTTP/Art-Net start; SoftAP only if join fails. */
|
||||||
|
g_mode = WIFI_MODE_STA;
|
||||||
|
g_up = false;
|
||||||
|
g_sta_deadline = make_timeout_time_ms(500);
|
||||||
|
g_sta_pending = true;
|
||||||
|
set_debug("STA mode — joining LAN shortly (SoftAP if join fails)");
|
||||||
|
printf("[wifi] STA join deferred 0.5s; SoftAP only on failure\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wifi_mgr_poll(void) {
|
||||||
|
if (!g_sta_pending || !time_reached(g_sta_deadline)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_sta_pending = false;
|
||||||
|
|
||||||
|
printf("[wifi] STA join starting\n");
|
||||||
|
set_debug("attempting STA...");
|
||||||
|
|
||||||
|
bool sta_ok = start_sta_iface(&g_cfg);
|
||||||
|
if (sta_ok) {
|
||||||
|
g_mode = WIFI_MODE_STA;
|
||||||
|
g_up = true;
|
||||||
|
update_primary_ip();
|
||||||
|
mdns_start();
|
||||||
|
artnet_refresh();
|
||||||
|
|
||||||
|
char buf[160];
|
||||||
|
snprintf(buf, sizeof(buf), "STA ok ip=%s (SoftAP off)", g_sta_ip);
|
||||||
|
set_debug(buf);
|
||||||
|
printf("[wifi] %s\n", buf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("[wifi] STA failed — SoftAP fallback for recovery\n");
|
||||||
|
set_debug("STA failed — starting SoftAP fallback...");
|
||||||
|
if (!start_ap_iface(&g_cfg)) {
|
||||||
|
set_debug("STA failed and SoftAP fallback FAILED");
|
||||||
|
g_mode = WIFI_MODE_AP;
|
||||||
|
g_up = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sleep_ms(300);
|
||||||
|
g_mode = WIFI_MODE_AP;
|
||||||
|
g_up = true;
|
||||||
|
update_primary_ip();
|
||||||
|
mdns_start();
|
||||||
|
artnet_refresh();
|
||||||
|
|
||||||
|
char buf[160];
|
||||||
|
snprintf(buf, sizeof(buf), "AP fallback ssid='%s' ip=%s",
|
||||||
|
g_ap_ssid_active, g_ap_ip);
|
||||||
|
set_debug(buf);
|
||||||
|
printf("[wifi] %s\n", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
wifi_mode_t wifi_mgr_active_mode(void) {
|
||||||
|
return g_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *wifi_mgr_ip_str(void) {
|
||||||
|
update_primary_ip();
|
||||||
|
return g_ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *wifi_mgr_sta_ip_str(void) {
|
||||||
|
if (g_sta_up) {
|
||||||
|
refresh_sta_ip();
|
||||||
|
}
|
||||||
|
return g_sta_ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *wifi_mgr_ap_ip_str(void) {
|
||||||
|
return g_ap_ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *wifi_mgr_mdns_name(void) {
|
||||||
|
static char name[CONFIG_HOSTNAME_MAX + 8];
|
||||||
|
snprintf(name, sizeof(name), "%s.local", mdns_hostname());
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wifi_mgr_sta_up(void) {
|
||||||
|
return g_sta_up;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wifi_mgr_ap_up(void) {
|
||||||
|
return g_ap_up;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wifi_mgr_is_up(void) {
|
||||||
|
return g_up;
|
||||||
|
}
|
||||||
Vendored
+309
@@ -0,0 +1,309 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of the MicroPython project, http://micropython.org/
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2018-2019 Damien P. George
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// For DHCP specs see:
|
||||||
|
// https://www.ietf.org/rfc/rfc2131.txt
|
||||||
|
// https://tools.ietf.org/html/rfc2132 -- DHCP Options and BOOTP Vendor Extensions
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include "cyw43_config.h"
|
||||||
|
#include "dhcpserver.h"
|
||||||
|
#include "lwip/udp.h"
|
||||||
|
|
||||||
|
#define DHCPDISCOVER (1)
|
||||||
|
#define DHCPOFFER (2)
|
||||||
|
#define DHCPREQUEST (3)
|
||||||
|
#define DHCPDECLINE (4)
|
||||||
|
#define DHCPACK (5)
|
||||||
|
#define DHCPNACK (6)
|
||||||
|
#define DHCPRELEASE (7)
|
||||||
|
#define DHCPINFORM (8)
|
||||||
|
|
||||||
|
#define DHCP_OPT_PAD (0)
|
||||||
|
#define DHCP_OPT_SUBNET_MASK (1)
|
||||||
|
#define DHCP_OPT_ROUTER (3)
|
||||||
|
#define DHCP_OPT_DNS (6)
|
||||||
|
#define DHCP_OPT_HOST_NAME (12)
|
||||||
|
#define DHCP_OPT_REQUESTED_IP (50)
|
||||||
|
#define DHCP_OPT_IP_LEASE_TIME (51)
|
||||||
|
#define DHCP_OPT_MSG_TYPE (53)
|
||||||
|
#define DHCP_OPT_SERVER_ID (54)
|
||||||
|
#define DHCP_OPT_PARAM_REQUEST_LIST (55)
|
||||||
|
#define DHCP_OPT_MAX_MSG_SIZE (57)
|
||||||
|
#define DHCP_OPT_VENDOR_CLASS_ID (60)
|
||||||
|
#define DHCP_OPT_CLIENT_ID (61)
|
||||||
|
#define DHCP_OPT_END (255)
|
||||||
|
|
||||||
|
#define PORT_DHCP_SERVER (67)
|
||||||
|
#define PORT_DHCP_CLIENT (68)
|
||||||
|
|
||||||
|
#define DEFAULT_LEASE_TIME_S (24 * 60 * 60) // in seconds
|
||||||
|
|
||||||
|
#define MAC_LEN (6)
|
||||||
|
#define MAKE_IP4(a, b, c, d) ((a) << 24 | (b) << 16 | (c) << 8 | (d))
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t op; // message opcode
|
||||||
|
uint8_t htype; // hardware address type
|
||||||
|
uint8_t hlen; // hardware address length
|
||||||
|
uint8_t hops;
|
||||||
|
uint32_t xid; // transaction id, chosen by client
|
||||||
|
uint16_t secs; // client seconds elapsed
|
||||||
|
uint16_t flags;
|
||||||
|
uint8_t ciaddr[4]; // client IP address
|
||||||
|
uint8_t yiaddr[4]; // your IP address
|
||||||
|
uint8_t siaddr[4]; // next server IP address
|
||||||
|
uint8_t giaddr[4]; // relay agent IP address
|
||||||
|
uint8_t chaddr[16]; // client hardware address
|
||||||
|
uint8_t sname[64]; // server host name
|
||||||
|
uint8_t file[128]; // boot file name
|
||||||
|
uint8_t options[312]; // optional parameters, variable, starts with magic
|
||||||
|
} dhcp_msg_t;
|
||||||
|
|
||||||
|
static int dhcp_socket_new_dgram(struct udp_pcb **udp, void *cb_data, udp_recv_fn cb_udp_recv) {
|
||||||
|
// family is AF_INET
|
||||||
|
// type is SOCK_DGRAM
|
||||||
|
|
||||||
|
*udp = udp_new();
|
||||||
|
if (*udp == NULL) {
|
||||||
|
return -ENOMEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register callback
|
||||||
|
udp_recv(*udp, cb_udp_recv, (void *)cb_data);
|
||||||
|
|
||||||
|
return 0; // success
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dhcp_socket_free(struct udp_pcb **udp) {
|
||||||
|
if (*udp != NULL) {
|
||||||
|
udp_remove(*udp);
|
||||||
|
*udp = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int dhcp_socket_bind(struct udp_pcb **udp, uint16_t port) {
|
||||||
|
// TODO convert lwIP errors to errno
|
||||||
|
return udp_bind(*udp, IP_ANY_TYPE, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int dhcp_socket_sendto(struct udp_pcb **udp, struct netif *nif, const void *buf, size_t len, uint32_t ip, uint16_t port) {
|
||||||
|
if (len > 0xffff) {
|
||||||
|
len = 0xffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
|
||||||
|
if (p == NULL) {
|
||||||
|
return -ENOMEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(p->payload, buf, len);
|
||||||
|
|
||||||
|
ip_addr_t dest;
|
||||||
|
IP4_ADDR(ip_2_ip4(&dest), ip >> 24 & 0xff, ip >> 16 & 0xff, ip >> 8 & 0xff, ip & 0xff);
|
||||||
|
err_t err;
|
||||||
|
if (nif != NULL) {
|
||||||
|
err = udp_sendto_if(*udp, p, &dest, port, nif);
|
||||||
|
} else {
|
||||||
|
err = udp_sendto(*udp, p, &dest, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
pbuf_free(p);
|
||||||
|
|
||||||
|
if (err != ERR_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t *opt_find(uint8_t *opt, uint8_t cmd) {
|
||||||
|
for (int i = 0; i < 308 && opt[i] != DHCP_OPT_END;) {
|
||||||
|
if (opt[i] == cmd) {
|
||||||
|
return &opt[i];
|
||||||
|
}
|
||||||
|
i += 2 + opt[i + 1];
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void opt_write_n(uint8_t **opt, uint8_t cmd, size_t n, const void *data) {
|
||||||
|
uint8_t *o = *opt;
|
||||||
|
*o++ = cmd;
|
||||||
|
*o++ = n;
|
||||||
|
memcpy(o, data, n);
|
||||||
|
*opt = o + n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void opt_write_u8(uint8_t **opt, uint8_t cmd, uint8_t val) {
|
||||||
|
uint8_t *o = *opt;
|
||||||
|
*o++ = cmd;
|
||||||
|
*o++ = 1;
|
||||||
|
*o++ = val;
|
||||||
|
*opt = o;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void opt_write_u32(uint8_t **opt, uint8_t cmd, uint32_t val) {
|
||||||
|
uint8_t *o = *opt;
|
||||||
|
*o++ = cmd;
|
||||||
|
*o++ = 4;
|
||||||
|
*o++ = val >> 24;
|
||||||
|
*o++ = val >> 16;
|
||||||
|
*o++ = val >> 8;
|
||||||
|
*o++ = val;
|
||||||
|
*opt = o;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dhcp_server_process(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *src_addr, u16_t src_port) {
|
||||||
|
dhcp_server_t *d = arg;
|
||||||
|
(void)upcb;
|
||||||
|
(void)src_addr;
|
||||||
|
(void)src_port;
|
||||||
|
|
||||||
|
// This is around 548 bytes
|
||||||
|
dhcp_msg_t dhcp_msg;
|
||||||
|
|
||||||
|
#define DHCP_MIN_SIZE (240 + 3)
|
||||||
|
if (p->tot_len < DHCP_MIN_SIZE) {
|
||||||
|
goto ignore_request;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t len = pbuf_copy_partial(p, &dhcp_msg, sizeof(dhcp_msg), 0);
|
||||||
|
if (len < DHCP_MIN_SIZE) {
|
||||||
|
goto ignore_request;
|
||||||
|
}
|
||||||
|
|
||||||
|
dhcp_msg.op = DHCPOFFER;
|
||||||
|
memcpy(&dhcp_msg.yiaddr, &ip4_addr_get_u32(ip_2_ip4(&d->ip)), 4);
|
||||||
|
|
||||||
|
uint8_t *opt = (uint8_t *)&dhcp_msg.options;
|
||||||
|
opt += 4; // assume magic cookie: 99, 130, 83, 99
|
||||||
|
|
||||||
|
uint8_t *msgtype = opt_find(opt, DHCP_OPT_MSG_TYPE);
|
||||||
|
if (msgtype == NULL) {
|
||||||
|
// A DHCP package without MSG_TYPE?
|
||||||
|
goto ignore_request;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msgtype[2]) {
|
||||||
|
case DHCPDISCOVER: {
|
||||||
|
int yi = DHCPS_MAX_IP;
|
||||||
|
for (int i = 0; i < DHCPS_MAX_IP; ++i) {
|
||||||
|
if (memcmp(d->lease[i].mac, dhcp_msg.chaddr, MAC_LEN) == 0) {
|
||||||
|
// MAC match, use this IP address
|
||||||
|
yi = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (yi == DHCPS_MAX_IP) {
|
||||||
|
// Look for a free IP address
|
||||||
|
if (memcmp(d->lease[i].mac, "\x00\x00\x00\x00\x00\x00", MAC_LEN) == 0) {
|
||||||
|
// IP available
|
||||||
|
yi = i;
|
||||||
|
}
|
||||||
|
uint32_t expiry = d->lease[i].expiry << 16 | 0xffff;
|
||||||
|
if ((int32_t)(expiry - cyw43_hal_ticks_ms()) < 0) {
|
||||||
|
// IP expired, reuse it
|
||||||
|
memset(d->lease[i].mac, 0, MAC_LEN);
|
||||||
|
yi = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (yi == DHCPS_MAX_IP) {
|
||||||
|
// No more IP addresses left
|
||||||
|
goto ignore_request;
|
||||||
|
}
|
||||||
|
dhcp_msg.yiaddr[3] = DHCPS_BASE_IP + yi;
|
||||||
|
opt_write_u8(&opt, DHCP_OPT_MSG_TYPE, DHCPOFFER);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case DHCPREQUEST: {
|
||||||
|
uint8_t *o = opt_find(opt, DHCP_OPT_REQUESTED_IP);
|
||||||
|
if (o == NULL) {
|
||||||
|
// Should be NACK
|
||||||
|
goto ignore_request;
|
||||||
|
}
|
||||||
|
if (memcmp(o + 2, &ip4_addr_get_u32(ip_2_ip4(&d->ip)), 3) != 0) {
|
||||||
|
// Should be NACK
|
||||||
|
goto ignore_request;
|
||||||
|
}
|
||||||
|
uint8_t yi = o[5] - DHCPS_BASE_IP;
|
||||||
|
if (yi >= DHCPS_MAX_IP) {
|
||||||
|
// Should be NACK
|
||||||
|
goto ignore_request;
|
||||||
|
}
|
||||||
|
if (memcmp(d->lease[yi].mac, dhcp_msg.chaddr, MAC_LEN) == 0) {
|
||||||
|
// MAC match, ok to use this IP address
|
||||||
|
} else if (memcmp(d->lease[yi].mac, "\x00\x00\x00\x00\x00\x00", MAC_LEN) == 0) {
|
||||||
|
// IP unused, ok to use this IP address
|
||||||
|
memcpy(d->lease[yi].mac, dhcp_msg.chaddr, MAC_LEN);
|
||||||
|
} else {
|
||||||
|
// IP already in use
|
||||||
|
// Should be NACK
|
||||||
|
goto ignore_request;
|
||||||
|
}
|
||||||
|
d->lease[yi].expiry = (cyw43_hal_ticks_ms() + DEFAULT_LEASE_TIME_S * 1000) >> 16;
|
||||||
|
dhcp_msg.yiaddr[3] = DHCPS_BASE_IP + yi;
|
||||||
|
opt_write_u8(&opt, DHCP_OPT_MSG_TYPE, DHCPACK);
|
||||||
|
printf("DHCPS: client connected: MAC=%02x:%02x:%02x:%02x:%02x:%02x IP=%u.%u.%u.%u\n",
|
||||||
|
dhcp_msg.chaddr[0], dhcp_msg.chaddr[1], dhcp_msg.chaddr[2], dhcp_msg.chaddr[3], dhcp_msg.chaddr[4], dhcp_msg.chaddr[5],
|
||||||
|
dhcp_msg.yiaddr[0], dhcp_msg.yiaddr[1], dhcp_msg.yiaddr[2], dhcp_msg.yiaddr[3]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
goto ignore_request;
|
||||||
|
}
|
||||||
|
|
||||||
|
opt_write_n(&opt, DHCP_OPT_SERVER_ID, 4, &ip4_addr_get_u32(ip_2_ip4(&d->ip)));
|
||||||
|
opt_write_n(&opt, DHCP_OPT_SUBNET_MASK, 4, &ip4_addr_get_u32(ip_2_ip4(&d->nm)));
|
||||||
|
opt_write_n(&opt, DHCP_OPT_ROUTER, 4, &ip4_addr_get_u32(ip_2_ip4(&d->ip))); // aka gateway; can have multiple addresses
|
||||||
|
opt_write_n(&opt, DHCP_OPT_DNS, 4, &ip4_addr_get_u32(ip_2_ip4(&d->ip))); // this server is the dns
|
||||||
|
opt_write_u32(&opt, DHCP_OPT_IP_LEASE_TIME, DEFAULT_LEASE_TIME_S);
|
||||||
|
*opt++ = DHCP_OPT_END;
|
||||||
|
struct netif *nif = ip_current_input_netif();
|
||||||
|
dhcp_socket_sendto(&d->udp, nif, &dhcp_msg, opt - (uint8_t *)&dhcp_msg, 0xffffffff, PORT_DHCP_CLIENT);
|
||||||
|
|
||||||
|
ignore_request:
|
||||||
|
pbuf_free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dhcp_server_init(dhcp_server_t *d, ip_addr_t *ip, ip_addr_t *nm) {
|
||||||
|
ip_addr_copy(d->ip, *ip);
|
||||||
|
ip_addr_copy(d->nm, *nm);
|
||||||
|
memset(d->lease, 0, sizeof(d->lease));
|
||||||
|
if (dhcp_socket_new_dgram(&d->udp, d, dhcp_server_process) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dhcp_socket_bind(&d->udp, PORT_DHCP_SERVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dhcp_server_deinit(dhcp_server_t *d) {
|
||||||
|
dhcp_socket_free(&d->udp);
|
||||||
|
}
|
||||||
Vendored
+49
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of the MicroPython project, http://micropython.org/
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2018-2019 Damien P. George
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#ifndef MICROPY_INCLUDED_LIB_NETUTILS_DHCPSERVER_H
|
||||||
|
#define MICROPY_INCLUDED_LIB_NETUTILS_DHCPSERVER_H
|
||||||
|
|
||||||
|
#include "lwip/ip_addr.h"
|
||||||
|
|
||||||
|
#define DHCPS_BASE_IP (16)
|
||||||
|
#define DHCPS_MAX_IP (8)
|
||||||
|
|
||||||
|
typedef struct _dhcp_server_lease_t {
|
||||||
|
uint8_t mac[6];
|
||||||
|
uint16_t expiry;
|
||||||
|
} dhcp_server_lease_t;
|
||||||
|
|
||||||
|
typedef struct _dhcp_server_t {
|
||||||
|
ip_addr_t ip;
|
||||||
|
ip_addr_t nm;
|
||||||
|
dhcp_server_lease_t lease[DHCPS_MAX_IP];
|
||||||
|
struct udp_pcb *udp;
|
||||||
|
} dhcp_server_t;
|
||||||
|
|
||||||
|
void dhcp_server_init(dhcp_server_t *d, ip_addr_t *ip, ip_addr_t *nm);
|
||||||
|
void dhcp_server_deinit(dhcp_server_t *d);
|
||||||
|
|
||||||
|
#endif // MICROPY_INCLUDED_LIB_NETUTILS_DHCPSERVER_H
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Note: runtime serves embedded copies from src/http_server.c.
|
||||||
|
Edit both places if you change the UI, or regenerate embeds from these files.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>ArtNetDMX</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>ArtNetDMX</h1>
|
||||||
|
<nav><a href="/" class="active">Channels</a><a href="/wifi">WiFi</a></nav>
|
||||||
|
<div id="status" class="status">…</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="tabs">
|
||||||
|
<button type="button" id="tab0" class="active" data-u="0">Universe A</button>
|
||||||
|
<button type="button" id="tab1" data-u="1">Universe B</button>
|
||||||
|
<button type="button" id="clear">Clear</button>
|
||||||
|
<button type="button" id="full">All 255</button>
|
||||||
|
</div>
|
||||||
|
<div id="grid" class="grid"></div>
|
||||||
|
</main>
|
||||||
|
<script src="/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>WiFi — ArtNetDMX</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>ArtNetDMX</h1>
|
||||||
|
<nav><a href="/">Channels</a><a href="/wifi" class="active">WiFi</a></nav>
|
||||||
|
<div id="status" class="status">…</div>
|
||||||
|
</header>
|
||||||
|
<main class="form-wrap">
|
||||||
|
<form id="wifiForm">
|
||||||
|
<label>Mode
|
||||||
|
<select name="mode" id="mode">
|
||||||
|
<option value="0">Access Point</option>
|
||||||
|
<option value="1">Station (AP fallback if join fails)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<p class="hint">Station joins your LAN only. If join fails, SoftAP starts for recovery.</p>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Station</legend>
|
||||||
|
<label>SSID<input name="sta_ssid" id="sta_ssid" maxlength="31"></label>
|
||||||
|
<label>Password<input name="sta_pass" id="sta_pass" type="password" maxlength="63"></label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Access Point</legend>
|
||||||
|
<label>SSID<input name="ap_ssid" id="ap_ssid" maxlength="31"></label>
|
||||||
|
<label>Password<input name="ap_pass" id="ap_pass" type="password" maxlength="63"></label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Art-Net universes</legend>
|
||||||
|
<label>Port A<input name="universe_a" id="universe_a" type="number" min="0" max="32767"></label>
|
||||||
|
<label>Port B<input name="universe_b" id="universe_b" type="number" min="0" max="32767"></label>
|
||||||
|
</fieldset>
|
||||||
|
<button type="submit">Save & reboot</button>
|
||||||
|
<p class="hint">After save the device reboots into the selected mode. AP default: ArtNetDMX / artnetdmx @ 192.168.4.1</p>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
<script src="/wifi.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user