Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 76 additions & 51 deletions docs/BEHAVIOR.md

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions docs/TUNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ in `firmware/products/skyblip_go/settings.h`.
| `kLongPressMs` | 2000 (2 s) | milliseconds | - | Long enough that it cannot be the page press, short enough to do with gloves on. |
| `kParkMs` | 3000 (3 s) | milliseconds | - | A power-off parks the panel with a full refresh, which the SSD1681 clocks out in about 2.5 s, and an install or a recovery through black, about a second. |
| `kReleaseSettleMs` | 100 | milliseconds | Settle | nRF52 SENSE is a level detect, not an edge, so arming the wake pin while the button is still down wakes the device the instant SYSTEM OFF latches. |
| `kPlateauHoldMs` | 120000 (2 min) | milliseconds | Hold | [README](../firmware/core/power/README.md) argues it |

## [`firmware/core/protocol`](../firmware/core/protocol/README.md)

Expand Down Expand Up @@ -267,4 +266,4 @@ in `firmware/products/skyblip_go/settings.h`.
| `kBaroConversionCeilingMs` | 150 | milliseconds | Ceiling | Zephyr's BME280_MEASUREMENT_TIMEOUT_MS, over 3x the DS 9.1 worst case |
| `kBatteryPeriodMs` | 1000 (1 s) | milliseconds | Period | A cell moves over minutes. The gauge needs three readings before it can throw out a transient, so a second between them is the slowest cadence that still shows the state of charge on the first screen a pilot sees. |

162 constants over 18 folders.
161 constants over 18 folders.
2 changes: 1 addition & 1 deletion firmware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ The mechanism is the half people get wrong, because four different things all re
| `Forget` | past this the record itself is dropped | a fresh report | `kTargetForgetReports`, `kCallsignForgetS` |
| `Samples`, `Fixes` | consecutive readings that must agree | one reading the other way | `kLevelSamples`, `kConvergedFixes` |

Hysteresis is not on the list and never becomes a constant: it is the gap between two named thresholds, like the 12.0 m/s a takeoff needs and the 1.0 m/s a landing does, and naming the gap would be a third number nobody reads.
Hysteresis is not on the list and is usually no constant: it is the gap between two named thresholds, like the 12.0 m/s a takeoff needs and the 1.0 m/s a landing does, and naming the gap would be a third number nobody reads. The exception is a gap measured once and shared by every step of a ladder, which is named for what was measured: `kRecoveryMarginMv` is the spread a parked cell reads across, and each battery level is left upwards by that much above the step it was entered under (`core/power/README.md`).

The subject comes first because a mechanism on its own is ambiguous where it is used: `kForgetMs` forgets what? Two of those existed, in two namespaces, 30 s each by coincidence. A constant inside a class takes the class as its subject, so `ScreenService::kPresentFloorMs` needs no more. `Delay`, `Grace`, `Debounce` and `Steady` are spelled as one of the words above, so that a grep for every hold in the tree finds every hold.

Expand Down
5 changes: 3 additions & 2 deletions firmware/boards/lilygo/t_echo_plus/board.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,9 @@ class TEchoPlus {
if (now_ms - last_battery_ms_ < runtime::kBatteryPeriodMs) return;
last_battery_ms_ = now_ms;
uint16_t millivolts = 0;
if (platform_.read_battery_mv(millivolts))
bus_.battery.push(events::BatterySample{millivolts, platform_.external_power()});
// INFO: fc 05oct26 refused or not, the reading and VBUS go out: core/power judges it
platform_.read_battery_mv(millivolts);
bus_.battery.push(events::BatterySample{millivolts, platform_.external_power()});
}

// The producer side: everything hardware says arrives on the bus, and the
Expand Down
19 changes: 19 additions & 0 deletions firmware/boards/lilygo/t_echo_plus/factory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,22 @@ What it pins down:
At tag 0.6.1 the write window is `USER_FLASH_START..USER_FLASH_END` = `MBR_SIZE .. BOOTLOADER_REGION_START - DFU_APP_DATA_RESERVED` (`src/usb/uf2/uf2cfg.h:20-21`), and the nRF52840 branch of `Makefile:117` sets `DFU_APP_DATA_RESERVED = 10*4096`. With the region starting at 0xF4000 that is 0x1000..0xEA000, which is what `scripts/mkuf2.py` guards and `scripts/test_mkuf2.py` asserts. The device is two commits past that tag on LilyGO's build, and neither constant moved between them.

A unit that reports a different bootloader or SoftDevice version has not been checked against any of this. Read its `INFO_UF2.TXT` first: a smaller `USER_FLASH_END` silently truncates an install, and that is the failure the guards in `mkuf2.py` exist to catch.

## What it does with a wake

`0.6.1-2-g1224915` is Linar Yusupov's fork of the Adafruit bootloader (`lyusupov/Adafruit_nRF52_Bootloader`), two commits past the tag: `42d10b5` adds the board, and `1224915`, "Fix against unwanted wakeup by USB VBUS power on/off signal when battery is main power source", adds four lines to `src/main.c:181-184`. Adafruit's own master does not carry them. They run before the board is initialised, on every reset:

```c
if ((NRF_POWER->RESETREAS & POWER_RESETREAS_VBUS_Msk) && dfu_skip) {
NRF_POWER->RESETREAS |= POWER_RESETREAS_VBUS_Msk;
NRF_POWER->SYSTEMOFF = 1;
}
```

`dfu_skip` is GPREGRET holding `0x6d`, `DFU_MAGIC_SKIP`, the byte the application leaves there on its way to SYSTEM OFF. So while it is set, a cable plugged in wakes the SoC and the bootloader puts it straight back to sleep, before MCUboot and before the application. SoftRF, whose author wrote the fix, writes the same byte before every sleep (`src/platform/nRF52.cpp:3213-3226`), so for SoftRF giving up the cable is the point, and for this firmware too: a charger plugged into a device in a flight bag must not wake it. GPREGRET is cleared only after that test, so the byte survives and every later cable is swallowed the same way. A GPIO wake does not set the VBUS bit, and the pin configuration survives the wake reset, so an armed button still wakes the unit through it.

The application cannot simply stop writing the byte. `BUTTON_DFU` is P1.10, the device's only button (`src/boards/lilygo_techo/board.h`), and without the skip the bootloader reads it on every boot: a press that wakes the unit is still held when it looks, and `dfu_start` comes up USB DFU with no timeout. `BUTTON_FRESET` is P0.11, the pad, and both together are BLE OTA. The skip also bypasses the double-reset window and its RAM word at `0x20007F7C`, whose contents through SYSTEM OFF are not defined, at the cost of a double-click of RESET not arming on the first boot after a power-off.

So every switch-off writes `0x6d` (`boot_magic_for_system_off` in `core/power/shutdown.h`), and every switch-off arms the button. The byte keeps a waking press out of USB DFU, and the price is the cable: on this bootloader a charger never reaches the application, so a unit switched off on `FLAT BATTERY` or `CHARGE BATTERY` keeps that word through the whole charge, and the button, which the lockout refuses on a flat cell and lets through on the cable, is the only way back (`core/power/README.md`). This firmware once withheld the button after the cutoff and after a refusal on a flat cell, which with `0x6d` left those units to the reset pin alone, and then wrote `0x00` there so a cable could reach the refusal and repaint the glass. Both are gone: a unit the cutoff switched off wakes on its button like any other, and an armed button needs `0x6d`.

`0x57` for recovery outranks it (`hardware/platform/zephyr/dfu.h`).
2 changes: 1 addition & 1 deletion firmware/core/comms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ What a claim is not is access control. `CONFIG_BT_SMP` is off on this product, d

`{"cmd":"status"}` is the one frame the device also sends without being asked: `set_flight_state` pushes it when the flight state changes, and `set_battery_state` on a charging flip, a level change or a five-percent step. The flight state is what every write is gated on, so a page that holds its buttons for the ground learns it landed without polling. So it carries what a pilot's screen shows and nothing a bench reads, and it has to fit `kSmallestSupportedPayload`, the 182 bytes an iPhone commonly settles at, with every field at its widest. The buffer is that limit: `json::Writer` leaves a field out whole rather than cutting it, and `format_status` then refuses the frame and counts a `link_drops`.

What a support case reads lives where it is asked for. The address and the callsign are the config reply's. Why the device came up is `reset` in the diag sys group, and whether the board has a gauge at all is `valid` in the diag power group. One boot fact stays, `went_dark_flat`, because "the battery went flat" is the answer a pilot is owed for a unit that switched itself off (`core/power/README.md`). A key with no reading behind it is left out, never sent as a zero: `battery_percent` without a gauge sample, `die_temp_c` without a fresh die reading.
What a support case reads lives where it is asked for. The address and the callsign are the config reply's. Why the device came up is `reset` in the diag sys group, and whether the gauge is reading the cell is `valid` in the diag power group, with `mv` what the divider read when it is not: no divider, a refused reading, or the USB rail on the cable (`core/power/README.md`). One boot fact stays, `went_dark_flat`, because "the battery went flat" is the answer a pilot is owed for a unit that switched itself off (`core/power/README.md`). A key with no reading behind it is left out, never sent as a zero: `battery_percent` without a reading of the cell, which on the cable there never is, `die_temp_c` without a fresh die reading.

[`schemas/status.v1.schema.json`](../../../schemas/status.v1.schema.json) is the contract, and `scripts/check_status_schema.py` holds `format_status` to its keys, their order, their types and which of them may be absent. The widest frame is pinned byte for byte in `test/products/test_comms_report.cpp`.

Expand Down
Loading