Public Access
Past the command's name, Tab completes the word being typed as a path: a folder keeps its slash to go on from, a file completed whole gets a space, several candidates are listed. Any case typed, the name's own is taken. After a file command the first slash is understood (`cat no` is /no). The folder is read on the storage task, bounded to 400 entries looked at and 24 candidates. 489 host tests (2 new). Checked on the device: a folder, a file inside it, several candidates, no slash, another case, nothing matching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
207 lines
22 KiB
Markdown
207 lines
22 KiB
Markdown
+++
|
|
title = "System basics"
|
|
description = "The device works on any network, the card can be trusted, and you can see what the system is doing. A side milestone, like G1."
|
|
weight = 50
|
|
|
|
[extra]
|
|
docs = true
|
|
source = "docs/milestones/S1.md"
|
|
tag = "S1"
|
|
+++
|
|
**Status:** the three planned items are done: the SD driver fix in v0.6.1 (issue #21, ADR 0007), fixed IPv4 settings in v0.7.0 (issue #7), the System App in v0.8.0 (issue #11). v0.8.1 adds the resting main loop (issue #40) and the GNSS pause for the radio's noise (issue #20, still open for the 11 dB that remain). Still open in the milestone: #39, following the SD driver upstream.
|
|
|
|
**Goal:** the device works on any network, the card can be trusted, and you can see what the system is doing. A side milestone, like G1.
|
|
|
|
## Fixed IPv4, DNS and NTP (issue #7)
|
|
|
|
Not every network has a DHCP server: a lab bench, a direct link to a router, a network where addresses are handed out by hand. Until now every Saved Network used DHCP, DNS always came from DHCP, and the NTP server was `pool.ntp.org`, hard-coded.
|
|
|
|
**IPv4 only.** IPv6 isn't part of this, now or as a planned follow-up.
|
|
|
|
### Decisions (design round 2026-10-05)
|
|
|
|
| # | Decision |
|
|
|---|---|
|
|
| Q105 | The IP setting is **per Saved Network**: *Automatic* (DHCP, as before) or *Fixed*, with its own address, prefix and gateway. New networks start Automatic. |
|
|
| Q106 | The subnet is entered as a **prefix length** (`24`), with the mask shown next to it. |
|
|
| Q107 | The **gateway is optional**: left empty, the device talks to its own subnet only. |
|
|
| Q108 | **DNS is global:** two servers in Settings, used on every Fixed network. On Automatic networks DHCP's DNS is used, unless **"Always use my DNS"** is on. |
|
|
| Q109 | DNS defaults: **9.9.9.9** (Quad9), then **1.1.1.1** (Cloudflare). |
|
|
| Q110 | **NTP is global:** two servers in Settings, names or addresses, defaulting to `pool.ntp.org` and `time.cloudflare.com`. NTP servers offered by DHCP are used first. GNSS still outranks NTP for the clock. |
|
|
| Q111 | What's typed is checked, host-tested in `lib/wifi`: an address is four numbers from 0 to 255; a prefix is 1 to 30; the address isn't the subnet's network or broadcast address; the gateway is inside the subnet and isn't the device's own address. Refusals say why. |
|
|
| Q112 | Addresses are typed in the line editor, limited to digits and dots. |
|
|
| Q113 | Enter on a Saved Network opens **its page** (IP, Address, Prefix, Gateway, Forget) instead of asking to forget it. Settings > Wi-Fi gains DNS servers, "Always use my DNS" and NTP servers. The Status row opens **connection details**: address, mask, gateway, DNS and NTP in use, and where each came from. |
|
|
| Q114 | A change applies **at once**: the network in use reconnects with the new settings. No automatic way back; the keyboard still works if Wi-Fi is cut. |
|
|
| Q115 | Console: `wifi status` shows address, gateway, DNS, NTP and their sources; `wifi ip <ssid> dhcp`, `wifi ip <ssid> <address>/<prefix> [gateway]`, `wifi dns <a> [b]`, `wifi ntp <a> [b]`. Debug Builds: `wifi ip … try 60` goes back to the previous setting after 60 s unless confirmed with `wifi ip keep`. |
|
|
| Q116 | Left out: checking whether the address is already taken, and per-network DNS. |
|
|
|
|
The SDK already allows 3 NTP servers and 3 DNS servers and can take NTP servers from DHCP (`CONFIG_LWIP_SNTP_MAX_SERVERS=3`, `CONFIG_LWIP_DHCP_GET_NTP_SRV=y`), so the framework isn't rebuilt for this.
|
|
|
|
### Done when
|
|
|
|
- A Saved Network set to Fixed joins with that address, mask and gateway, and the device reaches the internet (IRC, Gemini, NTP) through the DNS servers from Settings.
|
|
- Set back to Automatic, it gets its address from DHCP again.
|
|
- With "Always use my DNS" on, an Automatic network resolves through the servers from Settings.
|
|
- The NTP servers from Settings set the clock.
|
|
- Wrong entries are refused with a reason, in Settings and on the console.
|
|
- Connection details show what's in use and where it came from.
|
|
- Tested on `knbg-guests` with 10.39.39.12 (the device's DHCP lease) and 10.39.39.13 (free: the device is alone on that network).
|
|
|
|
### Measured (2026-10-05 and 06, on `knbg-guests`)
|
|
|
|
The network is 10.39.39.0/24, gateway 10.39.39.1; DHCP gives 10.39.39.1 as DNS and offers no NTP server.
|
|
|
|
- **Fixed 10.39.39.12/24** (the device's own lease) and **Fixed 10.39.39.13/24**, gateway 10.39.39.1: the device joins with that address, DNS is 9.9.9.9 and 1.1.1.1 from Settings, and a Gemini page loads (name resolution, routing, TLS). On .13, .12 no longer answers.
|
|
- **A wrong gateway** (10.39.39.254) on a 60 s trial: the device stops answering from another subnet, and comes back by itself with the previous setting.
|
|
- **Back to Automatic:** 10.39.39.12 by DHCP again, DNS 10.39.39.1 from DHCP.
|
|
- **"Always use my DNS"** on an Automatic network: DNS becomes 9.9.9.9 and 1.1.1.1; switched off, the device joins again and has DHCP's DNS back.
|
|
- **NTP:** `pool.ntp.org` answers; set to `time.cloudflare.com` alone, that one answers within 25 s.
|
|
- **Refusals**, on the console and in Settings: the network's own address, a gateway outside the subnet, a prefix of 31 or 99, 10.39.39.300, an unknown network, a DNS name where an address is needed, a host name with an underscore.
|
|
- **In Settings:** the network's page pre-fills Fixed with the address, prefix and gateway in use; leaving the page applies it; connection details show each value and where it came from.
|
|
- **Not tested:** NTP servers offered by DHCP (this network offers none), and a Fixed network with no gateway.
|
|
|
|
### Work breakdown
|
|
|
|
1. **IPv4 logic** (host-tested): parsing and formatting addresses, prefix and mask, the checks of Q111.
|
|
2. **Storage:** the IP setting in each Saved Network; DNS, "Always use my DNS" and NTP in Settings.
|
|
3. **Wi-Fi Service:** apply it when joining; DNS and NTP; `wifi status` and the console commands.
|
|
4. **Settings:** the network page, the DNS and NTP rows, connection details.
|
|
5. **Tests on the device**, recorded here.
|
|
|
|
## System Monitor (issue #11)
|
|
|
|
Every milestone so far was driven by measurements, heap floors, stack sizes, TLS dips, that needed a Debug Build and a computer. The System App shows them on the device, in any build.
|
|
|
|
### Decisions (design round 2026-10-06)
|
|
|
|
| # | Decision |
|
|
|---|---|
|
|
| Q117 | An App of its own, **System**, in release builds too. Read-only. |
|
|
| Q118 | Four views, switched with Tab: **Overview** (CPU per core, memory, network, battery), **Tasks**, **Memory**, **System**. |
|
|
| Q119 | Sampled once a second. A task's share is its run time over the last second; a core's load is 100 % minus its idle task's share. |
|
|
| Q120 | **History only while the App is open:** two minutes at one sample a second, about 1 KB. The system already keeps what matters afterwards: the lowest free heap since boot and each task's lowest free stack. |
|
|
| Q121 | **Bytes are counted per service:** IRC, Gemini, the Debug Console and Firmware Updates add what they read and write to a shared counter. The network view shows the connection details, each service's bytes in and out, and the signal strength. |
|
|
| Q122 | Tasks: name, core, share, state and lowest free stack, sorted by share; `s` cycles the sort (share, stack, name). **Under 512 bytes of stack left shows in the warning colour.** |
|
|
| Q123 | Memory: free heap, lowest since boot, largest free block, and a two-minute graph of free heap **with the floors of Q86 drawn as lines** (55, 40 and 20 KB). |
|
|
| Q124 | System: uptime and why it last started, firmware and both app slots, chip temperature and CPU frequency, battery voltage and percentage, SD usage and write faults, the radio's and the GNSS receiver's state. |
|
|
| Q125 | `info` and `tasks` are split into a **snapshot** that the console and the App share; the arithmetic (shares from two samples, sorting, the stack warning) is host-tested. |
|
|
| Q126 | Left out: acting on tasks, an event log, exporting snapshots to the card. |
|
|
| Q127 | The main loop uses about 81 % of a core. The App shows it; fixing it is issue #40, not part of #11. |
|
|
|
|
The App has five views, not four: Q121's network view is one of its own (Overview, Tasks, Memory, Network, System).
|
|
|
|
### Measured (2026-10-06)
|
|
|
|
- **Traffic counters are exact.** A Gemini fetch of a 164,970-byte page counts 164,986 bytes in (the page and its 16-byte header line) and 42 out (the 40-character URL and CRLF). A 1,797,760-byte upload counts 1,798,123 in for the Debug Console, commands included.
|
|
- **The Memory view shows a TLS dip as it happens.** Starting IRC and a 165 KB Gemini fetch together: free heap falls from about 100 KB through the three floors to a low of 12.1 KB, then settles near 50 KB. That's the dip accepted in G1 (Q86).
|
|
- **A run-time counter only moves when its task is switched out.** FreeRTOS adds to a task's run time at the context switch. The main loop takes the samples, and with core 1 to itself it's never switched out: its counter said 2 % while the core's idle task had 0 %. So the task that samples gets what's left of its core. With that: **the main loop uses 100 % of core 1 at rest** (issue #40 said 81 %, an average since boot).
|
|
- **`tasks` on the console** sampled twice inside one command at first, a quarter second apart, and showed the loop at 1 %: it was asleep in the command's own wait. It now samples, lets the loop run for a second, and prints.
|
|
- **Low stack, flagged:** `IDLE0` (232 bytes left), `IDLE1` (328 to 352) and `spk_task` (256 to 264), all the framework's own tasks.
|
|
- **Cost:** 15.6 KB of flash for the App and the counters (1,742,723 bytes, release). Nothing while it's closed; about 2 KB of history and samples while it's open.
|
|
|
|
## The main loop rests (issue #40)
|
|
|
|
The loop polled the keyboard, ticked the Services, ran the consoles and redrew when needed, then came straight back: 50,000 passes a second, and core 1 100 % busy with the device idle and the screen off.
|
|
|
|
Nothing needs that. The keyboard controller buffers key events; the consoles and the radio have their own tasks or interrupts; no Service asks for a tick more often than every 50 ms. So after each pass the loop now rests: **5 ms with the screen on, 20 ms with it off**, and not at all during a serial file transfer (`sd put`), which reads its bytes from the loop. Safe Mode's loop rests 5 ms too. Debug Builds have `loop spin on|off` to bring the old behaviour back for comparison.
|
|
|
|
### Measured (2026-10-06, Debug Build, Wi-Fi connected, GNSS on, on USB power)
|
|
|
|
| | Spinning | Resting |
|
|
|---|---|---|
|
|
| Passes a second, screen off | 50,160 | 50 |
|
|
| Core 1 load, screen off | 100 % | 1 % |
|
|
| Passes a second, screen on (Launcher) | 1,203 | 167 |
|
|
| Core 1 load, screen on | 62 % | 10 % |
|
|
| Chip temperature at rest, settled | 38.3 C | 34.3 C |
|
|
| A 1.8 MB upload over the Debug Console | about 230 KB/s | 288 KB/s |
|
|
|
|
- Still working at this pace: GNSS (a 3D Fix, 22 satellites), a Gemini fetch (52 KB), the upload read back by SHA-256, the Sweep (still 606 to 610 ms a pass), the radio's DIO1 interrupt.
|
|
- **Not measured:** the current drawn (no meter on the battery line), and how typing feels on the real keyboard: a key now waits up to 5 ms for the loop, 20 ms if it's the one that wakes the screen.
|
|
- **The radio's noise floor didn't move** (-97 to -99 dBm at 125 kHz either way): the spinning loop wasn't the source (issue #20).
|
|
- **Not done:** real sleep. The framework is built without power management (`CONFIG_PM_ENABLE` is off), so an idle core only halts until the next interrupt. Automatic light sleep would need the framework rebuilt with it, Wi-Fi in modem sleep, and the USB serial port's behaviour checked. A next step if battery life calls for it.
|
|
|
|
## The radio's noise: the GNSS receiver (issue #20)
|
|
|
|
M3 found the LoRa radio's noise floor about 15 dB above what the chip hears alone, and that the source travels with the device. Which part? Debug Builds got a self-test, `lora noise test`: it changes one thing at a time, Sweeps the band eight passes (568 readings), records the median as the floor, and puts the thing back. It runs on the device by itself, because one condition switches Wi-Fi off, and `lora noise report` prints the result afterwards.
|
|
|
|
### Measured (2026-10-06, indoors, on USB power, dBm at 125 kHz)
|
|
|
|
| Condition | Floor |
|
|
|---|---|
|
|
| Antenna switched off (the chip alone) | -117 |
|
|
| Antenna on, GNSS in standby | -106 |
|
|
| Antenna on, GNSS running (as shipped) | -98 |
|
|
|
|
- **The GNSS receiver, while it runs, raises the floor by 8 dB.** Three runs: -98 or -99 with it running, -106 in standby, every time. On LongFast (250 kHz) the Sniffer's own reading goes from about -93.5 to -101.5 dBm.
|
|
- **It's the receiver working, not its serial line:** with one NMEA sentence a second instead of twenty (`PCAS03`), the receiver still tracking, the floor stays at -98.
|
|
- **Nothing else moves it by more than 1 dB**, with GNSS running or in standby: the main loop spinning or resting, the CPU at 240, 160 or 80 MHz, Wi-Fi on or off, the screen on or off, the radio chip's regulator as DC-DC or LDO, its receive gain boosted or not.
|
|
- **11 dB remain** between the antenna connected with GNSS quiet (-106) and the chip alone (-117). It comes in through the antenna and none of those switches changes it: the surroundings, or parts of the Cardputer that can't be switched off. Not separated: that needs another place, or the antenna on a cable away from the case.
|
|
- M3's quick check had GNSS at "1 or 2 dB": it read one frequency for a few seconds, in a noisier spot. The median over the band is the better measure.
|
|
|
|
### What the firmware does about it
|
|
|
|
**Settings > Pause GNSS for LoRa**, off by default: while the LoRa radio listens or sweeps, the GNSS receiver waits in standby, and wakes when the radio goes back to sleep (a Fix again after about 7 s here). Never during a Track. The GNSS App says "GNSS is paused" meanwhile. `gnss quiet on|off` on the console.
|
|
|
|
It's off by default because GNSS on by default was decided in M2 (Q58), and from M4 the radio listens all the time: then "pause while listening" means GNSS mostly off, which is a decision about position, the clock and Tracks, for M4's design round (issue #23).
|
|
|
|
## The Shell (issue #67)
|
|
|
|
The console's commands could only be typed on a PC: over USB, or over Wi-Fi with the Debug Console. A device in a bag, or on a network that is down, could not be asked anything. The Shell is an App that runs the same commands on the device's own screen and keyboard.
|
|
|
|
### Decisions (design round 2026-10-07)
|
|
|
|
| # | Decision |
|
|
|---|---|
|
|
| Q204 | **An App, "Shell", in the Launcher**, in every firmware: its commands already work over USB for anyone holding the device. |
|
|
| Q205 | **Trusted like USB serial**, not like the network: `debug on` and `debug token` work from it, as they do in Settings. The token is never shown. |
|
|
| Q206 | *Revised the same day, after trying it.* **It shows the replies to its own commands, and only those.** The console knows who each line is printed for (`Console::Origin`): a command run from the Shell prints as the Shell's, and so does what answers it later from another task, which notes who asked and takes it back when it prints (`ls`, `tasks`, `du`, `cp`, `update check`, `sd list`, `screenshot`, `gemini get`). What USB or the Debug Console asked for, and the system's own lines, are not the Shell's. **Ctrl+b shows everything** instead. The first version kept whatever was printed in the ten seconds after a command, which was a guess, and a noisy one. |
|
|
| Q207 | **Nothing while it's closed.** Open, a 4 KB ring of the console's and up to 4 KB of lines; both go when the App is left, with the list of commands for Tab. |
|
|
| Q208 | Enter runs the line; Fn with up and down recalls the last 16; Alt with up and down scrolls back. **Tab completes** the command's first word, from the firmware's `help` text, and *(added the same day)* **past it a path on the SD card**: a folder keeps its slash to go on from, a file completed whole gets a space, several candidates are listed. Whatever the case typed, the name's own is taken, since the card doesn't tell them apart. After a file command the first slash is understood (`cat no` is `/no`). A name with a space in it isn't completed. |
|
|
| Q209 | *Revised the same day.* **`rm` is Unix's, with a question.** A folder needs `-r`, here and over the consoles (where `rm <folder>` used to remove it with what was in it). In the Shell, a file, or a folder with something in it, is asked about unless `-f` (`-rf`, `-r -f`); an empty folder with `-r` goes without a word; what `rm` would refuse anyway, it refuses itself. Over the consoles nothing is asked: scripts delete as before. **`screenshot [seconds]`** saves the screen as a PNG in `/screenshots`, now or after a pause, since from the Shell "now" is the Shell. `get`, `put`, `coredump get` and `reset` answer `Debug Console only`. |
|
|
| Q210 | Commands are echoed as `> command` into the console, so a session reads the same from afar. **A token being set is not echoed.** |
|
|
| Q211 | **Not in Safe Mode**, which starts no Apps: issue #77. |
|
|
| Q212 | Its keys are a table in `app_keys.h`, so the help panel and the website have them; a page in the user guide. |
|
|
| Q213 | *Added the same day.* **An App's name with a capital opens it:** `Irc`, `Wifi`, `Gnss`, `Gemini`, `Lora`, `Storage`, `Notes`, `Shell`, `System`, `Settings` (the App's id, up to its first dash). From the Shell, without going back to the Launcher, and from the consoles too. The capital says "an App": every command of the firmware is in small letters. Tab completes them. |
|
|
|
|
### As built
|
|
|
|
- **`ShellApp`** (`src/apps/shell_app.cpp`), with its model host-tested in `lib/apps_model/src/shell_log.h`: lines arriving in pieces, the 4 KB limit, the command words out of the `help` text (Apps' names included), and Tab.
|
|
- **Who a line is for:** `Console::As` marks the calling task as printing for the Shell while it lives, and `Console::origin()` lets a command that answers later carry that to wherever it prints. The console has a second ring for the Shell, which gets the lines marked so, or everything.
|
|
- **The Shell hands its lines to the main loop**, which runs them like the consoles' commands. See below for why.
|
|
- **`info` says which App is in front** (`app: Shell`): so that a hand driving the device from afar can look before it types.
|
|
- **`screenshot`** writes the PNG a row at a time with no buffer: 8-bit indexed colour, the 256 colours of RGB332 as the palette, the pixels in one stored deflate block (`lib/files/src/png_rgb332.h`, 4 tests). 33,383 bytes for the 240 x 135 screen. A Toast says so once it is on the card, so the Toast is never in the picture.
|
|
- **The `help` text lost its shorthand** (`update check | list | status` is written out), since Tab reads its first words from there.
|
|
- **Tab on a path** reads the folder on the storage task while the main loop waits, so it is bounded: 400 entries looked at, 24 candidates given back, and `...` after the list when there were more.
|
|
- **Cost:** 21 KB of flash, 96 bytes of static RAM. Open: 7 KB of heap (107.6 KB free before, 100.7 with it open, 106.1 after leaving).
|
|
|
|
### What went wrong while building it
|
|
|
|
**The device crashed, and the test sent a message to an IRC channel.** The first version ran a command from inside the key handler. Driven from the Debug Console, that is: the main loop, a remote command, `key select`, the App manager, the Shell, `runCommand` a second time, the file command, and `printf` under all of it. The main loop has under 2 KB of stack to spare; `rm` on a folder went past it. The crash report decoded to exactly that chain.
|
|
|
|
The device restarted into the Launcher, and the test script, which did not look, went on typing. Its next Enter opened IRC, which connected, and a few lines later it typed "No" into a channel and pressed Enter. One word, sent to real people, that can't be taken back.
|
|
|
|
Two changes came of it. The Shell now **queues** its line and the main loop runs it, at the same stack depth as a console's command. And `info` reports the App in front, which the test script now checks before every line it types.
|
|
|
|
### Checks on the device (2026-10-07, driven over the Debug Console with `key`)
|
|
### Checks on the device (2026-10-07, driven over the Debug Console with `key`)
|
|
|
|
| Check | Result |
|
|
|---|---|
|
|
| Open it from the Launcher, type `ls /`, Enter | The folders are listed |
|
|
| Tab on `in` | `info install` is shown and the line stays; on `u` it becomes `update `; on `l`, `log ls lora loop` |
|
|
| Up | The line before comes back |
|
|
| `screenshot` | `/screenshots/20261007-104357.png`, 33,383 bytes. Fetched and decoded on the PC: 240 x 135, indexed, every chunk's CRC right, the pixels the Shell's screen |
|
|
| Output | With a Debug Console client connecting and disconnecting for every key, the Shell shows the commands and their replies and nothing else: `info`, `ls /` (answered by the storage task), `tasks` (answered a second later) |
|
|
| `rm` on a folder, without `-r` | Refused, the folder stays |
|
|
| `rm -r` on an empty folder | Removed, no question |
|
|
| `rm -r` on a folder with files | Asks; Cancel leaves it. `rm -rf` removes it without asking |
|
|
| `rm` on a file | Asks; Delete removes it |
|
|
| Tab on a path | `ls /no` becomes `ls /notes/`, and again, with one note in it, the note's whole name and a space. `ls /g` lists `gemini/ gnss/`. `cat no` becomes `cat /notes/`. `rm -r /CAP` becomes `rm -r /captures/`. `ls /zz` stays as it is |
|
|
| `No`, Tab, Enter | Completes to `Notes ` and opens Notes. `Shell` from the Debug Console opens the Shell |
|
|
| `screenshot 4`, then Home | The picture, taken four seconds later, is of the Launcher: the pause works, and the Toast isn't in it |
|
|
| `quit` | Back to the Launcher, and the memory comes back |
|
|
| The help panel in the Shell | Its keys, then the ones that work everywhere |
|
|
|
|
**Not checked:** Ctrl+b and the Alt scroll, which `key` can't press (no modifiers): the filter itself is host-tested, the key that flips it is not; the real keyboard altogether; and the Toast after a screenshot, which was published but not looked at.
|