# S1 — System basics **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 dhcp`, `wifi ip
/ [gateway]`, `wifi dns [b]`, `wifi ntp [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).