What it creates
The tool prepares a reusable Pico W “stem cell”: a board with MicroPython, your shared web-server module, and a boot program. A later application can give it a specific purpose.
boot.py, microdot.py, the registry API, or the database schema. Device-side networking and registration are therefore intended behaviour described by the scripts, rather than verified implementation.The provisioning flow
Connect in BOOTSEL mode. Copy flash_nuke.uf2 to erase existing flash, if selected.
Connect in BOOTSEL mode again. Copy MicroPython.uf2 to the USB boot drive.
Wait for /dev/ttyACM0, then check access with mpremote.
Enter SSID and password, or leave SSID blank to request setup access-point mode.
Copy module, optional Wi-Fi configuration, then boot program. Reset the Pico.
Poll MariaDB for the latest device row. The current check does not identify this particular Pico.
How to run it
Host requirements
The scripts expect a Linux host with Bash, sudo, lsblk, mount utilities, mpremote, a MySQL-compatible command-line client, and md5sum. The user needs USB serial access and permission to mount the Pico boot drive. A USB data cable is required.
The expected project location is ~/iot-device. UF2 files and provisioning programs must already be present there. The host must be able to reach the database for the final check.
bash ~/iot-device/iot-maker-pico.sh- At the wipe prompt, press Enter for the default wipe, or enter
nto skip. Only a singlenorNskips it. - When requested, hold BOOTSEL while connecting the Pico, then press Enter in the terminal.
- If wiping, unplug the Pico when instructed; reconnect with BOOTSEL for the MicroPython flash.
- After flashing, allow the board to reboot and expose its MicroPython USB serial interface.
- Enter Wi-Fi details. Password input is hidden. A blank SSID skips creation of
wifi.json. - After files are copied and the board resets, enter the database password for the registration check. The script offers Ctrl+C to stop waiting.
If Wi-Fi is skipped, the script advertises PicoSetup with password 12345678. Confirm those settings in boot.py. When a wipe is skipped, an old wifi.json may remain on the device, so leaving SSID blank does not guarantee AP mode.
What each script does
| Script | Work performed | Wait / result |
|---|---|---|
iot-maker-pico.sh | Orchestrates sub-scripts; asks whether to wipe; gathers Wi-Fi credentials; creates and removes the temporary configuration. | Uses set -e to stop on unhandled failures. |
nuke-pico.sh | Checks for nuke UF2; finds a 128M block device; clears stale mounts; mounts its first partition; copies the UF2; asks for unplugging. | Up to 20 detection attempts, about 1 second apart; fixed 5-second and 3-second pauses. |
flash-pico.sh | Uses the same detection and mounting approach, then copies MicroPython UF2. | Up to 20 detection attempts; fixed 5-second and 3-second pauses. |
wait-pico.sh | Watches specifically for /dev/ttyACM0; pauses; executes mpremote ls :. | Up to 40 serial checks, about 1 second apart, then 5 seconds to settle. One communication check. |
copy-files-pico.sh | Copies files in one mpremote command chain, writes boot.py last, then requests reset. | Errors are suppressed and ignored. Prints the host boot file's MD5. |
register-pico.sh | Asks for DB password and queries the most recently registered device. | Up to 30 queries, with 2-second pauses when no row is returned. Query runtime adds to the wait. |
Although the orchestrator comment lists five sub-scripts, its displayed step numbers run from 1 to 7. Wi-Fi is step 4; reboot is displayed as step 6; the registry check is step 7.
Files and connections
| Location | Purpose |
|---|---|
~/iot-device/scripts/ | Five helper shell scripts. |
~/iot-device/provision/pico_w/flash_nuke.uf2 | Optional flash-erasing firmware. |
~/iot-device/provision/pico_w/MicroPython.uf2 | MicroPython image; version is not established by the supplied code. |
~/iot-device/provision/pico_w/boot.py | Device boot program copied to :boot.py. |
~/iot-device/provision/common/microdot.py | Shared module copied to :microdot.py. |
/tmp/wifi.XXXXXX.json | Temporary host file containing SSID and password; copied to :wifi.json, then removed on the normal success path. |
/media/$USER/RPI-RP2 | Host mount point for the Pico boot drive. |
/dev/ttyACM0 | Serial device watched by the readiness script. mpremote itself uses automatic device selection. |
Registry connection
Host: 192.168.100.51 · user: rforssen · database: iot_registry · table: devices.
SELECT device_id, last_seen
FROM devices
ORDER BY registered_at DESC
LIMIT 1;Query the registry with one SSH command
Run the following in Windows PowerShell. It connects to the Raspberry Pi and runs the query against MariaDB on the NAS:
ssh -t rforssen@raspberrypi "mysql -h 192.168.100.51 -u rforssen -p iot_registry -e 'SELECT device_id, last_seen FROM devices ORDER BY registered_at DESC LIMIT 1;'"Replace raspberrypi with the Raspberry Pi's IP address or SSH hostname if that name does not resolve on your PC.
- Authenticate to SSH using your usual key or the Raspberry Pi account password.
- At MySQL's
Enter password:prompt, enter the database password forrforssen. This may differ from the SSH password. - The query prints
device_idandlast_seenfor the most recently registered device, then SSH exits. If the table is empty, no device row is returned.
-t requests a terminal for the interactive password prompt. MySQL's -p asks for the password instead of embedding it in the command; -e executes the SQL and exits. The Raspberry Pi needs the MySQL-compatible client installed and network access to 192.168.100.51.
registered_at, not last_seen.The host scripts only read the registry. They do not insert a device row. Whatever registers the Pico must be implemented elsewhere, presumably in the device software and its backend.
Known limitations and useful improvements
These findings follow from the supplied source; the scripts have not been executed against hardware.
| Finding | Practical consequence | Improvement |
|---|---|---|
| Registry check accepts any latest row. | An existing device can immediately produce “Device provisioned,” even if this Pico never registers. last_seen is displayed but not checked. | Read this Pico's identity and wait for a matching registration or heartbeat after provisioning began. |
Copy command ends with 2>/dev/null || true. | Transfer and reset failures are hidden; success messages are unconditional. | Check file transfer separately, retain error output, and handle expected reset disconnection explicitly. |
USB disk detection searches for 128M. | It can select an unrelated device; size display and partition layout are assumed. | Identify the Pico by USB properties and filesystem label before copying firmware. |
| Fixed serial name; automatic mpremote selection. | A Pico on ttyACM1 will not pass the wait; another serial device may be selected by mpremote. | Discover and consistently pass the intended serial port. |
Wi-Fi JSON is built with echo. | Quotes or backslashes in credentials can produce invalid or unintended JSON. | Use a JSON serializer and read -r. |
| No temporary-file cleanup trap. | A failure or interruption before normal cleanup can leave the Wi-Fi password on the host. | Install an EXIT trap as soon as the temporary file is created. |
| Many shell variables are unquoted. | Spaces and wildcard characters can alter arguments, including DB password handling. | Quote expansions; pass DB credentials through an appropriate protected client configuration. |
| Database errors are hidden. | Bad credentials or an unreachable database appear as “not seen in DB yet.” Client network waits are not bounded by the loop alone. | Report connection failures separately and configure a connection timeout. |
| MD5 is calculated only on the host. | It identifies the source boot file; it does not prove that the Pico received that file. | Read back or hash device files to verify the installed contents. |
| UF2 copy success is treated as flash success. | No direct firmware verification follows the copy. Later serial readiness is the practical runtime check. | Verify the board and running MicroPython version after reboot. |
The scripts deliberately avoid sync or explicit unmount after copying UF2, relying on the board to accept the image and disconnect. The comments explain the author's experience; this document does not independently validate that rationale.
Troubleshooting
| Symptom | What to inspect |
|---|---|
| UF2 file not found | Check the required firmware files under ~/iot-device/provision/pico_w. |
| “Device not found” | Reconnect while holding BOOTSEL; use a data cable; inspect lsblk. Current detection requires a displayed size containing 128M. |
| “ttyACM0 never appeared” | Inspect dmesg | tail -20 and USB serial nodes. Check whether the board became ttyACM1. |
| Serial present but mpremote fails | Try mpremote ls : manually; check serial permissions and whether another program has the port open. |
| “Copied” but device does not boot correctly | Run transfers with visible errors and inspect the device files; current success messages are not verification. |
| Device does not join Wi-Fi | Inspect the actual wifi.json and boot.py; consider escaping issues or a retained old configuration. |
| No registry row appears | Check database connectivity, credentials, device networking, and the device-side registration code. |
| Registry reports success unexpectedly quickly | Compare the returned ID with the intended Pico. An older device row can satisfy the current query. |
Source reference
The following expandable sections preserve the shell scripts from the supplied terminal transcript. They are reference material, not corrected versions. The terminal's directory listings and prompts are omitted.
iot-maker-pico.sh
#!/bin/bash
# iot-maker-pico.sh — orchestrator for Pico W stem cell provisioning
# ─────────────────────────────────────────
# 20260417 Refactored into sub-scripts
# 20260416 Initial version
# ─────────────────────────────────────────
#
# Usage: ./iot-maker-pico.sh
#
# Calls in order:
# scripts/nuke-pico.sh (optional)
# scripts/flash-pico.sh
# scripts/wait-pico.sh
# scripts/copy-files-pico.sh
# scripts/register-pico.sh
set -e
SCRIPTS=~/iot-device/scripts
echo ""
echo "╔══════════════════════════════════════╗"
echo "║ IoT Device Maker ║"
echo "║ Pico W stem cell ║"
echo "╚══════════════════════════════════════╝"
echo ""
# ── Step 1: Nuke? ─────────────────────────────────────────────
echo "► Step 1: Prepare device"
echo " Nuking is recommended — it ensures a clean flash."
read -p " Nuke existing firmware first? (Y/n): " DO_NUKE
echo ""
if [[ ! "$DO_NUKE" =~ ^[Nn]$ ]]; then
bash $SCRIPTS/nuke-pico.sh
else
echo " ⚠️ Skipping nuke — flash may fail on non-fresh devices"
echo ""
fi
# ── Step 2: Flash MicroPython ─────────────────────────────────
bash $SCRIPTS/flash-pico.sh
# ── Step 3: Wait for MicroPython ─────────────────────────────
bash $SCRIPTS/wait-pico.sh
# ── Step 4: Ask WiFi credentials ─────────────────────────────
echo ""
echo "► Step 4: WiFi credentials"
echo " (leave SSID blank to skip — device will start AP mode instead)"
echo ""
read -p " SSID: " SSID
TMP_WIFI=""
if [ -n "$SSID" ]; then
read -s -p " Password: " PASSWORD
echo ""
TMP_WIFI=$(mktemp /tmp/wifi.XXXXXX.json)
echo "{\"ssid\": \"$SSID\", \"password\": \"$PASSWORD\"}" > $TMP_WIFI
export TMP_WIFI
else
echo " ⚠️ Skipped — device will start AP mode (PicoSetup / 12345678)"
fi
# ── Step 5: Copy files ────────────────────────────────────────
bash $SCRIPTS/copy-files-pico.sh "$TMP_WIFI"
[ -n "$TMP_WIFI" ] && rm -f $TMP_WIFI
# ── Step 6-7: Wait for registration ──────────────────────────
bash $SCRIPTS/register-pico.shnuke-pico.sh
#!/bin/bash
# nuke-pico.sh — wipe Pico W flash before provisioning
# ─────────────────────────────────────────
# 20260417 Initial version
# ─────────────────────────────────────────
#
# Usage: ./scripts/nuke-pico.sh
# or called from iot-maker-pico.sh
set -e
PICO=~/iot-device/provision/pico_w
MOUNT=/media/$USER/RPI-RP2
if [ ! -f "$PICO/flash_nuke.uf2" ]; then
echo " ❌ flash_nuke.uf2 not found: $PICO/flash_nuke.uf2"
exit 1
fi
echo "► Nuke: wipe existing firmware"
echo " Hold BOOTSEL, plug in the device, then press ENTER"
read
echo " Waiting for RPI-RP2..."
for i in $(seq 1 20); do
DEV=$(lsblk -o NAME,SIZE | grep "128M" | awk '{print $1}' | head -1)
if [ -n "$DEV" ]; then
echo " Found: /dev/$DEV"
break
fi
sleep 1
done
if [ -z "$DEV" ]; then
echo " ❌ Device not found — did you hold BOOTSEL?"
exit 1
fi
sudo mkdir -p $MOUNT
# Clean up any stale mounts from previous attempts
echo " Cleaning up stale mounts..."
sudo umount -l $MOUNT 2>/dev/null || true
sudo umount -l $MOUNT 2>/dev/null || true
sudo umount -l $MOUNT 2>/dev/null || true
sudo umount -l $MOUNT 2>/dev/null || true
sudo mount /dev/${DEV}1 $MOUNT
echo " Waiting for filesystem to initialise..."
sleep 5
echo " Flashing flash_nuke.uf2..."
sudo cp $PICO/flash_nuke.uf2 $MOUNT/
# Do NOT sync or umount — let the Pico accept and disconnect naturally
echo " ✅ Nuked — device rebooting"
sleep 3
echo ""
echo " Unplug the device, then press ENTER"
read
echo ""flash-pico.sh
#!/bin/bash
# flash-pico.sh — flash MicroPython UF2 onto Pico W
# ─────────────────────────────────────────
# 20260417 Initial version
# ─────────────────────────────────────────
#
# Usage: ./scripts/flash-pico.sh
# or called from iot-maker-pico.sh
set -e
PICO=~/iot-device/provision/pico_w
MOUNT=/media/$USER/RPI-RP2
if [ ! -f "$PICO/MicroPython.uf2" ]; then
echo "❌ MicroPython.uf2 not found: $PICO/MicroPython.uf2"
exit 1
fi
echo "► Step 2: Flash MicroPython"
echo " Hold BOOTSEL, plug in the device, then press ENTER"
read
echo " Waiting for RPI-RP2..."
for i in $(seq 1 20); do
DEV=$(lsblk -o NAME,SIZE | grep "128M" | awk '{print $1}' | head -1)
if [ -n "$DEV" ]; then
echo " Found: /dev/$DEV"
break
fi
sleep 1
done
if [ -z "$DEV" ]; then
echo " ❌ Device not found — did you hold BOOTSEL?"
exit 1
fi
sudo mkdir -p $MOUNT
# Clean up any stale mounts from previous attempts
echo " Cleaning up stale mounts..."
sudo umount -l $MOUNT 2>/dev/null || true
sudo umount -l $MOUNT 2>/dev/null || true
sudo umount -l $MOUNT 2>/dev/null || true
sudo umount -l $MOUNT 2>/dev/null || true
sudo mount /dev/${DEV}1 $MOUNT
echo " Waiting for filesystem to initialise..."
sleep 5
echo " Flashing MicroPython.uf2..."
sudo cp $PICO/MicroPython.uf2 $MOUNT/
# Do NOT sync or umount — let the Pico accept the UF2 and disconnect naturally
# Forcing umount causes incomplete writes and boot loops
echo " ✅ MicroPython flashed — waiting for device to reboot..."
sleep 3wait-pico.sh
#!/bin/bash
# wait-pico.sh — wait for Pico W to appear as MicroPython serial device (ttyACM0)
# ─────────────────────────────────────────
# 20260417 Watch for ttyACM0 specifically before attempting mpremote
# ─────────────────────────────────────────
#
# Usage: ./scripts/wait-pico.sh
# or called from iot-maker-pico.sh
set -e
echo "► Step 3: Waiting for MicroPython..."
echo " (watching for ttyACM0)"
# Step 1: Wait for ttyACM0 to appear
ACM_FOUND=0
for i in $(seq 1 40); do
if [ -e /dev/ttyACM0 ]; then
echo " ✅ ttyACM0 detected"
ACM_FOUND=1
break
fi
echo " ... waiting for ttyACM0 ($i/40)"
sleep 1
done
if [ $ACM_FOUND -eq 0 ]; then
echo " ❌ ttyACM0 never appeared"
echo " Tip: check 'dmesg | tail -20' to see if device is detected"
exit 1
fi
# Step 2: Give MicroPython a moment to settle, then verify with mpremote
sleep 5
if mpremote ls : > /dev/null 2>&1; then
echo " ✅ Device ready"
exit 0
else
echo " ❌ ttyACM0 present but mpremote not responding"
echo " Tip: try 'mpremote ls :' manually"
exit 1
ficopy-files-pico.sh
#!/bin/bash
# copy-files-pico.sh — copy provisioning files to Pico W in one mpremote session
# ─────────────────────────────────────────
# 20260417 Initial version
# ─────────────────────────────────────────
#
# Usage: ./scripts/copy-files-pico.sh [/tmp/wifi.json]
# or called from iot-maker-pico.sh
#
# boot.py is copied LAST so it doesn't run during the session.
# Reset is included at the end of the mpremote chain.
set -e
PICO=~/iot-device/provision/pico_w
COMMON=~/iot-device/provision/common
TMP_WIFI=$1
if [ ! -f "$PICO/boot.py" ]; then
echo "❌ boot.py not found: $PICO/boot.py"
exit 1
fi
if [ ! -f "$COMMON/microdot.py" ]; then
echo "❌ microdot.py not found: $COMMON/microdot.py"
exit 1
fi
echo "► Step 5: Copying files..."
if [ -n "$TMP_WIFI" ] && [ -f "$TMP_WIFI" ]; then
mpremote \
cp $COMMON/microdot.py :microdot.py + \
cp $TMP_WIFI :wifi.json + \
cp $PICO/boot.py :boot.py + \
reset 2>/dev/null || true
echo " ✅ microdot.py copied"
echo " ✅ wifi.json copied"
else
mpremote \
cp $COMMON/microdot.py :microdot.py + \
cp $PICO/boot.py :boot.py + \
reset 2>/dev/null || true
echo " ✅ microdot.py copied"
echo " ⚠️ wifi.json skipped — device will start AP mode"
fi
echo " ✅ boot.py copied (last)"
echo " MD5: $(md5sum $PICO/boot.py | awk '{print $1}')"
echo ""
echo "► Step 6: Device rebooting..."
echo " ✅ Reset sent"register-pico.sh
#!/bin/bash
# register-pico.sh — wait for newly provisioned Pico W to appear in database
# ─────────────────────────────────────────
# 20260417 Initial version
# ─────────────────────────────────────────
#
# Usage: ./scripts/register-pico.sh
# or called from iot-maker-pico.sh
#
# Press Ctrl+C to skip if you don't want to wait.
DB_HOST=192.168.100.51
DB_USER=rforssen
DB_NAME=iot_registry
echo ""
echo "► Step 7: Waiting for device to register..."
echo " (press Ctrl+C to skip)"
echo ""
read -s -p " DB password: " DB_PASS
echo ""
REGISTERED=""
for i in $(seq 1 30); do
RESULT=$(mysql -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME \
-se "SELECT device_id, last_seen FROM devices ORDER BY registered_at DESC LIMIT 1;" 2>/dev/null)
if [ -n "$RESULT" ]; then
REGISTERED=$RESULT
break
fi
echo " ... waiting ($i/30)"
sleep 2
done
echo ""
if [ -n "$REGISTERED" ]; then
echo "╔══════════════════════════════════════╗"
echo "║ ✅ Device provisioned ║"
echo "║ $REGISTERED"
echo "╚══════════════════════════════════════╝"
else
echo "╔══════════════════════════════════════╗"
echo "║ ⚠️ Device not seen in DB yet ║"
echo "║ Check manually in a minute ║"
echo "╚══════════════════════════════════════╝"
fi