refactor(examples): move Pico W WiFi examples to the pro overlay seam

The Pico W WiFi showcase examples are a paid-overlay feature now (the WiFi
engine moved to the overlay). Read them through a build-time `@pro` seam so the
SSR prerender + gallery + sitemap include them when built with the overlay, and
OSS gets an empty stub.

- data/examples.ts: import { proExamples } from '@pro/data/proExamples' (static,
  build-time) instead of the local examples-picow-wifi.ts; delete that file.
- src/__pro_stub__/data/proExamples.ts: OSS no-op (empty list) for the @pro alias.
- vitest.config.ts: mirror the @pro alias (stub by default / overlay when
  VITE_PRO_BUILD) so tests loading examples.ts resolve it.
- scripts/generate-sitemap.mjs: also parse <PRO_OVERLAY_PATH>/data/proExamples.ts
  when building with the overlay (the script reads example IDs from source text,
  so it can't follow the alias).
- Tests: drop the picow-wifi import/usage from the 5 OSS example tests (they
  validate the OSS set now); prune the 4 obsolete picow netlist snapshots. The
  overlay's proExamples get their own coverage in pro/.../__tests__/.
This commit is contained in:
David Montero 2026-06-15 15:54:44 +02:00
parent 7db9e41278
commit 375b952d47
11 changed files with 43 additions and 409 deletions

View File

@ -57,6 +57,23 @@ const exampleIds = [
...parseExampleIds(circuitSource),
];
// Pro overlay examples (e.g. the Pico W WiFi showcase) live in
// <PRO_OVERLAY_PATH>/data/proExamples.ts and are spread into examples.ts via the
// `@pro` alias at build time. This script parses example IDs from source TEXT
// (it never executes the module), so it can't follow the alias — read the
// overlay file directly when building with the overlay. OSS builds skip this.
if (process.env.VITE_PRO_BUILD && process.env.PRO_OVERLAY_PATH) {
try {
const proSource = readFileSync(
resolve(process.env.PRO_OVERLAY_PATH, 'data/proExamples.ts'),
'utf-8',
);
exampleIds.push(...parseExampleIds(proSource));
} catch {
// No overlay examples file — nothing to add.
}
}
const exampleUrls = exampleIds.map((id) => ({
loc: `${DOMAIN}/examples/${id}`,
lastmod: TODAY,

View File

@ -0,0 +1,8 @@
// Open-source no-op stub for `@pro/data/proExamples` (see vite.config.ts).
// OSS builds ship NO pro examples — the Pico W WiFi showcase is a paid overlay
// feature, so the velxio-prod overlay replaces this with the real list at build
// time (VITE_PRO_BUILD=true + PRO_OVERLAY_PATH). The static import in
// data/examples.ts resolves here in OSS, contributing an empty spread.
import type { ExampleProject } from '../../data/examples';
export const proExamples: ExampleProject[] = [];

View File

@ -3449,32 +3449,3 @@ R_autopull_n5 n5 0 100Meg
.op
.end"
`;
exports[`netlist snapshot — picow-wifi (4 examples) > picow-wifi-async-led 1`] = `
"* Velxio circuit
.op
.end"
`;
exports[`netlist snapshot — picow-wifi (4 examples) > picow-wifi-relay-web-server 1`] = `
"* Velxio circuit
V_relay-led_sense n0 relay-led_sense_mid DC 0
D_relay-led relay-led_sense_mid n1 LED_RED
R_autopull_n0 n0 0 100Meg
R_autopull_n1 n1 0 100Meg
.model LED_RED D(Is=1e-20 N=1.7)
.op
.end"
`;
exports[`netlist snapshot — picow-wifi (4 examples) > picow-wifi-servo-web 1`] = `
"* Velxio circuit
.op
.end"
`;
exports[`netlist snapshot — picow-wifi (4 examples) > picow-wifi-websocket-led 1`] = `
"* Velxio circuit
.op
.end"
`;

View File

@ -21,7 +21,6 @@ import { analogExamples } from '../data/examples-analog';
import { digitalExamples } from '../data/examples-digital';
import { hundredDaysExamples } from '../data/examples-100-days';
import { epaperExamples } from '../data/examples-displays-epaper';
import { picowWifiExamples } from '../data/examples-picow-wifi';
import { circuitExamples } from '../data/examples-circuits';
import type { ExampleProject } from '../data/examples';
@ -30,7 +29,6 @@ const ALL_EXAMPLES: ExampleProject[] = [
...digitalExamples,
...hundredDaysExamples,
...epaperExamples,
...picowWifiExamples,
...circuitExamples,
];

View File

@ -25,7 +25,6 @@ import { analogExamples } from '../data/examples-analog';
import { digitalExamples } from '../data/examples-digital';
import { hundredDaysExamples } from '../data/examples-100-days';
import { epaperExamples } from '../data/examples-displays-epaper';
import { picowWifiExamples } from '../data/examples-picow-wifi';
import { circuitExamples } from '../data/examples-circuits';
import {
stripBrandPrefix,
@ -62,7 +61,6 @@ const ALL_EXAMPLE_SOURCES = {
digital: digitalExamples,
'100-days': hundredDaysExamples,
'epaper-displays': epaperExamples,
'picow-wifi': picowWifiExamples,
circuits: circuitExamples,
};

View File

@ -25,7 +25,6 @@
import { describe, it, expect } from 'vitest';
import { hundredDaysExamples } from '../data/examples-100-days';
import { epaperExamples } from '../data/examples-displays-epaper';
import { picowWifiExamples } from '../data/examples-picow-wifi';
import { circuitExamples } from '../data/examples-circuits';
import { exampleToBuildNetlistInput } from '../utils/exampleToBuildNetlistInput';
import { solveInput } from './helpers/solveInput';
@ -91,5 +90,4 @@ function buildSuite(bucket: string, examples: ExampleProject[]): void {
buildSuite('100-days', hundredDaysExamples);
buildSuite('epaper-displays', epaperExamples);
buildSuite('picow-wifi', picowWifiExamples);
buildSuite('circuits', circuitExamples);

View File

@ -26,7 +26,6 @@ import { analogExamples } from '../data/examples-analog';
import { digitalExamples } from '../data/examples-digital';
import { hundredDaysExamples } from '../data/examples-100-days';
import { epaperExamples } from '../data/examples-displays-epaper';
import { picowWifiExamples } from '../data/examples-picow-wifi';
import { circuitExamples } from '../data/examples-circuits';
import { exampleToBuildNetlistInput } from '../utils/exampleToBuildNetlistInput';
import { buildNetlist } from '../simulation/spice/NetlistBuilder';
@ -62,7 +61,6 @@ const BUCKETS: Bucket[] = [
{ name: 'digital', examples: digitalExamples },
{ name: '100-days', examples: hundredDaysExamples },
{ name: 'epaper', examples: epaperExamples },
{ name: 'picow-wifi', examples: picowWifiExamples },
{ name: 'circuits', examples: circuitExamples },
];

View File

@ -27,7 +27,6 @@ import { analogExamples } from '../data/examples-analog';
import { digitalExamples } from '../data/examples-digital';
import { hundredDaysExamples } from '../data/examples-100-days';
import { epaperExamples } from '../data/examples-displays-epaper';
import { picowWifiExamples } from '../data/examples-picow-wifi';
import { circuitExamples } from '../data/examples-circuits';
import type { ExampleProject } from '../data/examples';
import { BOARD_KIND_FQBN, type BoardKind } from '../types/board';
@ -39,7 +38,6 @@ const ALL_EXAMPLES: ExampleProject[] = [
...digitalExamples,
...hundredDaysExamples,
...epaperExamples,
...picowWifiExamples,
...circuitExamples,
];

View File

@ -1,368 +0,0 @@
/**
* Pico W WiFi showcase curated examples that highlight Velxio's
* new CYW43439 chip emulation (see frontend/src/simulation/cyw43/).
*
* Source projects come from
* https://github.com/KritishMohapatra/100_Days_100_IoT_Projects
* (cloned at third-party/100_Days_100_IoT_Projects/) every example
* here is a verbatim copy of the upstream `Main Files/main.py` for the
* matching project, with the WiFi credentials replaced by the synthetic
* `Velxio-GUEST` AP that the emulator advertises.
*
* Each example is wired up to load with:
* boardType: 'pi-pico-w' triggers Cyw43Bridge attachment
* languageMode: 'micropython' MicroPythonLoader
* files: [{ name: 'main.py', ... }]
*
* The end-to-end harness that proves these work end-to-end lives at
* test/test_Raspberry_Pi_Pico_W/test_code/tests/07_picow_iot_projects.test.ts
*/
import type { ExampleProject } from './examples';
const TAGS_WIFI = ['100-days', 'pi-pico-w', 'micropython', 'wifi', 'cyw43'];
/** Replace placeholder SSID/password lines with our virtual AP. */
function withVelxioGuest(source: string): string {
return source
.replace(/SSID\s*=\s*"[^"]*"/g, 'SSID = "Velxio-GUEST"')
.replace(/ssid\s*=\s*"[^"]*"/g, 'ssid = "Velxio-GUEST"')
.replace(/WIFI_SSID\s*=\s*"[^"]*"/g, 'WIFI_SSID = "Velxio-GUEST"')
.replace(/PASSWORD\s*=\s*"[^"]*"/g, 'PASSWORD = ""')
.replace(/password\s*=\s*"[^"]*"/g, 'password = ""')
.replace(/PASS\s*=\s*"[^"]*"/g, 'PASS = ""')
.replace(/WIFI_PASSWORD\s*=\s*"[^"]*"/g, 'WIFI_PASSWORD = ""');
}
// ─── Project sources ─────────────────────────────────────────────────
//
// Pasted from upstream `Main Files/main.py`. Comments at the top of each
// string credit the original author/repo. Anyone can re-extract these by
// running `python test/test_100_days/_emit_examples_data.py`.
const ASYNC_LED_CONTROL_PY = withVelxioGuest(`# Pico W Async LED Control — MicroPython
# Source: github.com/KritishMohapatra/100_Days_100_IoT_Projects
# Project: Pico_W_Async_LED_Control_(MicroPython)
#
# Connects to Velxio-GUEST and runs a tiny async HTTP server on :80
# that toggles the on-board LED via Pin('LED'). The LED is wired
# through the CYW43439 chip Velxio's new emulator picks up the
# gpioout IOCTL and drives the LED in the canvas.
import uasyncio as asyncio
import network
from machine import Pin
SSID = "Velxio-GUEST"
PASSWORD = ""
led = Pin("LED", Pin.OUT)
led.off()
async def wifi_connect():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
print("Connecting WiFi...")
while not wlan.isconnected():
await asyncio.sleep(0.5)
ip = wlan.ifconfig()[0]
print("IP:", ip)
print("Open browser: http://%s/" % ip)
led_on = False
def page():
# The board's onboard LED isn't drawn on the canvas, so show a big
# state indicator here; the buttons reload so you can see it flip.
color = "#22c55e" if led_on else "#666"
label = "LED ON" if led_on else "LED OFF"
return ("<!DOCTYPE html><html><head>"
"<meta name=viewport content='width=device-width,initial-scale=1'></head>"
"<body style='font-family:sans-serif;text-align:center;padding:24px;background:#fafafa'>"
"<h2>Pico W Onboard LED</h2>"
"<div style='margin:18px auto;width:170px;padding:26px 0;border-radius:14px;"
"background:%s;color:#fff;font-size:24px;font-weight:bold'>%s</div>"
"<button style='font-size:18px;padding:12px 28px;margin:6px' "
"onclick=\\"fetch('on').then(()=>location.reload())\\">ON</button>"
"<button style='font-size:18px;padding:12px 28px;margin:6px' "
"onclick=\\"fetch('off').then(()=>location.reload())\\">OFF</button>"
"</body></html>") % (color, label)
async def handle_client(reader, writer):
global led_on
request = (await reader.readline()).decode()
while await reader.readline() != b"\\r\\n":
pass
if "GET /on" in request:
led.on(); led_on = True
elif "GET /off" in request:
led.off(); led_on = False
writer.write(("HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\nConnection: close\\r\\n\\r\\n" + page()).encode())
await writer.drain()
await writer.wait_closed()
async def main():
await wifi_connect()
await asyncio.start_server(handle_client, "0.0.0.0", 80)
print("Server running on port 80")
while True:
await asyncio.sleep(1)
asyncio.run(main())
`);
const RELAY_WEB_SERVER_PY = withVelxioGuest(`# IoT Relay Control Web Server — MicroPython
# Source: github.com/KritishMohapatra/100_Days_100_IoT_Projects
# Project: IoT_Relay_Control_Web_Server_(Raspberry_Pi_Pico_2W)
#
# A blocking-style HTTP server on port 80 that flips a relay on
# GP2 in response to GET /on and GET /off. The server runs on the
# IP that Velxio's emulator hands out (10.13.37.42).
import network
import socket
import machine
import time
relay = machine.Pin(2, machine.Pin.OUT)
# Active-high so the LED wired to GP2 on the canvas lights when ON.
relay_state = 0
relay.value(relay_state)
ssid = "Velxio-GUEST"
password = ""
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)
print("Connecting...")
while not wlan.isconnected():
time.sleep(0.5)
ip = wlan.ifconfig()[0]
print("Connected at:", ip)
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((ip, 80))
s.listen(1)
print("Open browser: http://%s/" % ip)
def page():
on = (relay_state == 1)
color = "#22c55e" if on else "#666"
label = "RELAY ON" if on else "RELAY OFF"
return ("<html><head><meta name=viewport content='width=device-width,initial-scale=1'></head>"
"<body style='font-family:sans-serif;text-align:center;padding:24px;background:#fafafa'>"
"<h2>Pico W Relay (GP2)</h2>"
"<div style='margin:18px auto;width:170px;padding:26px 0;border-radius:14px;"
"background:%s;color:#fff;font-size:24px;font-weight:bold'>%s</div>"
"<button style='font-size:18px;padding:12px 28px;margin:6px' "
"onclick=\\"fetch('on').then(()=>location.reload())\\">ON</button>"
"<button style='font-size:18px;padding:12px 28px;margin:6px' "
"onclick=\\"fetch('off').then(()=>location.reload())\\">OFF</button>"
"</body></html>") % (color, label)
# Wrap each client in try/except: a browser that drops the connection
# mid-response would otherwise raise ECONNRESET and kill the server loop.
while True:
try:
conn, addr = s.accept()
request = str(conn.recv(1024))
if "/on" in request:
relay_state = 1; relay.value(relay_state)
elif "/off" in request:
relay_state = 0; relay.value(relay_state)
conn.send("HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\n" + page())
conn.close()
except OSError:
try:
conn.close()
except Exception:
pass
`);
const SERVO_WEB_PY = withVelxioGuest(`# Pico W Web Servo Controller — MicroPython
# Source: github.com/KritishMohapatra/100_Days_100_IoT_Projects
# Project: Pico_W_Web_Servo_Controller
import network
import socket
from machine import Pin, PWM
import time
ssid = "Velxio-GUEST"
password = ""
print("Connecting to WiFi...")
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.connect(ssid, password)
while not sta.isconnected():
time.sleep(0.5)
print("Connected!", sta.ifconfig()[0])
servo = PWM(Pin(15), freq=50)
def write_servo(angle):
angle = max(0, min(180, angle))
pulse_us = 500 + (2500 - 500) * (angle / 180)
duty = int((pulse_us / 20000) * 65535)
servo.duty_u16(duty)
def webpage(pos):
return ("<html><body><h1>Servo {p}&deg;</h1>"
"<input type=range min=0 max=180 value={p} "
"oninput=\\"fetch('?value='+this.value)\\"></body></html>").format(p=pos)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', 80))
s.listen(5)
current_pos = 90
write_servo(current_pos)
print("Web server started. Open browser: http://%s/" % sta.ifconfig()[0])
while True:
conn, addr = s.accept()
request = conn.recv(1024).decode('utf-8')
if "GET /?value=" in request:
try:
v = int(request.split("/?value=")[1].split(" ")[0])
if 0 <= v <= 180:
current_pos = v; write_servo(v)
except (ValueError, IndexError):
pass
conn.send('HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\n')
conn.sendall(webpage(current_pos).encode('utf-8'))
conn.close()
`);
const WS_LED_PY = withVelxioGuest(`# WebSocket LED Control — MicroPython
# Source: github.com/KritishMohapatra/100_Days_100_IoT_Projects
# Project: WebSocket_LED_Control_using_Raspberry_Pi_Pico_W
import socket, network, time, ubinascii, uhashlib
from machine import Pin
led = Pin(15, Pin.OUT); led.value(0)
SSID = "Velxio-GUEST"
PASSWORD = ""
def ws_accept(key):
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
return ubinascii.b2a_base64(uhashlib.sha1((key + GUID).encode()).digest()).strip().decode()
def ws_decode(data):
if len(data) < 6: return ""
payload_len = data[1] & 127
mask = data[2:6]
payload = data[6:6 + payload_len]
return bytes(payload[i] ^ mask[i % 4] for i in range(len(payload))).decode()
wlan = network.WLAN(network.STA_IF); wlan.active(True); wlan.connect(SSID, PASSWORD)
while not wlan.isconnected():
time.sleep(1)
print("Connected!", wlan.ifconfig()[0])
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", 80)); server.listen(1)
while True:
conn, addr = server.accept()
try:
raw = conn.recv(1024).decode()
if "Sec-WebSocket-Key" in raw:
key = raw.split("Sec-WebSocket-Key: ")[1].split("\\r\\n")[0].strip()
conn.send(("HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\n"
"Connection: Upgrade\\r\\nSec-WebSocket-Accept: %s\\r\\n\\r\\n" % ws_accept(key)))
while True:
data = conn.recv(1024)
if not data: break
msg = ws_decode(data)
if msg == "ON": led.value(1); reply = "LED IS ON"
elif msg == "OFF": led.value(0); reply = "LED IS OFF"
else: reply = "OK"
conn.send(bytearray([0x81, len(reply)]) + reply.encode())
finally:
conn.close()
`);
// ─── Curated entries ─────────────────────────────────────────────────
export const picowWifiExamples: ExampleProject[] = [
{
id: 'picow-wifi-async-led',
title: 'Pico W — Async LED control over Wi-Fi',
description:
'Pico W joins Velxio-GUEST then runs an async HTTP server on :80. /on and /off toggle the on-board LED through the CYW43 chip — the same path the real driver takes.',
category: 'communication',
difficulty: 'beginner',
boardType: 'pi-pico-w',
languageMode: 'micropython',
files: [{ name: 'main.py', content: ASYNC_LED_CONTROL_PY }],
code: '',
components: [],
wires: [],
tags: TAGS_WIFI,
},
{
id: 'picow-wifi-relay-web-server',
title: 'Pico W — IoT relay web server',
description:
'Blocking HTTP server that drives a relay on GP2. Hit http://10.13.37.42/on and /off to flip it. Source: 100_Days_100_IoT_Projects/IoT_Relay_Control_Web_Server.',
category: 'communication',
difficulty: 'beginner',
boardType: 'pi-pico-w',
languageMode: 'micropython',
files: [{ name: 'main.py', content: RELAY_WEB_SERVER_PY }],
code: '',
components: [
// A red LED wired to GP2 so the relay state is visible on the canvas:
// GP2 HIGH (relay ON) lights it. Driven via the wire (anode->GP2,
// cathode->GND), the same path an Arduino LED uses.
{ type: 'wokwi-led', id: 'relay-led', x: 380, y: 40, properties: { color: 'red', pin: 2 } },
],
wires: [
{ id: 'w-relay-a', start: { componentId: 'pi-pico-w', pinName: 'GP2' }, end: { componentId: 'relay-led', pinName: 'A' }, color: '#ef4444' },
{ id: 'w-relay-c', start: { componentId: 'relay-led', pinName: 'C' }, end: { componentId: 'pi-pico-w', pinName: 'GND.1' }, color: '#1f2937' },
],
tags: TAGS_WIFI.concat(['relay']),
},
{
id: 'picow-wifi-servo-web',
title: 'Pico W — Web servo controller',
description:
'Drives a servo on GP15 from a slider on a web page served by the Pico W. Source: 100_Days_100_IoT_Projects/Pico_W_Web_Servo_Controller.',
category: 'robotics',
difficulty: 'intermediate',
boardType: 'pi-pico-w',
languageMode: 'micropython',
files: [{ name: 'main.py', content: SERVO_WEB_PY }],
code: '',
components: [],
wires: [],
tags: TAGS_WIFI.concat(['servo']),
},
{
id: 'picow-wifi-websocket-led',
title: 'Pico W — WebSocket-controlled LED',
description:
'Hand-rolled WebSocket server. Browser opens an upgrade request, then sends "ON"/"OFF" frames to toggle the LED on GP15. Source: 100_Days_100_IoT_Projects/WebSocket_LED_Control_using_Raspberry_Pi_Pico_W.',
category: 'communication',
difficulty: 'advanced',
boardType: 'pi-pico-w',
languageMode: 'micropython',
files: [{ name: 'main.py', content: WS_LED_PY }],
code: '',
components: [],
wires: [],
tags: TAGS_WIFI.concat(['websocket']),
},
];

View File

@ -8,7 +8,12 @@ import { circuitExamples } from './examples-circuits';
import { analogExamples } from './examples-analog';
import { digitalExamples } from './examples-digital';
import { hundredDaysExamples } from './examples-100-days';
import { picowWifiExamples } from './examples-picow-wifi';
// Pro overlay examples (the Pico W WiFi showcase) — resolves to the real list
// when built with the overlay (VITE_PRO_BUILD), else an empty stub in OSS.
// Static import so the build-time SSR prerender + gallery + sitemap pick them
// up. See pro/frontend/src/pro/data/proExamples.ts (overlay) and
// src/__pro_stub__/data/proExamples.ts (OSS no-op).
import { proExamples } from '@pro/data/proExamples';
import { epaperExamples } from './examples-displays-epaper';
import { retroIntelExamples } from './examples-retro-intel';
import { robotDesktopExamples } from './examples-robot-desktop';
@ -9735,7 +9740,7 @@ export const exampleProjects: ExampleProject[] = [
...analogExamples,
...digitalExamples,
...hundredDaysExamples,
...picowWifiExamples,
...proExamples,
...epaperExamples,
...retroIntelExamples,
...robotDesktopExamples,

View File

@ -22,11 +22,22 @@ import path from 'path';
* used by overlay tests (e.g. pro/.../snapshot.test.ts importing
* `@velxio/store/useEditorStore`) must be declared here too or
* test files explode with "Cannot find package '@velxio/...'".
* - `@pro` likewise mirrors vite.config.ts: it resolves to the OSS no-op
* stub by default, or the real overlay when VITE_PRO_BUILD +
* PRO_OVERLAY_PATH are set. data/examples.ts statically imports
* `@pro/data/proExamples`, so without this alias every test that loads
* examples.ts would explode with "Cannot find package '@pro/...'".
*/
const proOverlayPath =
process.env.VITE_PRO_BUILD && process.env.PRO_OVERLAY_PATH
? path.resolve(process.env.PRO_OVERLAY_PATH)
: path.resolve(__dirname, 'src/__pro_stub__');
export default defineConfig({
resolve: {
alias: {
'@velxio': path.resolve(__dirname, 'src'),
'@pro': proOverlayPath,
},
},
// Allow vitest to import test files / sources from outside this