Skip to content

Repository files navigation

Scottina Light

Pocket digital beast of burden. A standalone diagnostics instrument for the Seeed Wio Terminal — the little sibling of Scottina.

Same kiosk language as the mother project — CRT phosphor palette, semiotic pictograms, navigable tiles that appear as their hardware is detected — on a 320×240 panel driven by a 5-way switch instead of a touchscreen.

It boots, scans the bus, and logs on its own. It depends on Scottina, Scottina Light, and CanTick for nothing.

scottina-light

Scope: diagnostics only

This firmware is a passive field instrument. That constraint is deliberate, and it is enforced in the code rather than left to good intentions — see include/scope.h.

Permitted

  • Passive observation: I²C bus scan, UART listen/autobaud, CAN passive sniff, onboard sensor reads.
  • Raw capture to SD, and decode/replay of that capture.

The one sanctioned transmit exception

  • CAN heartbeat / ACK replies required to remain a valid node on a live bus. There is deliberately no code path — and no UI affordance — for composing or injecting an arbitrary CAN frame.

Explicitly out of scope, do not add

  • Any offensive or attack tooling; arbitrary frame injection.
  • Network mapping (ARP sweep, port scan, packet capture, host enumeration). The RTL8720DN AT-firmware layer cannot do these anyway; the Wi-Fi surface is limited to scan / connect / report-own-IP.

Hardware

Board Seeed Wio Terminal
MCU ATSAMD51P19 — 512 KB flash, 256 KB RAM
Display 320×240 TFT
Input 5-way switch + 3 buttons (A / B / C)
Onboard LIS3DHTR accelerometer, mic, buzzer, light sensor, microSD, RTL8720DN Wi-Fi/BLE
Expansion 2× Grove, 40-pin header
CAN External I²C CAN-BUS module (Longan 1030017 / Seeed 113020111) — MCP2515 + MCP2551 behind an ATmega168PA, register protocol at I²C 0x25. The SAMD51's native CAN is not cleanly broken out

Two I²C buses are scanned: Wire (Grove) and Wire1 (internal).

Where the CAN module plugs in

Three different parts answer to "Grove MCP2515" and all three are probed, in this order. Read the board name, not the LEDs — the I²C and UART modules are both 20×40 mm and both carry TX/RX LEDs silkscreened TX and RX, which is a very easy way to misidentify the part you are holding.

Part Socket Cost
I²C CAN-BUS (Iengon I2C CAN BUS, addr 0x25) left Grove — the I²C socket none
Serial CAN-BUS (UART) 40-pin header, pins 8 + 10 (Serial1) none
Serial CAN-BUS (UART) right Grove, needs a crossover borrows SERCOM4 from the accelerometer
Bare MCP2515 40-pin header SPI none

The I²C module is the best of the three: it reports true DLC, a true extended-ID flag and RTR, and checksums every frame. The UART sibling reports none of those — ask canfe::dlcReported() before trusting len/ext/rtr.

It NACKs about half the time, and that is normal

Measured on the bench: 0x25 answered 4 probes out of 8, with SDA and SCL properly pulled up throughout. The ATmega168PA is a firmware I²C slave that also has an MCP2515 to service and simply misses address matches while busy.

Two consequences, both fixed here, and both of which had Light reporting no CAN front end while the module sat there answering every other request:

  • i2cbus::scan() used to require two ACKs in a row. It now requires two out of four tries — still enough to reject the cold-begin phantom ACK (that artifact answers once and never again), but no longer a coin flip for a busy slave.
  • Every register access in the CAN front end retries (8 attempts). A single-shot read of this part is a coin flip.

Notes on the UART sockets

Header wiring — module TX (yellow, Grove pin 1) → header pin 10 (BCM15, RX); module RX (white, Grove pin 2) → header pin 8 (BCM14, TX).

The right Grove socket needs the two signal wires swapped, because Grove pin 1 is the module's TX and on that socket pin 1 is D0 = PB08 = SERCOM4/PAD[0], the only pad a SAMD51 USART can transmit on. Straight through, the module's TX meets ours and our RX meets its RX.

Grove power on this board is not gated behind OUTPUT_CTR_5V / OUTPUT_CTR_3V3 (PC14/PC15) — measured; toggling them changes nothing at the socket.

Both working sockets are probed at boot, header first. The right Grove socket costs something real: D0/D1 are PB08/PB09, and the SAMD51 pin-mux gives those pads exactly one peripheral each — SERCOM4/PAD[0] and SERCOM4/PAD[1], with no second column to fall back on. This board already spends SERCOM4 on Wire1, the internal bus carrying the LIS3DHTR. One peripheral, two jobs.

So it is lent per screen: the CAN screen borrows SERCOM4 while it is open and hands it straight back on the way out. Everywhere else the accelerometer owns it and Vibration behaves exactly as before. The I²C scan skips Wire1 while it is lent rather than reporting the accelerometer as missing.

The Arduino core forces the mechanism: Wire.cpp hard-defines SERCOM4_0..3_Handler for Wire1, so the stock Uart class cannot be linked against sercom4 at all. The vector table is relocated to RAM at boot and those four entries are swapped on handover.


Status

v1-foundation. The skeleton, the discovery spine, and the black-box logger are real; the decode layer is not yet written.

Working

  • Screen framework (enter / tick / exit / onButton) with a nav stack.
  • Boot splash — phosphor rain, wordmark, staged progress bar.
  • I²C scanner — two-pass scan across both buses, a known-device registry, and WHO_AM_I-style verification to resolve address collisions. Unresolved collisions raise a pick-list; the choice is persisted to SD.
  • Autodetect → tile. Tiles materialise only when their hardware answers, and the launcher re-scans on tick, so hot-plugged hardware shows up on its own. CAN is the one deliberate exception: finding that front end means transmitting a +++ knock, so the probe cannot free-run on a tick and the tile is permanent, with its badge carrying the empty-socket state.
  • CAN passive sniffer — three front ends probed (I²C 0x25, then the UART module on either socket, then a header-SPI MCP2515), then live IDs with per-ID frame rates, DLC and payloads, plus raw timestamped capture to SD. Receive only; there is no send path in the code at all.
  • Vibration — live RMS / peak / trace off the onboard LIS3DHTR, with SD logging.
  • Serial — UART autobaud (marine-first ordering: NMEA 0183 at 4800, AIS at 38400), byte dump, manual baud pick on lock failure.
  • Logs — on-device browser over the SD captures.
  • Raw logger — rolling files, size cap, and a minimum-free-space floor so a long capture cannot silently fill the card.
  • Config in three tiers (see below), never hard-failing on a missing or malformed file.
  • Boot smoke self-test over serial.

Not yet implemented

  • CAN decode / dash — the sniffer is raw only.
  • DBC + PGN registries, 11-bit → DBC and 29-bit → N2K routing, fast-packet reassembly.
  • Replay of an SD capture through the decoders.
  • Audio FFT, Wi-Fi utility, captive-portal DBC/PGN loader.

The full specification and build sequence live in WioTerminal-Island-v1-TODO.md.


Build & flash

Requires PlatformIO.

pio run                 # build
pio run -t upload       # flash
pio device monitor      # 115200 baud

Adjust upload_port / monitor_port in platformio.ini to match your board's serial device.

Dependencies are resolved by PlatformIO: Seeed_Arduino_LCD, the LIS3DHTR driver, Seeed Arduino FS, Seeed Arduino SFUD, and ArduinoJson 7.

The test hook

platformio.ini builds with -DSL_TEST_HOOK=1, which lets you drive the UI over serial without hands on the device:

a b c   buttons A / B / C
u d l r 5-way directions
p       5-way press

Drop that flag for a field build.


Navigation

Launcher — 5-way to move across the tile grid, PRESS or A to open a tile, B to cycle the theme.

Inside a screenC goes back. Per-screen actions are printed in the header hint: A toggles logging on the Vibration screen, restarts the autobaud probe on Serial, and so on.


SD card

The card is optional — the firmware boots and scans without one — but it is where config and captures live. Directories are created on first mount.

/config.json          Tier 3: authored off-device (or pushed by the dock)
/config/i2c.json      Tier 2: persisted I²C disambiguation choices
/config/ui.json       Tier 2: persisted theme
/logs/*.log           raw captures
/tables/*.json        decode tables, pushed by the dock

The dock may write only /tables/ and /config.json, may read only /logs/, and may delete only /logs/ — and only against a verified hash. Tier-2 files are the user's own on-device choices and are not readable over the dock at all.

/config.json

Every key is optional. A missing, unreadable, or malformed file falls back to built-in defaults with a note on serial — bad config never hard-fails the boot.

{
  "wifi": { "ssid": "", "pass": "" },
  "can":  { "bitrate": 0, "link_baud": 0, "cs_pin": 50 },
  "uart": { "baud": 0 },
  "log":  { "max_bytes": 8388608, "min_free_mb": 32 }
}

uart.baud of 0 means autobaud, and so does can.link_baud — that one is the baud of the link to the module, walked across the five rates its firmware supports. Pin it to skip the walk and to make a failure report itself instead of being papered over by a fallback.

can.bitrate of 0 does not mean autobaud; it means leave the CAN bitrate exactly as the module was last configured. Bitrate is set, never scanned. An MCP2515 sitting on a live bus at the wrong bitrate does not fail quietly — it misreads every frame and answers with error frames, damaging the bus it was brought along to observe. Set it to one of the module's 18 rates (250000 for NMEA 2000 and J1939, 500000 for most vehicles) and it is applied over AT+C.

cs_pin belongs to the legacy header-SPI MCP2515 path and defaults to the board's PIN_SPI_SS, which is 50 on the Wio Terminal.

Config is split by how often it changes and how painful it is to type on a 5-way switch: Tier 1 is auto-detected (I²C addresses, UART baud, CAN bitrate), Tier 2 is picked on-device and persisted, Tier 3 is authored off-device and SD JSON is its single source of truth.


Themes

Three CRT-inspired skins, ported from the mother project. Chrome is monochrome — one phosphor colour and its shades — while the traffic-light status colours stay vivid across all three, so a warning always reads as a warning.

  • green — classic green phosphor
  • amber — P3 amber phosphor
  • light — sterile clinical white; chrome is greyscale, and only ok/warn/bad carry colour, for meaning alone

Cycle with B on the launcher. The choice persists to /config/ui.json.


Serial output

Every boot prints a smoke self-test — LCD geometry, the internal accelerometer's WHO_AM_I, the I²C scan census, SD state, config provenance, and a logger write→close→stat→cleanup round-trip:

SMOKE begin Scottina Light v1-foundation
SMOKE lcd=PASS  320x240
SMOKE i2c.imu=PASS  Wire1 0x18 WHO_AM_I=0x33
...
SMOKE result=PASS checks=8 failures=0
SMOKE end

Layout

include/
  scope.h              scope banner + product identity — read this first
  scottina_light.h     the whole public surface
  theme.h              palette struct
  pictograms.h         glyph enum
  tft_setup.h          TFT_eSPI config, force-included by the build
src/
  main.cpp             boot sequence + smoke self-test
  core_i2c.cpp         bus, probe, two-pass scan, known-device registry
  core_storage.cpp     SD mount, three-tier config, raw logger
  core_ui.cpp          input, chrome, nav stack
  screens.cpp          launcher + the five screens
  splash.cpp           phosphor rain
  theme.cpp            the three palettes
  pictograms.cpp       vector glyphs

Two details worth knowing before you touch the low level:

The cold-begin phantom ACK. On a bus with no pull-ups — nothing plugged into Grove — the SAMD51 SERCOM reports a spurious ACK for whichever address is probed first after a cold begin(). Verified on hardware: a cold probe of 0x42 ACKs, and every probe after it correctly NACKs. i2cbus::beginBus() burns one throwaway transaction to absorb that false positive, and it must run before anything scans.

No enumerator may be named RTC or ADC. CMSIS defines both as object-like macros in samd51p19a.h, and the preprocessor does not respect enum scope.


Docking

Light has no battery-backed RTC. Standalone, it does not know what time it is, so its SD logs are stamped in seconds-since-power-on — useless for lining up against anything Scottina Prime saw. Docking is what fixes that.

Plug Light into Prime over USB and a sync runs itself: Prime pushes the wall clock, pushes the enabled decode tables into /tables/, and pulls the black-box logs off the card. Prime always wins — nothing is ever edited on Light, and a stale table is overwritten rather than merged. You watch; you don't drive.

The wire contract is DOCK-PROTOCOL.md, mirrored verbatim in the kilodash repo. Three things are worth knowing without reading it:

  • Diagnostics only, still. The dock is provisioning and retrieval: it can set the clock, write tables, and read closed logs. It cannot trigger transmission, start a capture, or execute anything — see include/scope.h. Docking does not weaken that contract.
  • Docking suspends logging, and says so. The SD layer permits exactly one open file at a time, so the dock cannot serve a log file while the logger holds the handle. A dock session therefore closes the active capture and reopens a fresh one on BYE. The gap is written into the new file's header, along with the clock epoch and its quality (ntp / rtc / unsynced) — a clock Prime could not vouch for is labelled as such, never laundered into the record as truth. A 10 s watchdog resumes logging if the cable is yanked mid-session, so a dead dock can never leave the black box switched off.
  • A delete is verified or it does not happen. Logs are only unlinked after Prime returns the sha256 of what it actually received and Light agrees. A pull that verifies nothing is a copy; a delete that verifies nothing is data loss.

Nothing on Light reads /tables/ yet — the decode layer is not written — so a successful table push has no visible effect on the device. That is correct: the dock provisions ahead of its consumer.

The protocol's conformance vectors live in dock-vectors.json and are shared with kilodash. The frame codec and the path reject-pass are free of Arduino and SD so they run against all 47 of them on a laptop:

pio test -e native

Relationship to Scottina

Scottina is the mother project — the front panel. Scottina Light is its Wio Terminal sibling, sharing the visual language and the per-screen model but none of the code and none of the runtime dependency. Where a subsystem exists on both, the screen registry order and the colour keys follow the mother.

Standalone by design.


by Scott McLeslie

About

Pocket digital beast of burden — a diagnostics-only field instrument for the Seeed Wio Terminal. I2C/UART/CAN discovery, autodetected sensor tiles, and a black-box SD logger. Wio sibling of Scottina.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages