diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index a06cc01..754e72a 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -623,11 +623,57 @@ until `releases/v/acceptance.json` is committed. ## Hardware facts bank (measured) +- **The factory's envelope, decoded** (firmware 2.6.0-2228, the 23 captured + headers, this board's own factory logs). The pulse header is the job's + operating envelope and the factory refuses to cut without it: 29 tags are + mandatory, 346 are header-legal, an unknown tag is logged and skipped. The + service fills the fan duties, the coolant window, the per-sensor temperature + ceilings, the lid IR thresholds, the accelerometer thresholds and an HV + current cap per job; a cut job carries the real fan duties (air assist + 1023, exhaust 65535, intake 43278, equal to ForgeFIRM's run profile) and a + hunt or motion file carries air assist 204 with the extraction fans off. + Every tach window is zero in every capture except `AArx` 64500 on cuts, and + the factory's intake and exhaust tach monitors treat zero as not configured, + so a stalled extraction fan is caught there by the temperature it causes; + when a fan alert does fire during a cut the factory pauses the print on the + same transition as a user pause. Temperature runs two tiers there: a plain + alert pauses, a `*_temp_critical` fails the machine; the units are per + sensor (the coolant family is carried twice, raw counts with the NTC's hot + end as "min", and millidegrees), and a stock machine's live coolant window + is 10 to 30 C idle and 5 to 35 C warm-up and run, `CMrx` 33000 on a cut + being exactly the shipped 33 C ceiling. The factory does not verify coolant + flow: the calorimetric `CF` controller in its firmware is never armed and + its heater is written only at phase changes. ForgeFIRM's answer, all of it + landed and bench-proven in the catalog (`cooling.gate-off`, + `cooling.fan-gate-trips`, `cooling.critical-tier`, the hunt leg of + `cloud.mode-switch`) and in the CAMPAIGN-LOG drills: every gate a plain + setting with an off end; a header value only ever tightening a local one; + every fan held to a measured floor with a fault, not a pause, for the + session; a coolant critical line above the ceiling's pause; the board + temperatures watched per job; and the rest of the envelope declared, tag by + tag, in `CLOUD.md` "The pulse header". - **Board temperatures at idle** (room ~22 C, machine on for hours): the chassis LM75 reads **29.0 C**, `pic/pwr_temp` reads **589 raw** (the - unverified guess `raw * 0.08715 - 21` would make that 30.3 C; a thermometer - on the supply heatsink decides). Ranged per job by the engine from here - on. + unverified guess `raw * 0.08715 - 21` would make that 30.3 C), the SoC die + **42.8 C**. Ranged per job by the engine from here on. The supply stays a + raw count by decision: its heatsink cannot be reached with a thermometer + while the machine runs, so the conversion is not going to be verified on + this bench, and a number nobody has checked is not published as degrees. + The per-job range in raw counts is the record, and a ceiling, if one is + ever wanted, is set in raw counts from it (`temp_calibrate.py supply-*` + stays for a machine where the heatsink is reachable). +- **The SoC guards itself.** The i.MX6DL (rev 1.3) on-die monitor is + `thermal_zone0` (`imx_thermal_zone`, the same node as `hwmon0`), governor + `step_wise`, trips at **85 C passive** and **90 C critical** (the + consumer-grade points the driver derives from the fuses: hot point 95, + critical at hot minus 5, passive at hot minus 10). The passive trip is + bound to `cpufreq-cpu0` (996 / 792 / 396 MHz, governor `ondemand`) and + both GPU cooling devices; the critical trip is the kernel's orderly + poweroff. A throttle slows the engine, the camera and the protocol thread + before the step stream (the ring is in hand). The factory board carries + **no heatsink or fan on the SoC**, only the mounting holes for one; the + factory's load never needed it, and ForgeFIRM's may, which is what the + per-job SoC range and the throttle log line are there to show. - **Fan speeds at the cut profile** (exhaust duty 65535, intake 43278, air assist 1023; sampled at 1 Hz over 120 s from idle, the exhaust duct's inline booster fan off): exhaust **11640 rpm** steady (spread 11444 to @@ -975,7 +1021,10 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`. by tracking it, and the watch could be re-armed on fixed numbers after all. Unproven: it assumes the header's quartiles map onto the raw channels and share their units. Confirm both by reading the four channels while stepping - the lamp, then compare against the header the next cloud job carries. + the lamp, then compare against the header the next cloud job carries. By + decision those header thresholds (`IR??`) are the prior for this redesign + and nothing else: the cloud client declares them ignored, and the watch + stays disabled until it is lamp-aware. 5. **Limit-switch homing.** The planned second homing method (`$22` stays 0 until it lands); printable brackets are in `3d-models/`. Also: calibrate `gfcloud_home_x/y` against a jog to a known reference if the factory corner @@ -1013,10 +1062,11 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`. `LCfl`. What is left is tracked in `python3-gfhardware/forgefirm-app/docs/CLOUD.md` "Outstanding items" and is short: whether the service accepts an 8 MP machine's larger images (no - HD machine has been on the bench), and the pulse header's unenforced - safety envelope, which is item 19, listed separately because the - enforcement lands in the cooling engine and has to hold in GRBL mode too, - not only under the cloud client. The memory guards + HD machine has been on the bench). The pulse header's envelope is + settled: every tag the service fills in is applied, passed through as a + limit that can only tighten, refused on, logged or declared ignored with + its reason (`CLOUD.md` "The pulse header"), and the gates behind it live + in the cooling engine so they hold in GRBL mode too. The memory guards (`pulse_reject_threshold_bytes`, 128 MiB of compressed body) stay reasoned rather than measured, by decision: nothing the service sends comes near them, and every job logs the body and program sizes the @@ -1322,194 +1372,29 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`. line does to it, and how it composes with the armed window's disarm grace across a long hold. -19. **Pulse-header envelope (cloud mode).** The pulse header is the job's - operating envelope, and the factory refuses to cut without it: 29 of its - tags are mandatory, a header missing one is rejected, and a known tag that - is not header-legal is rejected too. The service fills the safety-relevant - ones with real values per job rather than echoing back what the machine - reported. ForgeFIRM applies thirteen tags (the three run fan duties, - `STfr`, the X/Y current, decay and microstep set, `ZSmd`), refuses the - file on two (`MCsn`, the serial it is locked to, and `PDfm`, the pulse - data format, both checked before a byte reaches the ring) and drops the - rest. Seventeen of the mandatory ones are among the dropped. - - **Landed first, the pattern every gate ships on:** a gate is a plain - setting with a wide legal range, a recommended band, and an off end (a - ceiling at its maximum, a window of zero) that turns the gate off by - value, with no separate switch; the panel warns outside the band and - while any gate is off, the engine logs each gate setting at every run - start, `/status` and `/cool/status` carry `gates_off`, and an off gate - keeps measuring. Applied to the coolant ceiling, its resume gate, the - flow window and the flow rise (`forgectrl/src/gates.c`, `SERVICES.md` - "Gate settings", `COOLING.md` §8a, `cooling.gate-off` in the catalog, - bench PASS 2026-08-21 on dev image `20260821210903`). - **The pass-through is in:** the cloud client derives the job's limits - from the header (`CMrx`/`CMrn` as degrees, `EFrx`/`IFrx`/`AArx` as the - minimum speeds their maximum periods mean) and rides them on every - `/cool/state`; the engine resolves each as the stricter of local and - header, never loosening and never overruling an off gate, logs the - effective set and publishes it in `/cool/status`; the coolant ceiling - is the live consumer and the fan floors follow - (`cloud.pause-resume` checks both log lines on a real print; bench PASS - 2026-08-21 on dev image `20260821220926`, the service's hunt windows - ignored as looser and the print's 33 C window matched). **The airflow - gates are in** (`forgectrl/src/airflow.c`): every fan held to a floor - while the run profile is applied (exhaust, intakes and air assist by - tachometer, purge by current), a spin-up grace, three ticks under the - floor, a fault for the rest of the session (`AIRFLOW`, no resume), a - header window only ever raising a floor. A fan is judged at the - operating point its floor was measured at: always while armed (when a - job's profile may raise a fan but never lower it below the run duty), - and unarmed whenever it is commanded at the run duty; a cloud hunt, - sent with the extraction fans off, is measured and not judged. - `cooling.fan-gate-trips` and the hunt leg of `cloud.mode-switch` in - the catalog, both PASS on the pinned dev image `20260822135848` - (campaign 18 of 18). The shipped floors are 55 percent of the bench - machine's measured steady speeds (facts bank: exhaust 6400, intake - 2290, air assist 6000 rpm, purge current 300, grace 15 s). The - unplugged-exhaust-fan drill PASS on the same image: `AIRFLOW` at - grace plus three ticks with the exhaust dead, the other fans held, - the reason relayed on the Grbl port, and the replugged fan `ok` inside - the next session's grace (CAMPAIGN-LOG). From the drill: the fault - ends with its run session (decided; a standing hold at idle canceled - GRBL jogs and would have refused the cloud print that re-proves the - fan), since every session judges every fan afresh after the grace; - `cooling.fan-gate-trips` checks it, PASS on dev image `20260822145201`. - **The coolant critical tier is in:** `cool_temp_critical_c` (default - 38 C, 6 to 70, off at 70, kept above the ceiling by the settings - cross-check) is the fail tier above the ceiling's pause: at or over it - in a run session the verdict is `CRITICAL` (fire blocked, hold, no - resume this job), the fault ends with the session, no header touches - it; `cooling.critical-tier` in the catalog (45), PASS on dev image - `20260822154257`, and the warm-loop drill (`critical_tier_drill.py`) - PASS the same day on a genuinely rising loop: `OVERTEMP` at the - ceiling, `CRITICAL` four seconds later at the line, the fault ending - with the session (CAMPAIGN-LOG). **The board temperatures are watched:** - the chassis LM75 (degrees) and the supply sensor (raw count, its - conversion still unverified) ride `/status` as `temps`, show on the - Status tab, and are ranged over every run session into one run-end - log line; no gate behind either until the record says where one - belongs, and the supply's conversion waits on thermometer points at - the heatsink during a long cut (`temp_calibrate.py supply-point`, - three points, then `supply-fit`). Bench run of the status fields and - the log line owed on the next image. - - Nothing here can put energy where it was not commanded: the hardware chain - is the emission boundary and no header field touches it, and forgectrl runs - its own coolant ceiling, flow verification, emission witness, liveness gate - and silence timeout. The gap is that the envelope enforced is ForgeFIRM's - fixed one rather than the job's, and that several failure modes the factory - watches have no counterpart here at all. In rough order of what a failure - would cost: - - - **Fan tachometers gate nothing.** Every tach is read for `/status` and - the dashboard and none is checked against a limit. A fan that stalls - mid-cut is invisible. Exhaust is the fume path and is the one to close - first. The tach conversions are already in the facts bank; what is - missing is a gate and a verdict, which belongs in the cooling engine next - to the flow check. - - **What the factory does here is now settled, and it is not what the - header suggests.** Three facts, and they pull in different directions. - - First, the header windows are empty and always have been. A cut job - carries real fan duties (`AArd` 1023, `EFrd` 65535, `IFrd` 43278); a - motion or hunt file carries `AArd` 204 and the other two at zero, which - is correct, since those jobs do not cut. But `AArn`, `AArx`, `EFrn`, - `EFrx`, `IFrn` and `IFrx` are zero in both kinds, and zero in pulse files - captured from a stock factory machine on the live service, and zero in - that machine's own settings report. The only movement in nine years is - that the service started setting `AArx` to 64500 on cut jobs. Nor do the - limits arrive out of band: across every captured session, counting every - action type, the service has ever pushed seven keys, all image, camera - and network. No limit that bounds the machine reaches it that way. - - Second, the factory's own monitors are mostly disabled anyway. The intake - and exhaust tach monitors install a lenient evaluator that treats a zero - limit as "not configured" and returns in-range, so those two are disabled - twice over. Air assist keeps the strict evaluator and does receive - `AArx`, so it is the one fan monitor that can fire. Because the tach - attributes are pulse *periods* and the kernel reports 0 for a stopped or - absent tach, that alert means "no usable tach signal", not "slow fan". - A stalled extraction fan is not caught by tachometer on a factory - machine; it is caught by the temperature it causes. - - Third, and this is the part worth adopting: when a fan tach alert does - fire during a cut, the factory **pauses the print**. It posts a - `HARDWARE_ALERT` to the hardware task, and in the running state that - event takes exactly the same transition as a user pause, differing only - in one flag. Not an abort, not a warning: a pause. - - So the policy is known even though the numbers are not. Pick thresholds - on the bench, let a header value only tighten them, and make the verdict - a pause rather than an abort. - - **Only the coolant loop has a temperature ceiling.** `cool_temp_max` on - the upstream sensor is the whole thermal gate. Board, head, interconnect, - lid, fused and power-supply temperatures are read and displayed and gate - nothing, though the header carries a run max, a warmup max and a critical - max for each, and `PTmn`/`PTmx` on the supply are mandatory. - - **The factory stops for temperature far more readily than it does for - fans, and the scale question is answered.** Its plain temperature alerts - raise an aggregate `alarm` condition which posts `ENVIRONMENTAL_ALERT`, - and during a cut that pauses the print on the same transition as a user - pause. Its `*_temp_critical` conditions are in the machine-unusable set - and post `FAILURE` instead, which is a different state and not a pause. - Two tiers, then: every temperature alert pauses, and a critical fails the - machine. - - The units are per-sensor, not universal, which is what made the header - numbers look wrong. The coolant loop is the worked example: the factory - carries the same six per-phase limits twice, `CT{i,w,r}{n,x}` in raw ADC - counts and `CM{i,w,r}{n,x}` in millidegrees, and the raw family's "min" - is the *hot* limit because the thermistors are NTCs. Run the firmware's - own beta conversion over the compiled `CT` defaults and round numbers - fall out (4.01 to 50.01 C idle, 1.02 to 31.04 C warmup and run), which is - the check that the conversion is right. A stock machine's live `CM` - values are 10 to 30 C idle and 5 to 35 C warmup and run, with 1.0 C of - hysteresis on each end. The conversions live in `UAPI.md`. - - What remains is per-sensor: establish each family's scale from its own - conversion before adopting any ceiling, and remember the factory reads - four locations where ForgeFIRM exposes one chassis sensor, so three of - the four header ceilings have nothing here to compare against. - - **Coolant flow is not verified by the factory either, and its heater is - idle.** Worth knowing before treating the factory as the reference for - the cooling engine's flow check. The loop is built for calorimetric flow - detection, a heater in line between two thermistors, and the firmware - carries a full closed-loop controller for it: the `CF` family, with a - setpoint, proportional and integral gains, a differential-temperature - readout and min/max limits on it. None of it is armed. The service never - sends those settings, a stock machine reports none of them, and the three - faults it would raise (`coolant_flow_alert`, `coolant_flow_fault`, - `coolant_heater_fault`) have no raiser anywhere in the factory image. The - factory watches one coolant thermistor against a per-phase window with - hysteresis, and that is all. Its heater is written only at phase changes, - from a configured per-phase value, and nothing reads the result back. - ForgeFIRM's flow verification is therefore ahead of the factory, not - behind it, and nothing about the factory's numbers should be used to - weaken it. - - **No crash or tilt abort.** The head accelerometer is used only as the - motion-liveness probe. The factory runs two tiers off the same sensor and - the header supplies both: a per-axis *alert* threshold, which posts - `MACHINE_INITIATED_PAUSE` and so pauses the print, and a separate per-axis - *abort* threshold, which posts `ABORT`. `HAxr`, `HAyr` and `HAar` arrive - every job. The lid accelerometer and the two tilt conditions ride the same - structure, with `lid_tilt` and `head_tilt` grouped with the fan alerts - rather than with the accelerometer ones. - - **Beam detect is unread.** The kernel carries the latch; no userspace - reads it. In the factory, `beam_detect_alert` pauses the print and - `beam_detect_abort` aborts it, and a `beam_detect_report` is uploaded - afterward at whatever severity the report-upload condition selects. See - also item 15, which is circling the same hardware from the other side. - - Deciding what to adopt is part of the work, not a foregone conclusion. A - limit that arrives from a remote service is a limit that service can raise, - so the sane shape is probably to take the header value as a ceiling that - can only tighten a locally configured one, never loosen it. Whatever lands - needs acceptance coverage in the same change. +19. **Head crash and rail-contact detector (planned).** The head + accelerometer is the motion-liveness probe and nothing more; the + factory runs two tiers off the same sensor (a per-axis alert that + pauses, a per-axis abort), and its thresholds arrive in every pulse + header in a unit and behind a filter that are not known. A detector + here is bench-measured from scratch, not adopted from the header: the + rail-contact signature in the facts bank (a 20 to 40 times jump within + 4 ms on a fast strike, near-silent on a slow one) is the starting + point, and the header values are only a cross-check once the units + are established. A pause on contact, on the factory's shape, would be + the first use. **Deliberately not gated:** an armed GRBL job after an underrun cuts at the stale origin unless homing is required (GRBL mode permits unhomed cutting; the underrun itself alarms and unlinks the anchor). Not in the acceptance catalog -by design, for the same reason. +by design, for the same reason. From the pulse-header envelope, each by +decision and each declared in the cloud client so every job's log counts it +as decided rather than missed: the warm-up fan profile (the run profile +covers the warm-up hold), the supply temperature window (the service sends +the whole ADC range and the factory binds it to nothing; the supply is +watched per job instead), the head, lid, interconnect and fused temperature +ceilings (no sensor at those locations; the chassis is watched per job), the +head accelerometer thresholds (item 19), the lid IR thresholds (item 4), the +HV current caps (the sampled emission witness covers the idle case, and HV +current is ranged per job), the thermal report upload conditions and the +pump flag. Beam detect stays with item 15. diff --git a/docs/COOLING.md b/docs/COOLING.md index d00058f..f7b659d 100644 --- a/docs/COOLING.md +++ b/docs/COOLING.md @@ -436,12 +436,17 @@ Stated plainly so nobody counts on them: cannot be detected (the output has no readback), so this will become a user setting plus a simple hysteresis around the factory's setpoints. - **A fire watch that acts** (§7). -- **Chassis and supply ceilings.** Both temperatures are measured and not - gated: the chassis LM75 in degrees and the supply sensor as a raw count, - in `/status` as `temps`, and ranged over every job in one log line - (`temps this job: ...`). A ceiling for each comes from that record once - there is enough of it, and the supply's conversion from a thermometer on - its heatsink (`temp_calibrate.py supply-point`). +- **Chassis, SoC and supply ceilings.** Three temperatures are measured and + not gated: the chassis LM75 and the SoC die in degrees, the supply sensor + as a raw count, in `/status` as `temps` (with the kernel's CPU throttle + state beside them), on the Status tab, and ranged over every job in one + log line (`temps this job: ...`, naming a throttle if one happened). The + SoC already guards itself (the kernel throttles the CPU at 85 C and + powers the board off at 90 C on this part). A ceiling for each comes + from that record once there is enough of it. The supply's conversion + stays unverified by decision (its heatsink is not reachable with a + thermometer while the machine runs), so its reading stays a raw count + and any ceiling for it would be set in raw counts too. - **Fan floors measured on more than one machine.** The shipped floors are a fraction of one bench machine's run-duty speeds; a machine whose fans read differently sets its own (§8), and a floor of zero turns that gate diff --git a/forgetest/forgetest/suite/cooling.py b/forgetest/forgetest/suite/cooling.py index 23a20e2..d3eda1a 100644 --- a/forgetest/forgetest/suite/cooling.py +++ b/forgetest/forgetest/suite/cooling.py @@ -271,8 +271,8 @@ def _tail_has(fc, needle): "engine must skip the gate (verdict OK), report it in gates_off on /status " "and /cool/status, say so in the settings reply, and log the run-start line; " "restored, everything reads as before. Alongside: /status carries the watched " - "board temperatures (chassis in degrees, supply as a raw count) and every run " - "session ends with them ranged into one log line.") + "board temperatures (chassis and SoC die in degrees, supply as a raw count, the " + "CPU unthrottled) and every run session ends with them ranged into one log line.") def gate_off(ctx): fc = ctx.forgectrl ev = ctx.evidence @@ -291,6 +291,10 @@ def gate_off(ctx): "/status temps.chassis_c is not a plausible chassis temperature: %s", temps) ctx.check(isinstance(temps.get("supply_raw"), int) and 0 <= temps["supply_raw"] <= 1023, "/status temps.supply_raw is not a 10-bit count: %s", temps) + ctx.check(isinstance(temps.get("soc_c"), (int, float)) and 10.0 <= temps["soc_c"] <= 100.0, + "/status temps.soc_c is not a plausible die temperature: %s", temps) + ctx.check(temps.get("soc_throttle") == 0, + "the CPU is throttled (or its cooling device is missing) at idle: %s", temps) c0 = _cool(fc) ctx.check(c0.get("verdict") == "OK", "engine is not at OK before the test: %s", c0) ctx.check(c0.get("gates_off") == [], "a gate is already off: %s", c0.get("gates_off")) @@ -349,8 +353,9 @@ def gate_off(ctx): ctx.log("restored ceiling %s reports state %s", val, state) # Every run session ends with the board temperatures ranged # into one line. - ctx.check(_tail_has(fc, "temps this job: chassis "), - "the run-end board-temperature line is missing from the forgectrl log") + ctx.check(_tail_has(fc, "temps this job: chassis ") and _tail_has(fc, ", soc "), + "the run-end board-temperature line (chassis, soc, supply) is missing from the " + "forgectrl log") finally: if not restored: # The engine reads settings at run start only: restoring diff --git a/forgetest/tests/test_cooling_suite.py b/forgetest/tests/test_cooling_suite.py index 8a0f7a1..028bb44 100644 --- a/forgetest/tests/test_cooling_suite.py +++ b/forgetest/tests/test_cooling_suite.py @@ -229,7 +229,8 @@ class GateOffTests(unittest.TestCase): cooling.SESSION_END_WAIT_S = 3 self.fc.state["status"] = dict(self.fc.state["status"], coolant={"down_c": 22.4, "up_c": 22.3, "pump": True, "tec": False}, - temps={"chassis_c": 29.0, "supply_raw": 589}, + temps={"chassis_c": 29.0, "supply_raw": 589, "soc_c": 42.8, + "soc_throttle": 0}, gates_off=[]) self.fc.state["cool"] = {"phase": "idle", "verdict": "OK", "fire_ok": False, "hold": False, "gates_off": []} @@ -284,7 +285,7 @@ class GateOffTests(unittest.TestCase): if self.temps_line: self.fc.state["logs_tail"]["text"] += ( "Aug 22 12:00:30 forgectrl: cool: temps this job: chassis 29.0..29.4 C, " - "supply raw 587..592\n") + "soc 42.8..47.1 C, supply raw 587..592\n") time.sleep(0.2) cool["phase"] = "idle" threading.Thread(target=end, daemon=True).start() diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc index 148225a..b8004d4 100644 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc +++ b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc @@ -2,5 +2,5 @@ # only SRCREV and PV here - the image manifest leaves *-pin.inc out of the # layer content hash because the component entry already identifies the # pinned source (forgefirm-image-manifest.bbclass). -SRCREV = "368fd0c6bb4ce0eea6e9fad39ec4dfec593f914d" +SRCREV = "b27398a393ec2d3fb407cf2ab5f7dfba3dee19d3" PV = "0.1.0" diff --git a/meta-forgefirm/recipes-forgefirm/forgefirm-app/forgefirm-app-pin.inc b/meta-forgefirm/recipes-forgefirm/forgefirm-app/forgefirm-app-pin.inc index e571fc2..84238e0 100644 --- a/meta-forgefirm/recipes-forgefirm/forgefirm-app/forgefirm-app-pin.inc +++ b/meta-forgefirm/recipes-forgefirm/forgefirm-app/forgefirm-app-pin.inc @@ -4,9 +4,9 @@ # because the component entry already identifies the pinned source # (forgefirm-image-manifest.bbclass). The python3-gfhardware recipe in # meta-glowforge-bsp pins the same repository; move both together. -SRCREV = "81027ffc3617c092e0fa23005d599229ebe57d61" +SRCREV = "e65cfc286a352b6a73aa64f973d1864cdc1e3f34" # Bump PV with every SRCREV move: the hash-derived package version is not # monotonic on its own and buildhistory QA fails the build when it sorts # backwards. -PV = "0.1.15+git" +PV = "0.1.16+git"