Repository navigation
Conversation
E68BD9's run to cutoff flipped Normal and Low five times in five minutes and Low and Critical three times in one, because three samples at or above a step were enough to climb back across a reading that spreads 35 mV (#115). Climbing a step now also needs the reading 35 mV above it, sized from that run's half-hour spread. Going down is unchanged.
…l as the cell On the cable the divider reads the USB rail, about 4.7 V, not the cell: E68BD9 read 4670-4700 mV through a charge from 45 %, and 0B1B2C read above 4700 and refused every sample, hiding the cable with them (#114). VBUS now rides on every battery pass, a refused reading is counted in implausible and its raw value kept in cell_mv, and on the cable the gauge says nothing rather than 100 %. The charge curve and the learned trim are deleted: neither can work while the divider sees the rail, and a trim learned off it would move the cutoff. power_budget.py charges the IMU row only to a unit whose boot record has the Inclinometer capability.
…e cutoff switched off LilyGO's UF2 bootloader (lyusupov fork, 1224915) sends any VBUS wake straight back to SYSTEM OFF when GPREGRET holds the skip magic, which system_off wrote before every switch-off, so a cable on a flat unit never reached the refusal that repaints the glass and re-arms the button (#113). The skip byte now follows the button: kept when the button is armed, because the bootloader's DFU button is ours, cleared when it is withheld. A retained charger_woke bit shows USB WOKE on the self-test page and in the diag reply, and the watchdog is armed before setup so a refused boot that hangs reboots.
A third capture on the CAPTURE page records like POWER RUN and runs a flight clock for the glass and the g-meter, so a bench discharge pays for a flying glass. What goes on air stays the truth: ADS-L G.1.2 carries the flight state and G.1.16 holds a grounded unit to 0.1 Hz, so the cadence and the payload stay the ground ones, and power_budget.py prints the transmit not sent beside the model. No flight log is written, so a bench run cannot push real flights out. The Boot record names the profile in a flagged byte, so older captures decode as before.
… the bootloader A unit the cutoff switched off answered neither its button nor, on the factory bootloader, a cable: only the reset pin brought it back. Letting the cable through by writing 0x00 traded that for a bootloader that would put a waking press into USB DFU, so every SYSTEM OFF now arms the button and writes 0x6d, and a charger never wakes the device. A press on a flat cell is refused by the lockout, leaves the flat frame without a refresh and waits for the button to come up before arming it, and a press on the cable runs. The charger_woke note and its USB WOKE and diag field go with the cable they reported, and GPREGRET2 is back on its 0xa8 layout.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The first two power runs on silicon (
docs/bench/2026-10-05-power-run-*.md) showed four problems that stop the next charge and discharge run from being trustworthy:This branch fixes the first three and adds a
FLIGHT RUNcapture for the fourth, within what ADS-L allows on air. One commit per change, in this order: #115, #114, #113 (two commits: the first let the cable through, the second, 3abc34e, replaces it with the behavior chosen in review), flight run.Behavior changes
FLAT BATTERYorCHARGE BATTERYstays on the glass while charging, the charger's red LED shows the chargesetup()and reboots itvalidfalse,external_powerfalse and the cable unseen. E68BD9 read the rail as the cell, so100 %while charging from 45 %-- V --% CHG,battery_percentleft out,chargingequal toexternal_powerimplausible, raw value kept incell_mvwithvalidclearFULL,POWER RUNFULL,POWER RUN,FLIGHT RUNFLIGHT RUNarmedPOWER RUN, and the radar and g-meter run a flight clock from arming. The air and the flight log stay groundedpower_budget.pyboot, classes a flight run, prints the transmit not sentVisual changes
The glass, drawn by the host build from each page's own test snapshot.
Implementation notes
#113, the button after the cutoff.
UF2 Bootloader 0.6.1-2-g1224915(factory/INFO_UF2.TXT). lyusupov's commit 1224915 sends any VBUS wake straight back to SYSTEM OFF whenGPREGRETholdsDFU_MAGIC_SKIP(0x6d), before MCUboot or the app runs. SoftRF, by the same author, writes 0x6d before every sleep, so for SoftRF ignoring the charger is the point.power::boot_magic_for_system_off, recovery's 0x57 still wins).ButtonWake,button_wake_afterandbutton_wake_after_refusalare gone. A refused boot waits for the button to come up (kReleaseSettleMs, 100 ms) before arming it, or the held press would wake it again at once.setup()and fed in the refusal loop, which is capped at 6 s bykRefusalParkCeilingMs. The console line prints how longsetup()took.#114, the cable.
power::plausible_mvincore/power/battery.his now the one 1800..4700 mV window, used by both platforms and the boot read.board.hpushes every sample with VBUS, whatever the ADC returned.powerrecord keeps its layout.trim_learned(bit 7) and bytes 13-14 are retired, written as zero and never reused, andscripts/blip_records.pystill reads them from old captures.core/power/README.md"The two curves" and "The trim" are rewritten to say what the divider sees.#115, the levels.
kRecoveryMarginMv = 35incutoff.his the median half-hour spread of the gauge's median over E68BD9's 37 windows off the cable. Astatic_assert(kCriticalMv + kRecoveryMarginMv < kLowMv)keeps a cell climbing out of Critical inside Low.Flight run.
state.flight.running,seconds). Only the glass and the g-meter read it, andOwnshipServicepublishes a second one held airborne while the profile is armed.Bootbyte 14 names the profile, behind flag bit 2, so the two runs already indocs/bench/data/decode with noprofilekey instead of asfull.Design decisions
Delete the trim and the charge curve, not fence them off. On the cable the divider reads the rail, about 4.7 V on both units, so neither can learn anything. A trim learned off a rail near 4.2 V would have moved the cutoff.
settings.battery_offset_manualstays in the blob, because dropping it means a settings version bump.The cable never wakes the device, the button always does. Considered: letting the cable through after the cutoff only (the first power: a charger on a unit the cutoff switched off neither repaints the glass nor arms the button #113 commit), which still left
CHARGE BATTERYstuck and mixed two rules; and replacing the factory bootloader, which makes everything work but needs a bootloader flash on every unit, where a power loss mid-write leaves a unit only SWD can recover. The cost of the choice: the glass does not change on its own when a charger goes in.A refused press is not cheap.
boot_pathneeds the cell reading and the stored trim, so it runs afterboard_.begin(): every part is probed, the radio and the panel are set up, Bluetooth advertises, and an IMU unit hashes its stored image. Not measured yet. Movingread_stored_imu_image()after the refusal is a safe first cut, not done here.One recovery margin for every step, because the spread belongs to the reading, not to the step.
firmware/README.mdsaid hysteresis "never becomes a constant". This adds an exception for a measured gap shared by every step of a ladder. Two named exit thresholds are the alternative if you prefer the old rule.Flight run without the airborne cadence. Ground-flagged bursts at 1 Hz break G.1.16, and airborne-flagged ones would put a false aircraft on air. So the run models the transmit it does not send (54 bursts of 5 ms a minute at 90 mA, about +0.40 mA) on a separate line, outside the modelled total. Measuring it for real needs test firmware into a dummy load or a shielded box.
No flight log in a flight run. A 60-hour bench run would push real flights out of the ring, and nothing could tell a simulated flight from a real one. The cost left out is a 24-byte write every 4 s and an erase every 11 minutes.
Data and rollout
powergains no field,bootgains a flagged profile byte. The schemas and READMEs say "builds before 2026-10-05", because this change has no build number yet.POWER RUNorFLIGHT RUN.website/content/pages/power{,.fr}.html.erbstill say three samples to leave a step.website/content_images/pages/skyblip-go/capture.png) and the "two captures" prose incontrols{,.fr}anddisplays{,.fr}need retaking with emscripten.Risks and edge cases
boot_pathrefuses onLowPowerWakewithUsbVbus, but a VBUS wake sets only RESETREAS bit 20, which Zephyr's hwinfo folds intoRESET_POR, so the chip reportsUsbVbus | PowerOn. Hidden today because the bootloader takes VBUS wakes first. Worth one bench check before relying on it.FLAT BATTERYon the glass without a refresh, and the next press should behave the same.setup()returns. The static constructors beforemainare still unsupervised.Tests
make -C firmware test: 1848 passed, 37 skipped.test/boards/test_board_battery.cpp), the gauge refuses the rail, and the codec and status reply match.FLAT BATTERY.test/products/test_flight_run.cpp): the cadence stays at 3 positions and 3 names per 30 s, every burst says on ground at the real position, and a stop leaves no flight time and no flight-log session.make -C firmware test-sanitize: pass.make -C firmware tidy(clang-tidy 22.1.8) andclang-format 23.1.1 --dry-run --Werrorover the tree: pass.check_test_coverage,check_adsl_clauses,check_tuning_names,check_status_schema,test_mkuf2,test_check_imu_image,check_no_dfu_package,test_blip,test_power_budget,test_link_budget: pass.node --testinsimulator/: pass.docs/BEHAVIOR.mdanddocs/TUNING.mdregenerate without a diff.Closes #114, closes #115. Closes #113. Refs #109.
Screenshots live on refs/assets/github under pr/118-fix-power-bench-runs/.