The upstream image switched from .env.local to TOML config, which silently broke the dotminipc container; record what happened and where its config lives now, since it shares the same upstream software as the Pi fleet. Co-Authored-By: Claude Sonnet 5 <[email protected]>
dotmesh-monitor
Ansible playbooks for deploying MeshCore monitoring nodes (Raspberry Pi Zero W / Zero 2 W).
Hosts
| Host | Hardware | Group |
|---|---|---|
| dm-baldock | Pi Zero W (armv6) | zero_w |
| dm-ashwell | Pi Zero 2 W (armv7) | zero2_w |
| dm-edworth | Pi Zero 2 W (armv7) | zero2_w |
There's also a fourth capture node, dotminipc (device name dm-stotfold) —
not managed by this repo (it's a Docker container on the shared dotnetwork
host, not a dedicated Pi), but documented below since it's the same upstream
software and shares config with the Pi fleet. See "dotminipc" below.
Prerequisites
Local machine:
pip install ansible
# or: sudo apt install ansible
New Pi node checklist:
- Flash Raspberry Pi OS Lite (Trixie) with Raspberry Pi Imager, using "Edit Settings" (OS customisation) to set hostname, the
daviduser + password, and thedotnetworkwifi (SSID/password only — the imager only supports one network at flash time; the deployed-location anddotmobilenetworks get added later by thewifirole, see below). - Before ejecting the card, verify the customisation actually got written — mount the boot partition and check
network-config/user-dataaren't just the commented-out stock template (see "Imager gotcha" below). Onlycmdline.txt's regdomain getting a fresh timestamp while the rest stay at the image's build date is the tell that it silently failed. - Boot the Pi, find its LAN IP (e.g. from the router's DHCP leases), confirm SSH access with the password you set — no key is seeded at flash time.
- Run
ssh-copy-id david@<ip>from an interactive terminal (not through a non-interactive shell/script — it needs a real TTY to prompt for the password) so ansible can connect with a key. - Connect the MeshCore device via USB, then find its serial ID:
ls /dev/serial/by-id/. - Add
serial_port(andwifi_ssid_location) toansible/host_vars/<hostname>/vars.yml, and the location wifi password toansible/host_vars/<hostname>/vault.yml(see "Vault" below). - Run
site.ymlagainst just that host, overriding the host's address since Tailscale/DNS won't resolve it yet:ansible-playbook site.yml --limit <hostname> -e "ansible_host=<ip>". This authorizes your SSH keys, joins the deployed-location + dotmobile wifi networks, installs (but does not authenticate) Tailscale, and deploys everything else in one pass. - Tailscale needs one manual step: SSH in and run
sudo tailscale up, then open the printed URL in a browser to approve the device on your tailnet. (You can pass-e tailscale_auth_key=tskey-...— from the admin console — to authenticate non-interactively instead, but there's no stored key anywhere for this repo, so the interactive route is simplest for a one-off node.) - The meshcore-packet-capture installer also needs one manual step (see "meshcore-packet-capture install is interactive" below): SSH in and run
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/agessaman/meshcore-packet-capture/main/install.sh)", answering its ~3 prompts (service account, install method — pick 1, IATA/broker config — defaults are fine,site.ymloverwrites.env.localafterward anyway). - Once Tailscale is up, re-run
site.ymlwithout theansible_hostoverride — it'll resolve via the Tailscale hostname from here on, and will now just write.env.local+ enable the service since the installer step is already satisfied.
meshcore-packet-capture install is interactive
The install.sh bootstrap has no real non-interactive path for a fresh install — its --update flag only changes behavior when an installation already exists. It also refuses to run at all with piped stdin (curl | sudo bash errors out asking you to download the script first) and its Python layer explicitly opens /dev/tty for prompts, which just hangs forever over plain SSH/ansible (no human there to answer). We tried feeding it scripted answers via script/a pty and it's not worth the fragility — just run it manually once per node (step 9 above); ansible/roles/meshcore_capture/tasks/main.yml's creates: guard means ansible never touches it again afterward.
Imager gotcha (2026-07)
The Raspberry Pi Imager available via Flathub (org.raspberrypi.rpi-imager) is stuck on 1.9.6 and there's no newer .deb/Flatpak in the Ubuntu or Flathub repos either — Flathub hasn't published the 2.0.x rewrite. 1.9.6 silently fails to apply OS customisation (hostname, user, SSH, wifi) on newer Raspberry Pi OS Trixie images: it only writes the kernel cfg80211.ieee80211_regdom= cmdline parameter and leaves user-data/network-config as the stock commented-out template, with no error. The result looks exactly like a wifi problem (Pi never appears on the network) but is actually "the card has no credentials on it at all."
Fix: grab the real .deb from the GitHub releases page (e.g. rpi-imager_2.0.10_amd64.deb) and sudo dpkg -i it — that version writes the customisation correctly.
Usage
Deploy to a single host (recommended for first run / testing):
cd ansible
ansible-playbook -i inventory.yml site.yml --limit dm-edworth
Deploy to all nodes:
ansible-playbook -i inventory.yml site.yml
Dry run:
ansible-playbook -i inventory.yml site.yml --limit dm-edworth --check
If sudo requires a password, add --ask-become-pass.
You'll be prompted for a Tailscale auth key — leave blank if the node is already authenticated.
What it does
- wifi — configures NetworkManager connections for
dotnetwork(home),dotmobile(phone hotspot, field troubleshooting fallback), and the host's deployed-location network - base — apt upgrade, installs screen/pipx/vnstat/git, sets MOTD, authorizes SSH keys for both laptop partitions, installs Tailscale (always) and authenticates it (only if
tailscale_auth_keyis set — otherwise runsudo tailscale upmanually once, see checklist above) - meshcore_cli — installs
meshcore-clivia pipx - meshcore_capture — runs the agessaman/meshcore-packet-capture install script (skipped once already installed — see "meshcore-packet-capture install is interactive" above, this needs a manual first run), writes config (TOML
config.d/99-user.tomlon current "system"-layout nodes, legacy.env.localon the two nodes still on the old layout — seemeshcore_capture_layoutingroup_vars/all/vars.yml), enables the capture service, deploys update/log helper scripts - scripts — deploys
voltage.shandbandwidth.sh
Config
Shared MQTT config lives in group_vars/meshcore.yml. Per-host serial port and wifi SSID are in host_vars/<hostname>/vars.yml.
Running the playbook again re-applies .env.local and restarts the service if it changed — safe to run on already-deployed nodes.
Vault
group_vars/all/vault.yml (shared wifi/SSH secrets) and host_vars/<hostname>/vault.yml (per-host deployed-location wifi password) are encrypted with Ansible Vault. ansible.cfg points at ../.vault_pass (gitignored, not committed) for the password — ask David for a copy, or generate a fresh one and re-encrypt if starting over:
ansible-vault view --vault-password-file ../.vault_pass group_vars/all/vault.yml
ansible-vault edit --vault-password-file ../.vault_pass host_vars/dm-edworth/vault.yml
*/vault.yml.example shows the expected keys.
group_vars/meshcore.yml (MQTT credentials) is still plaintext — consider moving it into the vault too if this repo is shared further.
dotminipc (Docker node, not managed by this repo)
A fourth capture point, device name dm-stotfold, runs as a Docker container
(ghcr.io/agessaman/meshcore-packet-capture:latest) on dotminipc
(172.16.31.92), the shared Docker/HA host documented in the sibling
dotnetwork repo. It isn't part of this repo's inventory — no Ansible role
here touches it — but it's the same upstream software as the Pi fleet, so
config drift between the two is worth knowing about.
- Compose file:
dotnetwork/docker/dotminipc/meshcore-packet-capture/docker-compose.yml - Config:
dotnetwork/docker/dotminipc/meshcore-packet-capture/config.d/99-user.toml— host-only, gitignored (contains MQTT credentials), same TOML shape as this repo'smeshcore_capturerole template (ansible/roles/meshcore_capture/templates/99-user.toml.j2); keep the broker list in sync between the two if either changes. The committedconfig.d/99-user.toml.examplein the dotnetwork repo is the sanitized reference copy. - Deploy/restart:
ssh [email protected], then from/opt/docker:docker compose up -d meshcore-capture --force-recreate. Logs:docker logs -f meshcore-packet-capture.
2026-07-27 outage, for reference: the :latest image was rebuilt
2026-07-25 and switched its config format from .env.local to TOML
(/etc/meshcore-packet-capture/config.toml + config.d/*.toml —
the same "system" layout meshcore_capture_layout already models for the Pi
fleet). It silently stopped reading .env.local at all — despite upstream's
own README still describing that as a supported "legacy" path, in practice it
was just ignored — so the container fell back to the image's baked-in
defaults (connection_type = "ble", serial.ports = ["/dev/ttyUSB0"], no
owner key, only 2 of 5 brokers) and crash-looped on
could not open port /dev/ttyUSB0. Fix was to mount a full TOML override at
config.d/99-user.toml instead (see above) — same pattern as the Pi
meshcore_capture role already uses for system-layout nodes. Also found in
the process: the old .env.local had PACKETCAPTURE_MQTT5_SERVER set to
mqtt.meshmapper.cc, which fails TLS handshake — the correct domain is
mqtt.meshmapper.net (confirmed via openssl s_client; .net presents a
valid Let's Encrypt cert, .cc returns a TLS alert). If the Pi fleet's
99-user.toml.j2 broker list is ever regenerated from a stale copy of this
node's old config, watch out for that typo resurfacing.