User Onboarding
- Date:
2026-01-11
- Author:
Thermoquad
- Status:
Research Complete
Note
Development Status: Research Phase
This document captures research findings and recommendations. Implementation has not yet begun. The findings here will guide future development of user onboarding for Thermoquad heaters.
Executive Summary
This research investigates user onboarding for Thermoquad heaters, with the paramount goal: A user must be able to fully use a Thermoquad heater from their mobile phone or computer without an internet connection.
Key Findings:
WiFi AP mode with captive portal is a viable onboarding method for Block (Slate)
Zephyr provides WiFi AP mode, DHCP server, mDNS, and HTTP server capabilities
Open networks during setup are acceptable if a device password is required before operation
Web Bluetooth API enables PWA-to-BLE communication on Android/Chrome (not iOS Safari)
Roastee PWA can be served directly from Block and installed for offline use
BLE pairing should use LESC with application-layer authentication for security
Recommendations:
Use WiFi AP mode with captive portal for initial device setup
Require device password before any heater operations
Serve Roastee PWA directly from Block with service worker for offline support
Support optional BLE mode with time-limited pairing window
Use mDNS (
thermoquad-XXXX.local) for device discoveryOTA updates via Roastee (works with WiFi station mode, AP mode, or BLE)
System Architecture
Hardware Components
This research assumes the following hardware configuration:
Device |
Hardware |
Role |
|---|---|---|
Helios |
Hades (RP2350-based ICU) |
Burner control, Fusain slave |
Slate |
Block (RP2350 + RM2 module) |
WiFi/BT bridge, Fusain master |
Roastee |
User’s phone/computer |
PWA client interface |
Block Hardware Features:
Raspberry Pi Radio Module 2 (RM2) with CYW43439 [1]
WiFi 4 (802.11n, 2.4 GHz single-band)
Bluetooth 5.2 (Classic + BLE)
SoftAP support (up to 4 clients)
4 RGB status LEDs for user feedback
IP68 rated enclosure
RM2 Module Specifications:
Parameter |
Value |
|---|---|
WiFi Standard |
802.11b/g/n (WiFi 4) |
Frequency |
2.4 GHz single-band |
Max Data Rate |
96 Mbps (PHY rate) |
Bluetooth |
5.2 (Classic + LE) |
Power (Active RX) |
~43 mA at -50 dBm |
Power (Active TX) |
~271 mA at 16 dBm |
Power (Sleep) |
~1.19 mA (PM1 DTIM1) |
Dimensions |
14.5 x 16.7 x 2.6 mm |
Device Roles
In the Fusain protocol architecture:
Helios (ICU): Slave device, receives commands, sends telemetry
Slate (Block): Router/Controller, bridges user interfaces to ICU
Roastee (PWA): Client, user interaction and control
The Block acts as the central integration point, providing:
WiFi access point for initial setup
WiFi station mode for network integration
Bluetooth LE for direct device communication
WebSocket server for Roastee communication
Fusain bridge to Helios ICU
Onboarding Flow
Overview
┌─────────────────────────────────────────────────────────────────┐
│ USER ONBOARDING FLOW │
└─────────────────────────────────────────────────────────────────┘
1. POWER ON
├── Helios boots, connects to Slate via Fusain
├── Slate detects unconfigured state
└── Slate activates WiFi AP mode, LEDs indicate setup mode
2. WIFI CONNECT
├── User sees "Thermoquad-XXXX" network (open)
├── User connects from phone/laptop
└── Captive portal auto-opens (or user navigates to portal)
3. CAPTIVE PORTAL
├── Slate serves configuration UI
├── User sets device password (REQUIRED)
└── User configures WiFi (AP password OR join network)
4. NETWORK TRANSITION
├── Slate applies new WiFi settings
├── User reconnects to configured network
└── Slate reachable via mDNS: thermoquad-XXXX.local
5. PWA INSTALLATION
├── User visits http://thermoquad-XXXX.local
├── Roastee PWA served from Slate
├── User prompted to install PWA
└── PWA cached for offline use
6. AUTHENTICATION
├── Roastee prompts for device password
├── Password persisted in PWA settings
└── User can now control heater
7. OPTIONAL: BLE SETUP
├── User requests BLE pairing in Roastee settings
├── Slate enables BLE advertising for limited time
├── Roastee initiates Web Bluetooth pairing
└── Secure BLE connection established
Step 1: Power On
When a new Thermoquad heater is powered on:
Helios boots and initializes hardware (firmware defaults sufficient)
Slate boots and establishes Fusain connection with Helios
Slate checks persistent storage for configuration state
If unconfigured, Slate enters Setup Mode: - Activates WiFi in AP mode - SSID:
Thermoquad-XXXX(XXXX from RP2350 unique ID) - Open network (no password initially) - Starts DHCP server, DNS server, HTTP server - RGB LEDs display distinct “setup mode” pattern
LED Pattern for Setup Mode: (Example)
Slow blue pulse: WiFi AP active, awaiting connection
Fast blue pulse: Client connected, portal active
Green: Configuration complete
Red: Error state
Step 2: WiFi Connect
The user connects their phone or computer to the Thermoquad-XXXX network.
Why Open Network Initially?
The initial network is open (no password) because:
No pre-printed credentials on device (user requirement)
Eliminates need for default passwords like “admin”
Simpler onboarding for non-technical users
Security enforced at application layer (device password required)
Security Mitigation:
Captive portal only allows configuration, not heater operation
Device password MUST be set before any Fusain commands
Configuration session has timeout
LEDs indicate when device is in setup mode (physical awareness)
Step 3: Captive Portal
Modern devices automatically detect captive portals and display them. [2]
Detection Mechanism:
Operating systems probe specific URLs to detect captive portals:
Platform |
Detection URLs |
|---|---|
iOS/macOS |
|
Android |
|
Windows |
|
Firefox |
|
Slate Implementation:
Slate’s DNS server returns its own IP for ALL DNS queries, redirecting all HTTP traffic to the captive portal. When the device probes its detection URL:
DNS resolves to Slate’s IP (e.g., 192.168.4.1)
HTTP request redirected to configuration page
Device displays captive portal UI
DHCP Option 114 (Modern Standard):
For iOS 14+ and macOS Big Sur+, Slate can advertise captive portal presence via DHCP Option 114, providing a better user experience. [3]
Configuration Options:
The captive portal presents two mutually exclusive WiFi configurations:
Option A: Secure AP Mode
Keep SSID as
Thermoquad-XXXX(or allow customization)User MUST set a WiFi password (WPA2/WPA3)
Block continues as access point
Suitable for: Direct connection, no existing network
Option B: Station Mode (Join Network)
User selects from scanned networks
User enters network password
Optional: Set custom hostname (default:
thermoquad-XXXX)Block connects as client to existing network
Suitable for: Home/shop integration, multiple devices
Required for Both Options:
Device Password: User MUST set a password for Roastee authentication
This password protects the HTTP/WebSocket API
Stored securely in Slate’s persistent storage (NVS)
Step 4: Network Transition
After configuration:
Slate saves settings to persistent storage (NVS)
Slate applies new WiFi configuration
If AP mode: Restarts AP with password protection
If Station mode: Connects to configured network
Slate starts mDNS responder:
thermoquad-XXXX.localLEDs indicate “configured” state
User Reconnection:
AP mode: User reconnects with new WiFi password
Station mode: User’s device auto-reconnects to home network
Device Discovery:
Slate registers with mDNS, making it discoverable as:
thermoquad-XXXX.local(default)<custom-hostname>.local(if user configured)
Step 5: PWA Installation
User navigates to http://thermoquad-XXXX.local in their browser.
Roastee PWA Serving:
Slate’s HTTP server serves the Roastee PWA directly:
HTML, CSS, JavaScript (gzip compressed)
Web App Manifest (
manifest.json)Service Worker for offline caching
Static assets embedded in Slate firmware
PWA Requirements: [4]
For a PWA to be installable, it must have:
Valid Web App Manifest with required fields
Registered Service Worker with offline fallback
Served over HTTPS (or localhost/
.localfor development)
Note on HTTPS:
Local network PWAs present a challenge for HTTPS. Options:
Self-signed certificate: Works but requires user to accept warning
mDNS with ``.local``: Some browsers allow service workers on
.localHTTP for local: Chrome allows service workers on
localhostand local IPs
Recommendation: Use HTTP for local network operation. The lack of internet requirement means HTTPS certificate validation is not the primary security mechanism—the device password is.
Install Prompt:
On Android/Chrome, the beforeinstallprompt event allows custom install UI.
On iOS, users must manually “Add to Home Screen” from Safari share menu. [5]
Offline Capability:
Once installed, Roastee’s service worker caches:
App shell (HTML, CSS, JS)
Static assets (icons, fonts)
API responses (where applicable)
The PWA works offline, connecting to Slate when available.
Step 6: Authentication
When Roastee connects to Slate:
WebSocket connection established
Roastee prompts for device password (first time)
Password sent to Slate for verification
On success: Auth token returned, stored in PWA settings
Subsequent connections use stored token
Authentication Flow:
Roastee Slate
│ │
│──── WebSocket Connect ────────>│
│ │
│<─── Auth Required ─────────────│
│ │
│──── Password ─────────────────>│
│ │
│<─── Auth Token ────────────────│
│ │
│──── Commands (with token) ────>│
│ │
│<─── Telemetry ─────────────────│
Token Management:
Tokens are session-based or long-lived (configurable)
Password can be changed via authenticated API
Logout invalidates current token
Step 7: Optional BLE Setup
After WiFi setup, users can optionally enable BLE communication.
Why BLE as Secondary?
WiFi provides better range and throughput
WiFi works with any browser
BLE is useful for: Battery-powered Luna, reduced power consumption
Web Bluetooth API has limited browser support
BLE Pairing Security:
BLE pairing MUST be secure. The approach:
BLE advertising disabled by default
User requests BLE pairing in Roastee (over WiFi)
Slate enables BLE advertising for limited time (e.g., 60 seconds)
Roastee initiates Web Bluetooth pairing
LESC pairing with application-layer authentication
After timeout, BLE advertising disabled
Web Bluetooth API Support: [6]
Platform |
Support |
|---|---|
Chrome (Android) |
Full support |
Chrome (Windows/macOS) |
Full support |
Edge |
Full support |
Safari (iOS/macOS) |
Not supported |
Firefox |
Experimental (flag required) |
iOS Limitation:
Web Bluetooth is not available on iOS Safari. For iOS BLE support, options:
Use WiFi-only on iOS (recommended)
Third-party browser (Bluefy) with Web Bluetooth support
Native iOS app (outside PWA scope)
Recommendation: Design Roastee to gracefully degrade on iOS, using WiFi as the primary communication method.
Zephyr Implementation
WiFi AP Mode
Zephyr supports WiFi AP mode (SoftAP) with the wifi_mgmt API. [7]
Kconfig Options:
# WiFi support
CONFIG_WIFI=y
CONFIG_WIFI_NM=y
# AP mode
CONFIG_WIFI_NM_WPA_SUPPLICANT=y
CONFIG_NET_L2_WIFI_MGMT=y
# Networking
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_TCP=y
CONFIG_NET_UDP=y
AP Mode Initialization:
#include <zephyr/net/wifi_mgmt.h>
static struct wifi_connect_req_params ap_params = {
.ssid = "Thermoquad-XXXX",
.ssid_length = 16,
.channel = WIFI_CHANNEL_ANY,
.security = WIFI_SECURITY_TYPE_NONE, // Open initially
};
int start_ap_mode(void)
{
struct net_if *iface = net_if_get_default();
return net_mgmt(NET_REQUEST_WIFI_AP_ENABLE, iface,
&ap_params, sizeof(ap_params));
}
Note: The CYW43439 supports up to 4 simultaneous clients in AP mode.
DHCP Server
Zephyr includes a DHCPv4 server for AP mode. [8]
Kconfig Options:
CONFIG_NET_DHCPV4_SERVER=y
Configuration:
#include <zephyr/net/dhcpv4_server.h>
static struct in_addr base_addr = { .s_addr = htonl(0xC0A80402) }; // 192.168.4.2
int start_dhcp_server(void)
{
struct net_if *iface = net_if_get_default();
return net_dhcpv4_server_start(iface, &base_addr);
}
DHCP Option 114 (Captive Portal):
For modern captive portal detection, implement DHCP Option 114:
// Option 114: Captive Portal URI
// Value: "http://192.168.4.1/portal"
This tells iOS 14+ devices about the captive portal URL directly.
DNS Server
For captive portal functionality, Slate needs a DNS server that returns its own IP for all queries (DNS hijacking).
Note: Zephyr does not include a DNS server out-of-box. Implementation options:
Custom minimal DNS server: Respond to all A queries with AP IP
Port existing lightweight DNS: Adapt from lwIP or similar
Minimal DNS Server (Concept):
// Pseudo-code for captive portal DNS
void handle_dns_query(struct dns_query *query)
{
// Respond to ALL queries with our IP
struct dns_response response = {
.name = query->name,
.type = DNS_TYPE_A,
.class = DNS_CLASS_IN,
.ttl = 60,
.ip = CAPTIVE_PORTAL_IP, // e.g., 192.168.4.1
};
send_dns_response(&response);
}
mDNS Responder
Zephyr includes mDNS responder support. [9]
Kconfig Options:
CONFIG_MDNS_RESPONDER=y
CONFIG_NET_HOSTNAME="thermoquad-XXXX"
mDNS Registration:
The device automatically responds to <hostname>.local queries when mDNS
is enabled. Set CONFIG_NET_HOSTNAME to the desired name.
HTTP Server
Zephyr’s HTTP server library supports static files, dynamic content, and WebSockets. [10]
Kconfig Options:
CONFIG_HTTP_SERVER=y
CONFIG_HTTP_SERVER_WEBSOCKET=y
CONFIG_HTTP_SERVER_RESOURCE_WILDCARD=y
Serving PWA Static Files:
Static files can be embedded at compile time:
#include <zephyr/net/http/server.h>
// Embedded gzip-compressed files
extern const uint8_t index_html_gz[];
extern const size_t index_html_gz_len;
static struct http_resource_detail_static index_resource = {
.common = {
.type = HTTP_RESOURCE_TYPE_STATIC,
.content_type = "text/html",
.content_encoding = "gzip",
},
.static_data = index_html_gz,
.static_data_len = index_html_gz_len,
};
HTTP_RESOURCE_DEFINE(index, &index_resource, "/");
WebSocket Support:
For Roastee communication:
static int ws_handler(struct http_client_ctx *client,
enum http_data_status status,
const uint8_t *data, size_t len)
{
// Handle WebSocket messages (Fusain protocol)
return 0;
}
static struct http_resource_detail_websocket ws_resource = {
.common = { .type = HTTP_RESOURCE_TYPE_WEBSOCKET },
.cb = ws_handler,
};
HTTP_RESOURCE_DEFINE(ws, &ws_resource, "/ws");
Bluetooth LE
Zephyr provides comprehensive BLE support with security options. [11]
Kconfig Options:
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_SMP=y
CONFIG_BT_SMP_SC_ONLY=y # LE Secure Connections only
CONFIG_BT_BONDABLE=y
CONFIG_BT_SETTINGS=y # Persist bonding info
Security Configuration:
For secure BLE pairing with application-layer auth:
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
static struct bt_conn_auth_cb auth_cb = {
.passkey_display = NULL, // No display
.passkey_confirm = NULL, // No numeric comparison
.cancel = auth_cancel,
.pairing_complete = pairing_complete,
};
// Use "Just Works" pairing (no MITM) but require app-layer auth
// BLE connection is encrypted, app password provides authentication
Time-Limited Advertising:
static struct k_timer pairing_timer;
void enable_ble_pairing(void)
{
// Start advertising
bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);
// Start 60-second timeout
k_timer_start(&pairing_timer, K_SECONDS(60), K_NO_WAIT);
}
void pairing_timeout(struct k_timer *timer)
{
// Stop advertising
bt_le_adv_stop();
}
PWA Implementation
Roastee Architecture
Roastee is a Progressive Web App built with TypeScript.
Note
For detailed technology stack decisions, see Roastee Web Stack. That document covers framework selection, bundle size optimization, and build configuration based on measured experiment results.
Technology Stack:
Framework: Svelte 5 (smallest runtime, see Roastee Web Stack)
Build Tool: Vite (fast builds, optimal tree-shaking)
State Management: Nanostores (framework-agnostic, ~500 bytes)
CSS: UnoCSS with Tailwind preset (on-demand generation)
WebSocket: Native WebSocket API
Web Bluetooth: Native Web Bluetooth API
PWA: Manual service worker (smaller than Workbox)
Key Components:
roastee/
├── src/
│ ├── api/
│ │ ├── websocket.ts # WebSocket connection management
│ │ ├── bluetooth.ts # Web Bluetooth integration
│ │ └── fusain.ts # Fusain protocol encoding/decoding
│ ├── components/
│ │ ├── Dashboard.tsx # Main control interface
│ │ ├── Settings.tsx # Device/app settings
│ │ └── Setup.tsx # BLE pairing UI
│ ├── stores/
│ │ ├── device.ts # Device state (telemetry, config)
│ │ └── auth.ts # Authentication state
│ └── sw.ts # Service worker
├── public/
│ ├── manifest.json # Web App Manifest
│ └── icons/ # App icons
└── vite.config.ts
Web App Manifest
{
"name": "Roastee - Thermoquad Controller",
"short_name": "Roastee",
"description": "Control your Thermoquad heater",
"start_url": "/",
"display": "standalone",
"background_color": "#1a1a1a",
"theme_color": "#ff6b00",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Service Worker Strategy
Caching Strategy: [12]
Cache-First for app shell (HTML, CSS, JS, icons)
Network-First for API data (telemetry, device state)
Stale-While-Revalidate for PWA updates
// Service worker registration
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered'))
.catch(err => console.error('SW registration failed', err));
}
// sw.ts - Cache app shell on install
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('roastee-v1').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/app.js',
'/app.css',
'/manifest.json',
]);
})
);
});
Web Bluetooth Integration
// bluetooth.ts
const THERMOQUAD_SERVICE_UUID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
const FUSAIN_CHAR_UUID = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy';
export async function connectBLE(): Promise<BluetoothDevice> {
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [THERMOQUAD_SERVICE_UUID] }],
});
const server = await device.gatt?.connect();
const service = await server?.getPrimaryService(THERMOQUAD_SERVICE_UUID);
const characteristic = await service?.getCharacteristic(FUSAIN_CHAR_UUID);
// Subscribe to notifications for telemetry
await characteristic?.startNotifications();
characteristic?.addEventListener('characteristicvaluechanged', handleData);
return device;
}
function handleData(event: Event) {
const value = (event.target as BluetoothRemoteGATTCharacteristic).value;
// Decode Fusain packet from value.buffer
}
BLE-to-WiFi Transition:
When user requests BLE pairing from Roastee:
Roastee sends
ENABLE_BLE_PAIRINGcommand over WiFiSlate starts BLE advertising with timeout
Roastee initiates Web Bluetooth device request
User selects device from browser prompt
GATT connection established
Roastee can now communicate over BLE
Security Considerations
Open Network Risk Assessment
Risk: Malicious actor connects during setup
Mitigations:
Physical proximity required: Must be near heater to see LEDs
No heater control: Captive portal only allows configuration
Device password required: Must set password to enable control
Session timeout: Setup mode times out after inactivity
Visual indicator: LEDs show when device is in setup mode
Residual Risk: Acceptable for consumer IoT device
Alternative: Wi-Fi Easy Connect (DPP)
Wi-Fi Alliance’s Device Provisioning Protocol (DPP) provides secure onboarding via QR code scanning. [13]
Pros:
Cryptographically secure (ECDH key exchange)
No open network phase
Industry standard
Cons:
Requires QR code on device (violates “no markings” requirement)
Limited device support
More complex implementation
Recommendation: DPP is not suitable for this use case due to the requirement for no device markings. The open network with mandatory device password approach is acceptable.
BLE Security
Pairing Mode: LE Secure Connections (LESC) with “Just Works”
Why “Just Works”?
Block has no display or input (cannot show passkey)
Numeric comparison requires display on both devices
Application-layer password provides authentication
Security Layers:
BLE Encryption: LESC provides encrypted link
Application Auth: Device password required for commands
Time-Limited Pairing: Advertising only enabled on user request
MITM Considerations:
“Just Works” is vulnerable to MITM during pairing. Mitigations:
Pairing only enabled by authenticated user (over WiFi)
Short pairing window (60 seconds)
Physical proximity required
LED indication during pairing mode
Factory Reset and Password Recovery
Users may forget their device password. Factory reset provides recovery.
See RP2350 Flash Usage for detailed NVS implementation.
Reset Methods by Device:
Device |
Method |
Notes |
|---|---|---|
Block (Slate) |
Button hold during boot |
Hold 10+ seconds, LED confirms reset |
Block (Slate) |
Roastee command |
Requires current authentication |
Hades (Helios) |
Jumper pads |
Short during boot (service access required) |
Hades (Helios) |
Fusain command |
Via authenticated Slate |
Block Factory Reset Flow:
Power off Block
Hold reset button
Power on while holding button
LEDs show reset pattern (e.g., rapid red blink)
After 10 seconds, LEDs turn solid (reset triggered)
Release button
Block reboots into setup mode (open AP, no password)
Hades Factory Reset Flow:
Hades resides inside the heater enclosure with no external buttons. Two reset options are available:
Option 1: Jumper Pads (Hardware)
For service technicians or advanced users:
Disconnect heater from power
Open heater enclosure to access Hades PCB
Locate factory reset pads (labeled RST/GND, 2.54mm spacing)
Optionally connect to UART0 (shell/logging port) for confirmation
Place jumper or short pads with tweezers
Power on heater while pads are shorted
Confirm reset via UART0 log:
No persisted configuration found, using defaultsRemove jumper
Close enclosure
UART0 Confirmation:
Connecting to Hades’ UART0 port provides direct confirmation of reset success without needing the full system operational. This is useful for bench testing or when Slate is not available.
Option 2: Remote Reset via Slate (Software)
If Slate is still accessible:
Open Roastee, authenticate with Slate
Navigate to Settings → Advanced → Reset Helios
Confirm reset action
Slate sends
FACTORY_RESETcommand via FusainHelios clears configuration and reboots
Helios reconnects with firmware defaults
What Gets Reset:
Device password (Block only - Helios has no password)
WiFi configuration
Custom hostname
BLE bonding information
User preferences
What Gets Preserved:
Firmware
Hardware calibration data (if any)
Security Consideration:
Factory reset on Block returns the device to open AP mode, allowing anyone nearby to reconfigure it. This is acceptable because:
Physical access to power cycle is required
Heater cannot operate without reconfiguration
New owner can set their own password
Roastee Handling:
When Roastee detects a factory-reset device:
Stored auth token becomes invalid
Roastee prompts: “Device was reset. Set new password.”
User completes setup flow again
New credentials stored in Roastee
OTA Update Considerations
OTA updates require internet connectivity. This section addresses how the onboarding architecture supports firmware updates for both Slate and Helios.
See RP2350 Flash Usage for detailed flash storage and MCUboot analysis.
Internet Connectivity Scenarios
Scenario A: WiFi Station Mode (Internet-Connected Network)
If the user configured Slate in station mode and connected to an internet-accessible network:
Roastee checks for updates via internet
Roastee notifies user of available update
User initiates update in Roastee
Slate downloads firmware directly from update server
Firmware written to
slot1_partitionvia MCUboot
This is the simplest OTA scenario.
Scenario B: WiFi AP Mode (No Internet)
If the user configured Slate in AP mode (isolated network):
User opens Roastee on an internet-connected device
Roastee downloads firmware update to device storage
User connects to Slate’s AP network
Roastee transfers firmware to Slate over WebSocket
Slate writes firmware to
slot1_partition
Scenario C: BLE with Internet-Connected Device
If user prefers BLE communication:
User opens Roastee on internet-connected phone
Roastee downloads firmware update
Roastee transfers firmware to Slate over BLE
Slate writes firmware to
slot1_partition
This scenario works even if Slate has no WiFi connectivity.
Firmware Storage Architecture
Based on the RP2350 Flash Usage research:
Self-Update (Slate updating itself):
Firmware written directly to
slot1_partitionNo filesystem needed (NVS/littlefs not used for firmware)
MCUboot handles validation and slot swap on reboot
Proxy Update (Slate updating Helios):
Slate receives Helios firmware over WiFi or BLE
Slate buffers firmware in littlefs:
/lfs/helios_update.binSlate transfers firmware to Helios over Fusain in chunks
Helios writes chunks to its
slot1_partition
Update Type |
Storage Location |
Size Required |
|---|---|---|
Slate self-update |
slot1_partition |
~400-500 KB (current firmware size) |
Helios proxy buffer |
littlefs on Slate |
~150 KB (Helios firmware + margin) |
Update metadata |
NVS |
< 1 KB (version, status flags) |
OTA via Roastee PWA
Roastee acts as the update orchestrator:
Update Check Flow:
Roastee Internet Slate
│ │ │
│── Check for updates ────>│ │
│<── Update manifest ──────│ │
│ │ │
│── Download firmware ────>│ │
│<── Firmware binary ──────│ │
│ │ │
│── Transfer firmware ───────────────────────────────>│
│ │ │
│<── Update status ──────────────────────────────────│
Firmware Transfer Protocol:
For transferring firmware from Roastee to Slate:
WebSocket (WiFi): Chunked binary transfer with acknowledgment
BLE (GATT): MTU-sized chunks with flow control
Chunk Size Recommendations:
WebSocket: 4-16 KB chunks (balance between overhead and memory)
BLE: 512 bytes (after MTU negotiation, typically 517 byte MTU)
Transfer Integrity:
Each chunk includes sequence number and CRC
Slate validates complete image before committing
Resume support for interrupted transfers
Roastee Offline Update Cache
Since Roastee is a PWA with offline capability:
User checks for updates while online
Roastee downloads and caches firmware in IndexedDB
User can later apply update even without internet
Useful for: Field updates, intermittent connectivity
Implementation:
// Cache firmware update in IndexedDB
async function cacheFirmwareUpdate(
deviceType: 'slate' | 'helios',
version: string,
firmware: ArrayBuffer
) {
const db = await openDB('roastee-updates', 1);
await db.put('firmware', {
deviceType,
version,
firmware,
downloadedAt: Date.now(),
});
}
// Check for cached update
async function getCachedUpdate(deviceType: string): Promise<ArrayBuffer | null> {
const db = await openDB('roastee-updates', 1);
const cached = await db.get('firmware', deviceType);
return cached?.firmware ?? null;
}
Configuration Persistence for OTA
User configuration must survive firmware updates:
Preserved Across Updates:
Device password (NVS)
WiFi configuration (NVS)
Custom hostname (NVS)
BLE bonding information (NVS)
User preferences (NVS)
MCUboot Guarantee:
MCUboot’s slot-based architecture preserves the storage_partition
(NVS/littlefs) across firmware updates. Only the code partition is modified.
First-Boot After Update:
MCUboot validates new firmware in slot0
Slate boots with new firmware
NVS configuration loaded (unchanged)
User session continues without re-authentication
Update Server Considerations
For production deployment, Roastee needs an update server:
Minimal Requirements:
HTTPS endpoint serving firmware manifests
Version information (semantic versioning)
Firmware binaries (signed for verification)
Device compatibility metadata
Example Manifest:
{
"devices": {
"slate": {
"latest": "1.2.0",
"url": "https://updates.thermoquad.com/slate/1.2.0.bin",
"sha256": "abc123...",
"releaseNotes": "Bug fixes and improvements"
},
"helios": {
"latest": "1.1.5",
"url": "https://updates.thermoquad.com/helios/1.1.5.bin",
"sha256": "def456...",
"releaseNotes": "Temperature control improvements"
}
}
}
Offline-First Consideration:
The update server is only needed for checking/downloading updates. Once firmware is cached in Roastee, updates can be applied without internet.
Feasibility Assessment
Component Feasibility
Component |
Status |
Notes |
|---|---|---|
WiFi AP Mode |
Feasible |
CYW43439 supports SoftAP, Zephyr has AP mode support |
DHCP Server |
Feasible |
Zephyr includes DHCPv4 server |
DNS Server |
Custom Required |
Minimal DNS server needed for captive portal |
mDNS Responder |
Feasible |
Zephyr includes mDNS responder |
HTTP Server |
Feasible |
Zephyr HTTP server supports static files, WebSocket |
Captive Portal |
Feasible |
Combine DNS hijack + HTTP redirect |
PWA Hosting |
Feasible |
Embed compressed assets in firmware |
BLE Peripheral |
Feasible |
Zephyr BLE stack fully supports peripheral role |
Web Bluetooth |
Partial |
Works on Android/Chrome, NOT iOS Safari |
OTA Updates |
Feasible |
MCUboot + Roastee orchestration; works via WiFi or BLE |
Platform Support Matrix
Platform |
WiFi |
BLE |
Notes |
|---|---|---|---|
Android (Chrome) |
Full |
Full |
Best experience |
Windows (Chrome) |
Full |
Full |
Desktop support |
macOS (Chrome) |
Full |
Full |
Desktop support |
iOS (Safari) |
Full |
None |
No Web Bluetooth; WiFi-only |
iOS (Bluefy) |
Full |
Full |
Third-party browser workaround |
Linux (Chrome) |
Full |
Flag |
Requires experimental flag |
Known Limitations
iOS BLE: Web Bluetooth not supported in Safari
HTTPS: Local network PWAs must handle certificate challenges
DNS Server: Custom implementation required for Zephyr
Flash Size: PWA assets add to firmware size (~100-500KB compressed)
Concurrent Connections: CYW43439 limited to 4 AP clients
Resource Estimates
Flash Usage (Block/Slate):
Component |
Estimated Size |
Notes |
|---|---|---|
Base Slate firmware |
~400 KB |
Current size |
WiFi/BLE stacks |
~100 KB |
May already be included |
HTTP server |
~20 KB |
Zephyr HTTP library |
Roastee PWA (gzip) |
~65 KB typical, 150 KB max |
See Roastee Web Stack for breakdown |
Total |
~585 KB typical, 770 KB max |
Fits in 4 MB flash |
RAM Usage:
Component |
Estimated Size |
Notes |
|---|---|---|
WiFi buffers |
~20 KB |
TX/RX buffers |
HTTP server |
~10 KB |
Connection state |
WebSocket |
~4 KB per client |
Per-connection |
BLE |
~10 KB |
Stack + buffers |
Implementation Roadmap
Phase 1: Core Infrastructure
Implement WiFi AP mode on Block
Implement DHCP server
Implement minimal DNS server (captive portal)
Implement HTTP server with static file serving
Test captive portal detection on iOS/Android/Windows
Phase 2: Configuration Portal
Design configuration UI (HTML/CSS/JS)
Implement network scanning API
Implement configuration persistence (NVS)
Implement network transition logic
Implement mDNS registration
Phase 3: Roastee PWA
Set up TypeScript project with Vite
Implement WebSocket client
Implement Fusain protocol encoder/decoder
Implement service worker for offline support
Test PWA installation on multiple platforms
Phase 4: Authentication
Implement device password storage
Implement authentication API
Implement token-based session management
Integrate authentication into Roastee
Phase 5: BLE Integration
Implement BLE GATT service on Block
Implement time-limited advertising
Implement Web Bluetooth client in Roastee
Test BLE pairing and communication
Implement WiFi-to-BLE transition flow
Phase 6: OTA Updates
Integrate MCUboot into Slate and Helios builds
Implement firmware transfer API (WebSocket + BLE)
Implement chunked transfer with integrity checks
Implement Roastee update checking and caching (IndexedDB)
Implement proxy update flow (Slate → Helios via Fusain)
Test all three OTA scenarios (WiFi station, WiFi AP, BLE)
References
Additional Resources:
Web Bluetooth API Documentation: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API
BLE Pairing and Security: https://technotes.kynetics.com/2018/BLE_Pairing_and_bonding/
Appendix A: SSID Generation
Generate unique SSID from RP2350’s unique ID:
#include <zephyr/drivers/hwinfo.h>
void generate_ssid(char *ssid, size_t len)
{
uint8_t device_id[8];
ssize_t id_len = hwinfo_get_device_id(device_id, sizeof(device_id));
if (id_len > 0) {
// Use last 4 bytes for hex suffix
snprintf(ssid, len, "Thermoquad-%02X%02X%02X%02X",
device_id[id_len-4], device_id[id_len-3],
device_id[id_len-2], device_id[id_len-1]);
} else {
strncpy(ssid, "Thermoquad-0000", len);
}
}
Appendix B: Captive Portal Response
Example captive portal redirect response:
HTTP/1.1 302 Found
Location: http://192.168.4.1/setup
Content-Length: 0
Connection: close
For Apple devices expecting specific content:
<!-- Return this instead of "Success" to trigger portal -->
<html>
<head>
<meta http-equiv="refresh" content="0;url=http://192.168.4.1/setup">
</head>
<body>
<a href="http://192.168.4.1/setup">Click here to configure</a>
</body>
</html>
Appendix C: Web Bluetooth GATT Service
Proposed GATT service structure for Fusain over BLE:
Thermoquad Service (UUID: TBD)
├── Fusain TX Characteristic (UUID: TBD)
│ ├── Properties: Write, Write Without Response
│ └── Purpose: Send Fusain packets to device
├── Fusain RX Characteristic (UUID: TBD)
│ ├── Properties: Notify
│ └── Purpose: Receive Fusain packets from device
└── Device Info Characteristic (UUID: TBD)
├── Properties: Read
└── Purpose: Device name, firmware version
MTU Considerations:
Default BLE MTU is 23 bytes (20 bytes payload). For larger Fusain packets:
Request MTU increase (up to 512 bytes)
Implement packet fragmentation if needed
Web Bluetooth handles MTU negotiation automatically