← Commit history
build/stm32-bringup/build-manifest.json+7−7
@@ -29,12 +29,12 @@   "sources": {     "firmware/fan_tach.c": "844f00c1f77e39529bd8f667d543ecee187ff4fe91108aedb7a2c36eb22846fa",     "firmware/fan_tach.h": "20fc7b0bbb1a040dfd0453e3af22cf62e7a564241c28cf9cd08dd9b6d87ddf80",-    "firmware/instruments.h": "1bcd895abf8fe74c5a96028b8b559eafdff0919e970244d4445ccbd72b7a6ee6",+    "firmware/instruments.h": "47756a2358e94e143b01bc1f99e5987b9fd8a01d81d762b0b490e96cebeb3151",     "firmware/load_core.c": "85017ff0a3e37e7b5e41604989326fe623e7f73427ecd8e1a29c63a38d003638",     "firmware/load_core.h": "408263af21e6c15e82f4118343a2b7d0dd2db61808754c0b91021acd1146fadf",-    "firmware/load_protocol.c": "208d7e7fb9a05da31c3c200b018f5b188e53ac3d95ac2288a61f683b4f3df1af",+    "firmware/load_protocol.c": "63b4404a0e510eb2e472d8f7f93b1aa78155fd171312b941948ab85346fc2b61",     "firmware/load_protocol.h": "f64f6b689d57501031515e8ee9f8c8349a2cea63738e73db16f95e747caf91fb",-    "firmware/stm32/README.md": "da1875c99b20608e8bdb6bba59d3a3dc045261cfa5bbcf2df1792089f31f4dc4",+    "firmware/stm32/README.md": "3b899060dee0a9e74e0841d6c220dd68d3211479b16ccaf3a700e6668c6526b0",     "firmware/stm32/clock.c": "f42f5c1ff229b065a048218ecc575624276aaa229e0bc340d80c80eec35416d2",     "firmware/stm32/dependencies.json": "7e8f3c17a59c856371a9f411dce6a9dadafbb8c8edd5ede5e4e5cf46b1567e92",     "firmware/stm32/fan_input.c": "d4ebe5572dbbcd1a3ea84c4e31442732a7237e94d554ec8acdc07142486e92f8",@@ -52,10 +52,10 @@     "tools/build_stm32_bringup.py": "20f8d74fa7bfc8d3ea9a4ba5fce1b912b98a88389285111ff70d58fde4aa93f2"   },   "artifacts": {-    "adom-load-bringup.hex": "1ee596b32a04a79ea6994515f4bc4d7e50e9c6afa037a40a65864f95d53b2e55",-    "adom-load-bringup.bin": "2f4b39f60ba4e6cd5af8de22b2224af326ea31ea9eafc68992a42e50531b734a",-    "adom-load-bringup.map": "38255082269a58d4b152f41c2cc6d3a7962eada683b12971c69e3541cdc08ab6",-    "adom-load-bringup.elf": "9ef8fce195c337c3e47d37be92f47b64cd6f189a4b57df7e1d72b5a441c2852e"+    "adom-load-bringup.hex": "41ddfee1467efabf8a871ed952a7b7418ded0120a28f9d31ef6c5fe1a46e50a6",+    "adom-load-bringup.bin": "839b1ec5e860f0b56dbba6362918d45148132c44dd86f37a9dcc4732fda42c97",+    "adom-load-bringup.map": "d74c18353387c363be04b9173ab34906ed8ffee994d1024855b2a2da0f2ec8fc",+    "adom-load-bringup.elf": "a83740bc95215f972e2035e75276d59bc9cef8026a33055e2e5ef7dbea509d5e"   },   "commands": [     [
control-panel/index.html+2−2
@@ -6,7 +6,7 @@ <header><strong>Adom / Electronic load</strong><button id="settingsButton" aria-label="Settings" title="Connection and application settings">&#9881;</button></header> <main><div class="intro"><div><h1>Constant-current load</h1><p>Set the load. Monitor the buck converter. Capture every measurement.</p></div><span class="badge" id="link">Disconnected</span></div> <div class="metrics"><div class="card"><div class="label">DUT voltage</div><div class="value"><span id="volts">--</span> <span class="unit">V</span></div></div><div class="card"><div class="label">Measured current</div><div class="value"><span id="amps">--</span> <span class="unit">A</span></div></div><div class="card"><div class="label">Dissipated power</div><div class="value"><span id="watts">--</span> <span class="unit">W</span></div></div><div class="card"><div class="label">Case temperature</div><div class="value"><span id="case_c">--</span> <span class="unit">°C</span></div></div></div>-<div class="layout"><section class="panel"><h2>Load control</h2><label for="current" class="label">Requested current (A)</label><input id="current" class="number" type="number" min="0" max="1" step="0.01" value="0.10"><input id="slider" aria-label="Requested current" type="range" min="0" max="1" step="0.01" value="0.1"><div class="small">Initial buck profile: 0–1 A, maximum 5 W.</div><div class="actions"><button id="apply" disabled>Apply current</button><button id="arm" class="primary" disabled>Enable load</button><button id="off" class="off" disabled>Load off</button></div><dl class="details"><dt>Device state</dt><dd id="state">UNKNOWN</dd><dt>Applied demand</dt><dd id="demand">-- A</dd><dt>Fan speed</dt><dd id="rpm">-- rpm</dd><dt>Instrument</dt><dd id="serial">Not bound</dd><dt>Control link</dt><dd>USB / 3.3 V UART</dd></dl><button id="connect">Connect instrument</button><button id="clear" disabled>Clear fault</button></section>+<div class="layout"><section class="panel"><h2>Load control</h2><label for="current" class="label">Requested current (A)</label><input id="current" class="number" type="number" min="0" max="1" step="0.01" value="0.10"><input id="slider" aria-label="Requested current" type="range" min="0" max="1" step="0.01" value="0.1"><div class="small">Initial buck profile: 0–1 A, maximum 5 W.</div><div class="actions"><button id="apply" disabled>Apply current</button><button id="arm" class="primary" disabled>Enable load</button><button id="off" class="off" disabled>Load off</button></div><dl class="details"><dt>Device state</dt><dd id="state">UNKNOWN</dd><dt>Applied demand</dt><dd id="demand">-- A</dd><dt>Sensor readings</dt><dd id="sensorStatus">Unavailable</dd><dt>Assembly</dt><dd id="assemblyStatus">Unqualified</dd><dt>Fan speed</dt><dd id="rpm">-- rpm</dd><dt>Instrument</dt><dd id="serial">Not bound</dd><dt>Control link</dt><dd>USB / 3.3 V UART</dd></dl><button id="connect">Connect instrument</button><button id="clear" disabled>Clear fault</button></section> <section class="panel"><h2>Measurement history</h2><div id="empty" class="empty"><strong>Waiting for the physical load</strong><span>Live readings appear after identity and OFF checks pass.<br>No simulated measurements are displayed.</span></div><svg id="chart" role="img" aria-label="Recent measured load power" viewBox="0 0 650 205"><path d="M40 10V175H640 M40 92H640" stroke="#30363d" fill="none"/><text x="4" y="16" fill="#9ba8b9" font-size="12">5 W</text><text x="6" y="178" fill="#9ba8b9" font-size="12">0 W</text><polyline id="trace" stroke="#00b8b1" stroke-width="2" fill="none"/></svg><div class="foot"><span>Measured power from device voltage × current</span><a href="api/export.csv">Export CSV</a></div><div id="events"></div></section></div> <div id="notice" role="status">Engineering prototype. MCU port and physical qualification remain pending.</div><div class="foot"><span>Independent hardware shutdown + firmware watchdog are part of the design.</span><span class="warning">5 W bring-up ceiling</span></div></main> <dialog id="settings"><h2>Settings</h2><div id="settingsText"></div><p class="small">The server binds an exact instrument path and serial at startup. Connection, load enable and current demand are never restored automatically.</p><label class="label" for="ttl">Idle shutdown (hours)</label><input id="ttl" class="number" type="number" min="0.1" max="168" value="24"><button id="saveSettings">Apply</button><button id="closeSettings">Close</button></dialog>@@ -15,7 +15,7 @@ const $=s=>document.querySelector(s);let token='',state={},own=false,busy=false, async function post(path,body){const r=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json','X-Load-Token':token},body:JSON.stringify({aiThread:caller,...body})});const d=await r.json();if(!d.ok)throw Error(d.error||'Command rejected');return d} function notice(s){$('#notice').textContent=s} async function cmd(cmd,args={}){try{const d=await post('api/cmd',{cmd,...args});render(d.state);if(cmd==='arm')own=true;if(cmd==='off')own=false;notice(cmd==='off'?'Load OFF confirmed.':cmd==='arm'?(d.state.enabled?'Load enabled. Keep this control page active.':'Arm accepted; waiting for confirmed output enable.'):'Command accepted: '+cmd)}catch(e){own=false;notice(e.message)}}-function render(s){state=s;for(const k of ['volts','amps','watts','case_c'])$('#'+k).textContent=Number.isFinite(s[k])?s[k].toFixed(k==='case_c'?1:3):'--';$('#link').textContent=s.connected?'Connected / '+s.state:'Disconnected';$('#state').textContent=s.state;$('#demand').textContent=Number.isFinite(s.applied_a)?s.applied_a.toFixed(3)+' A':'-- A';$('#rpm').textContent=s.rpm_valid&&Number.isFinite(s.rpm)?Math.round(s.rpm)+' rpm':'Unknown / unqualified';$('#serial').textContent=s.serial||'Not bound';for(const id of ['apply','off'])$('#'+id).disabled=!s.connected;$('#arm').disabled=!s.connected||s.state!=='OFF'||!s.rpm_valid||s.rpm<5000||s.rpm>10000;$('#clear').disabled=!s.connected||s.state!=='FAULT';$('#connect').disabled=s.connected;if(!s.connected||s.state==='FAULT'||s.state==='OFF')own=false;if(s.updated_at&&s.updated_at!==lastUpdate&&Number.isFinite(s.watts)){lastUpdate=s.updated_at;history.push(s.watts);if(history.length>180)history.shift();$('#trace').setAttribute('points',history.map((v,i)=>(40+i*600/179)+','+(175-Math.max(0,Math.min(5,v))*33)).join(' '));$('#chart').style.display='block';$('#empty').style.display='none'}if(s.error&&s.error!==lastError)notice(s.error);lastError=s.error}+function render(s){state=s;for(const k of ['volts','amps','watts','case_c'])$('#'+k).textContent=Number.isFinite(s[k])?s[k].toFixed(k==='case_c'?1:3):'--';$('#link').textContent=s.connected?'Connected / '+s.state:'Disconnected';$('#state').textContent=s.state;$('#sensorStatus').textContent=s.sensors_valid?'Valid':'Unavailable / invalid';$('#assemblyStatus').textContent=s.assembly_qualified?'Qualified':'Unqualified — enable locked';$('#demand').textContent=Number.isFinite(s.applied_a)?s.applied_a.toFixed(3)+' A':'-- A';$('#rpm').textContent=s.rpm_valid&&Number.isFinite(s.rpm)?Math.round(s.rpm)+' rpm':'Unknown / unqualified';$('#serial').textContent=s.serial||'Not bound';for(const id of ['apply','off'])$('#'+id).disabled=!s.connected;$('#arm').disabled=!s.connected||!s.sensors_valid||!s.assembly_qualified||s.state!=='OFF'||!s.rpm_valid||s.rpm<5000||s.rpm>10000;$('#clear').disabled=!s.connected||s.state!=='FAULT';$('#connect').disabled=s.connected;if(!s.connected||s.state==='FAULT'||s.state==='OFF')own=false;if(s.updated_at&&s.updated_at!==lastUpdate&&Number.isFinite(s.watts)){lastUpdate=s.updated_at;history.push(s.watts);if(history.length>180)history.shift();$('#trace').setAttribute('points',history.map((v,i)=>(40+i*600/179)+','+(175-Math.max(0,Math.min(5,v))*33)).join(' '));$('#chart').style.display='block';$('#empty').style.display='none'}if(s.error&&s.error!==lastError)notice(s.error);lastError=s.error} $('#slider').oninput=()=>$('#current').value=Number($('#slider').value).toFixed(2);$('#current').oninput=()=>$('#slider').value=$('#current').value; $('#apply').onclick=()=>cmd('set_current',{amps:Number($('#current').value)});$('#arm').onclick=()=>cmd('arm');$('#off').onclick=()=>cmd('off');$('#clear').onclick=()=>cmd('clear_fault');$('#connect').onclick=()=>cmd('connect'); $('#settingsButton').onclick=async()=>{const s=await(await fetch('settings')).json();$('#ttl').value=s.ttl_hours;$('#settingsText').textContent=`Version ${s.version}. ${s.mode}. Device: ${s.device||'not configured'}.`;$('#settings').showModal()};$('#closeSettings').onclick=()=>$('#settings').close();$('#saveSettings').onclick=async()=>{try{await post('settings',{ttl_hours:Number($('#ttl').value)});notice('Idle shutdown setting saved.');$('#settings').close()}catch(e){notice(e.message)}};
control-panel/server.py+6−4
@@ -21,7 +21,7 @@ from urllib.parse import urlparse from transport import SerialLoad, LinkError  ROOT = Path(__file__).resolve().parent-VERSION = "0.0.2"+VERSION = "0.0.3"   class Controller:@@ -37,7 +37,7 @@ class Controller:                           enabled=None, volts=None, amps=None, case_c=None,                           requested_a=0, applied_a=None, watts=None, rpm=None, rpm_valid=False,                           power_limit_w=5, serial=serial, error="No load connected",-                          hardware_verified=False, updated_at=None)+                          hardware_verified=False, sensors_valid=False, assembly_qualified=False, updated_at=None)      def event(self, text, caller="load monitor"):         self.events.append(dict(time=time.time(), aiThread=caller, text=text))@@ -52,7 +52,7 @@ class Controller:             except Exception: pass         self.link, self.owner = None, None         self.state.update(connected=False, mode="disconnected", enabled=None,-                          state="UNKNOWN", error=str(exc), applied_a=None,+                          state="UNKNOWN", error=str(exc), applied_a=None, sensors_valid=False, assembly_qualified=False,                           volts=None, amps=None, watts=None, rpm=None, rpm_valid=False, case_c=None)         self.event("Link lost; load state unknown: " + str(exc)) @@ -60,7 +60,7 @@ class Controller:         d = self.link.status()         self.state.update({k:v for k,v in d.items() if k not in ("id", "ok", "boot", "owner")})         self.state.update(connected=True, mode="hardware", error=None,-                          watts=d["volts"] * d["amps"], updated_at=time.time())+                          watts=d["volts"] * d["amps"] if d["sensors_valid"] else None, updated_at=time.time())         self.rows.append(dict(timestamp=time.time(), serial=self.serial, mode="hardware",                               volts=d["volts"], amps=d["amps"], watts=self.state["watts"],                               case_c=d["case_c"], rpm=d["rpm"], rpm_valid=d["rpm_valid"], state=d["state"]))@@ -112,6 +112,8 @@ class Controller:             try:                 if cmd == "arm":                     self.read()+                    if not self.state.get("sensors_valid") or not self.state.get("assembly_qualified"):+                        raise ValueError("Valid sensors and qualified assembly are required before enabling the load")                     if not self.state.get("rpm_valid") or not 5000 <= self.state["rpm"] <= 10000:                         raise ValueError("Qualified fan feedback is required before enabling the load")                     self.link.request("heartbeat")
control-panel/transport.py+18−2
@@ -68,7 +68,18 @@ class SerialLoad:      def status(self):         d = self.request("status")-        for key in ("volts", "amps", "case_c", "requested_a", "applied_a", "power_limit_w", "sample_age_ms"):+        for key in ("sensors_valid", "assembly_qualified"):+            if type(d.get(key)) is not bool:+                raise LinkError("Missing instrument qualification: " + key)+        for key in ("volts", "amps", "case_c"):+            if key not in d:+                raise LinkError("Missing telemetry: " + key)+            value = d[key]+            if value is None and not d["sensors_valid"]:+                continue+            if type(value) not in (float, int) or not math.isfinite(value):+                raise LinkError("Invalid telemetry: " + key)+        for key in ("requested_a", "applied_a", "power_limit_w", "sample_age_ms"):             value = d.get(key)             if type(value) not in (float, int) or not math.isfinite(value):                 raise LinkError("Invalid telemetry: " + key)@@ -83,8 +94,13 @@ class SerialLoad:             raise LinkError("Invalid device state")         if type(d.get("dac_code")) is not int or not 0 <= d["dac_code"] <= 4095:             raise LinkError("Invalid DAC telemetry")-        if not 0 <= d["sample_age_ms"] <= 50:+        if type(d["sample_age_ms"]) is not int or not 0 <= d["sample_age_ms"] <= 0xFFFFFFFF:+            raise LinkError("Invalid device sample age")+        if d["sensors_valid"] and d["sample_age_ms"] > 50:             raise LinkError("Device sensor sample is stale")+        if not d["sensors_valid"] or not d["assembly_qualified"]:+            if d["state"] not in ("OFF", "FAULT") or d["enabled"] or d["dac_code"] != 0 or d["applied_a"] != 0:+                raise LinkError("Unqualified instrument must confirm output OFF")         if not 0 < d["power_limit_w"] <= 5:             raise LinkError("This bring-up adapter requires a device ceiling of at most 5 W")         return d
design/stm32-bringup-build.json+777−9
@@ -29,12 +29,12 @@   "sources": {     "firmware/fan_tach.c": "844f00c1f77e39529bd8f667d543ecee187ff4fe91108aedb7a2c36eb22846fa",     "firmware/fan_tach.h": "20fc7b0bbb1a040dfd0453e3af22cf62e7a564241c28cf9cd08dd9b6d87ddf80",-    "firmware/instruments.h": "1bcd895abf8fe74c5a96028b8b559eafdff0919e970244d4445ccbd72b7a6ee6",-    "firmware/load_core.c": "f7a443d01013cc519e6b1110f2fc38abdf41222399db597cc70cca1832ae6ab3",+    "firmware/instruments.h": "47756a2358e94e143b01bc1f99e5987b9fd8a01d81d762b0b490e96cebeb3151",+    "firmware/load_core.c": "85017ff0a3e37e7b5e41604989326fe623e7f73427ecd8e1a29c63a38d003638",     "firmware/load_core.h": "408263af21e6c15e82f4118343a2b7d0dd2db61808754c0b91021acd1146fadf",-    "firmware/load_protocol.c": "208d7e7fb9a05da31c3c200b018f5b188e53ac3d95ac2288a61f683b4f3df1af",+    "firmware/load_protocol.c": "63b4404a0e510eb2e472d8f7f93b1aa78155fd171312b941948ab85346fc2b61",     "firmware/load_protocol.h": "f64f6b689d57501031515e8ee9f8c8349a2cea63738e73db16f95e747caf91fb",-    "firmware/stm32/README.md": "da1875c99b20608e8bdb6bba59d3a3dc045261cfa5bbcf2df1792089f31f4dc4",+    "firmware/stm32/README.md": "3b899060dee0a9e74e0841d6c220dd68d3211479b16ccaf3a700e6668c6526b0",     "firmware/stm32/clock.c": "f42f5c1ff229b065a048218ecc575624276aaa229e0bc340d80c80eec35416d2",     "firmware/stm32/dependencies.json": "7e8f3c17a59c856371a9f411dce6a9dadafbb8c8edd5ede5e4e5cf46b1567e92",     "firmware/stm32/fan_input.c": "d4ebe5572dbbcd1a3ea84c4e31442732a7237e94d554ec8acdc07142486e92f8",@@ -52,9 +52,777 @@     "tools/build_stm32_bringup.py": "20f8d74fa7bfc8d3ea9a4ba5fce1b912b98a88389285111ff70d58fde4aa93f2"   },   "artifacts": {-    "adom-load-bringup.hex": "42f04e001c0fcffa13830f8093853a73a79921f73f598670ffd9537236cb5bf8",-    "adom-load-bringup.bin": "1e7c37698fe2f07f5160709788b912531ca02c8e64496c145c3919f1d4802d6f",-    "adom-load-bringup.map": "85e535ea52666b9f87b7795fd2ef9b0cca838e69ee95917aa8bae6193aa43d81",-    "adom-load-bringup.elf": "85d9669f5fd738ffc1153c34b7c1666dd6dd5d937eeee1e33241bcbf6e4377d6"-  }+    "adom-load-bringup.hex": "41ddfee1467efabf8a871ed952a7b7418ded0120a28f9d31ef6c5fe1a46e50a6",+    "adom-load-bringup.bin": "839b1ec5e860f0b56dbba6362918d45148132c44dd86f37a9dcc4732fda42c97",+    "adom-load-bringup.map": "d74c18353387c363be04b9173ab34906ed8ffee994d1024855b2a2da0f2ec8fc",+    "adom-load-bringup.elf": "a83740bc95215f972e2035e75276d59bc9cef8026a33055e2e5ef7dbea509d5e"+  },+  "commands": [+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/load_core.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/00-load_core.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/load_protocol.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/01-load_protocol.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/fan_tach.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/02-fan_tach.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/stm32/fan_input.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/03-fan_input.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/stm32/main.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/04-main.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/stm32/clock.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/05-clock.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/stm32/usb_descriptors.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/06-usb_descriptors.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-std=c11",+      "-Werror",+      "-c",+      "/home/adom/project/adom-custom-electronic-load/firmware/stm32/syscalls.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/07-syscalls.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/src/tusb.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/08-tusb.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/src/common/tusb_fifo.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/09-tusb_fifo.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/src/device/usbd.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/10-usbd.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/src/class/cdc/cdc_device.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/11-cdc_device.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/src/portable/st/stm32_fsdev/dcd_stm32_fsdev.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/12-dcd_stm32_fsdev.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/src/portable/st/stm32_fsdev/fsdev_common.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/13-fsdev_common.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/14-stm32g0xx_hal.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_cortex.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/15-stm32g0xx_hal_cortex.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_pwr.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/16-stm32g0xx_hal_pwr.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_pwr_ex.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/17-stm32g0xx_hal_pwr_ex.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_rcc.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/18-stm32g0xx_hal_rcc.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_rcc_ex.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/19-stm32g0xx_hal_rcc_ex.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Src/stm32g0xx_hal_gpio.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/20-stm32g0xx_hal_gpio.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Source/Templates/system_stm32g0xx.c",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/21-system_stm32g0xx.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-Os",+      "-g3",+      "-ffunction-sections",+      "-fdata-sections",+      "-fno-common",+      "-fstack-usage",+      "-Wall",+      "-Wextra",+      "-DSTM32G0B1xx",+      "-DUSE_HAL_DRIVER",+      "-DADOM_BRINGUP_OUTPUT_LOCK=1",+      "-DADOM_USB_VID=51966",+      "-DADOM_USB_PID=16385",+      "-DADOM_LAB_BOOT_ID=\"lab20261009bringup\"",+      "-I/home/adom/project/adom-custom-electronic-load/firmware",+      "-I/home/adom/project/adom-custom-electronic-load/firmware/stm32",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/src",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/lib/CMSIS_5/CMSIS/Core/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Include",+      "-I/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/stm32g0xx_hal_driver/Inc",+      "-I/home/adom/project/adom-load-firmware-toolchain/root/usr/include/newlib",+      "-c",+      "/home/adom/project/adom-load-firmware-deps/tinyusb/hw/mcu/st/cmsis_device_g0/Source/Templates/gcc/startup_stm32g0b1xx.s",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/22-startup_stm32g0b1xx.o"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-gcc",+      "-mcpu=cortex-m0plus",+      "-mthumb",+      "-nostartfiles",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/00-load_core.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/01-load_protocol.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/02-fan_tach.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/03-fan_input.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/04-main.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/05-clock.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/06-usb_descriptors.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/07-syscalls.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/08-tusb.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/09-tusb_fifo.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/10-usbd.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/11-cdc_device.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/12-dcd_stm32_fsdev.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/13-fsdev_common.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/14-stm32g0xx_hal.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/15-stm32g0xx_hal_cortex.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/16-stm32g0xx_hal_pwr.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/17-stm32g0xx_hal_pwr_ex.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/18-stm32g0xx_hal_rcc.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/19-stm32g0xx_hal_rcc_ex.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/20-stm32g0xx_hal_gpio.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/21-system_stm32g0xx.o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/22-startup_stm32g0b1xx.o",+      "-T",+      "/home/adom/project/adom-custom-electronic-load/firmware/stm32/stm32g0b1kc.ld",+      "-Wl,--gc-sections,--print-memory-usage",+      "-Wl,-Map=/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.map",+      "-L/home/adom/project/adom-load-firmware-toolchain/root/usr/lib/arm-none-eabi/newlib/thumb/v6-m/nofp",+      "-Wl,--start-group",+      "-lc",+      "-lm",+      "-lnosys",+      "-lgcc",+      "-Wl,--end-group",+      "-o",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.elf"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-objcopy",+      "-O",+      "ihex",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.elf",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.hex"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-objcopy",+      "-O",+      "binary",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.elf",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.bin"+    ],+    [+      "/home/adom/project/adom-load-firmware-toolchain/root/usr/bin/arm-none-eabi-size",+      "/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.elf"+    ]+  ] }
design/stm32-bringup-build.log+2−2
@@ -1,6 +1,6 @@ Memory region         Used Size  Region Size  %age Used              RAM:       23968 B       144 KB     16.25%-           FLASH:       64592 B       256 KB     24.64%+           FLASH:       64688 B       256 KB     24.68%    text	   data	    bss	    dec	    hex	filename-  62836	   1752	  22216	  86804	  15314	/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.elf+  62936	   1752	  22216	  86904	  15378	/home/adom/project/adom-custom-electronic-load/build/stm32-bringup/adom-load-bringup.elf COMPILED ONLY: output permanently locked OFF; no flash or hardware validation performed.
docs/completion-plan.md+4
@@ -22,3 +22,7 @@ The finish gate has not passed. The board is not released for fabrication. ## Fan component delivery  Sunon MF40200V1-1000U-G99 version 0.2.3 is published in source and registry: factory flying leads, explicitly added Harwin harness, connector symbols/header footprint, complete static STEP/GLB variants and gentle 20-second hero cycle. Published playback, stopped mode and native connector parsing passed. Wiki Skill Pack 0.8.42 and Component Widgets 1.1.17 teach restrained state heroes and honest termination variants. Three raw recordings were added; two full-cycle takes decoded and reviewed, one short take is not a complete cycle. This supporting task does not complete load cooling or the finish gate.++## Firmware continuation++Fresh latch acknowledgement, instrument readback/fault injection, and null-sensor diagnostic host connections are implemented and host-tested (25 Python tests plus C suites). MCU output stays locked; no flash occurred. Continue with bounded I2C/ADC adaptation and hardware-independent integration tests. Do not mistake these checks for physical qualification or use them to close thermal/finish gates. The reviewed fan-stall replay brings the raw inventory to 59.
docs/firmware-contract.md+9−1
@@ -1,6 +1,6 @@ # Firmware and control-panel contract — R0 engineering draft -The portable C safety-policy core is implemented and host-tested. A pinned STM32G0B1KCT6 USB bring-up image now compiles to ELF, HEX and BIN, with output permanently locked OFF. It includes board-specific safe GPIO initialization, USB CDC descriptors and protocol dispatch; it has not been flashed or tested on hardware. The functional HAL, tach/ADC integration, register-driver validation, calibration, persistence and production boot identity remain open. See [STM32 build instructions](../firmware/stm32/README.md).+The portable C safety-policy core is implemented and host-tested. A pinned STM32G0B1KCT6 USB bring-up image now compiles to ELF, HEX and BIN, with output permanently locked OFF. It includes board-specific safe GPIO initialization, USB CDC descriptors and protocol dispatch; it has not been flashed or tested on hardware. Tach capture is compiled; physical tach verification, the functional HAL/ADC/I2C integration, calibration, persistence and production boot identity remain open. See [STM32 build instructions](../firmware/stm32/README.md).  USB CDC ACM is the preferred link to the control panel. The board is self-powered from auxiliary 5 V; USB VBUS is detected but does not power the fan or logic. D+/D− and ground connect to the panel USB host. UART at 3.3 V is an alternative through the molecule contacts; it is not a power interface and must not be wired to 5 V GPIO. Never enable a load by connecting a raw GPIO wire alone. @@ -39,3 +39,11 @@ Run `bash tools/check_firmware.sh`. Host checks cover core policy, tach feedback `design/mcu-pinmap.json` records the static package/net/peripheral audit. Twenty signal and two supply assignments match the selected STM32 package, design netlist and PCB. It does not validate register configuration or board-level signal integrity.  Request sequence numbers remain monotonic across all owners during one device boot. `identify` returns `last_sequence`; the host advances beyond it before `claim`. Expiry or disconnect never resets the sequence floor. A fresh unique boot identity is mandatory after MCU reset, and sequence exhaustion requires a new boot. Tests cover delayed claims after expiry and host reconnection against the same C endpoint.++## Instrument readback and bring-up monitoring — 9 October++The driver checks DAC gain, configuration, synchronization, command-register readback and reference status. INA226 initialization verifies configuration and calibration readback. Paired shunt/bus measurements now use triggered conversions: `load_ina_poll` distinguishes WAIT, READY and ERROR; only READY replaces outputs and starts the next conversion. WAIT must not refresh sample age. ERROR requires immediate output disable and reinitialization by the future adapter. This API is not yet connected to the MCU I2C peripheral.++The register model tests inject failure at every transaction in initialization, DAC writes and measurement reads. They also cover acknowledged-but-ignored setup/command writes, wrong IDs, reference fault, configuration reset, signed current, overflow and invalid arguments. These are host tests; they do not verify physical I2C timing or analog accuracy. Register definitions follow [TI DAC60501 Rev E](https://www.ti.com/lit/ds/symlink/dac60501.pdf) and [TI INA226 Rev C](https://www.ti.com/lit/ds/symlink/ina226.pdf).++Replies now include mandatory Boolean `sensors_valid` and `assembly_qualified`. Missing sensors may use null only when validity is false. The host accepts such a diagnostic connection only with OFF/FAULT, enable false, DAC zero and applied current zero. It preserves unavailable readings as null, does not calculate power, and refuses ARM in both server and UI. Valid telemetry still requires the existing 50 ms freshness bound. These fields are required by this draft protocol revision; update host and firmware together. A compiled C peer with missing sensors verifies the actual Python transport connection and rejected ARM.
docs/status.md+6
@@ -1,3 +1,9 @@+# Current checkpoint — 2026-10-09, 20:22 UTC++Board 18 remains unchanged; thermal/finish gates remain open. Instrument register fault-injection tests now pass, with DAC/configuration readback and coherent triggered INA226 reads. The control panel accepts explicitly unavailable sensors only while output is confirmed OFF, and refuses ARM until sensor and assembly qualification are valid. All sanitizer-backed C suites and 25 Python tests pass. The output-locked STM32 image builds at 64,688 flash bytes and 23,968 RAM bytes including reservations. No hardware was flashed.++The reviewed fan-stall replay is published and visually checked on the wiki. There are 59 raw takes, including rejected/static takes; the final narrated film is unfinished. See [clip library](build-clips.md) and [firmware contract](firmware-contract.md).+ # Current checkpoint - 2026-10-09, 20:10 UTC  Board 18 remains unchanged and native-connected. A separate package cooling calculator and chart now expose contact-area conversion, package/interface/sink resistance, and the difference between a ducted-flow scenario and installed fan performance. Three numerical unit/energy-balance checks pass. This sensitivity study does not pass the thermal gate or qualify a selected assembly. See [package cooling review](package-cooling-review.md).
firmware/instruments.c+33−12
@@ -1,17 +1,29 @@ /* Register-level implementation from TI DACx0501 SBAS794E and INA226 SBOS547.  * Board adapter supplies bounded-time I2C operations and big-endian 16-bit words.- * Keep MCU_ENABLE low throughout initialization and on ANY returned false.+ * Keep MCU_ENABLE low throughout initialization and on false / LOAD_INA_ERROR.+ * LOAD_INA_WAIT does not refresh sample age; adapter must expire stale data.  */ #include "instruments.h" #define DAC_ADDR 0x48 #define INA_ADDR 0x40+#define INA_CONFIG 0x4523+#define INA_CALIBRATION 1024+static bool readable(const load_i2c *b){return b && b->read16;}+static bool writable(const load_i2c *b){return readable(b) && b->write16;} bool load_dac_write(const load_i2c *b,uint16_t code){- if(code>4095)return false;- uint16_t status=0;- return b->write16(DAC_ADDR,8,(uint16_t)(code<<4)) && b->read16(DAC_ADDR,7,&status) && !(status&1);+ if(!writable(b)||code>4095)return false;+ uint16_t status=0,gain=0,readback=0,config=0,sync=0;+ /* A reset or ignored transaction must not silently change the current scale.+    The adapter must force enable LOW if any of these transactions fails. */+ return b->read16(DAC_ADDR,4,&gain) && gain==0x0101 &&+   b->read16(DAC_ADDR,3,&config) && config==0 &&+   b->read16(DAC_ADDR,2,&sync) && sync==0 &&+   b->write16(DAC_ADDR,8,(uint16_t)(code<<4)) &&+   b->read16(DAC_ADDR,8,&readback) && readback==(uint16_t)(code<<4) &&+   b->read16(DAC_ADDR,7,&status) && !(status&1); } bool load_instruments_init(const load_i2c *b){- uint16_t id=0,mfr=0,gain=0,status=0;+ uint16_t id=0,mfr=0,gain=0,status=0,config=0,calibration=0;  if(!b || !b->write16 || !b->read16 || !b->delay_ms)return false;  if(!b->write16(DAC_ADDR,8,0))return false;  if(!b->read16(DAC_ADDR,1,&id)||id!=0x2115)return false; /* DAC60501Z, not M or 16-bit */@@ -21,16 +33,25 @@ bool load_instruments_init(const load_i2c *b){  b->delay_ms(10); /* conservative startup delay; verify on hardware */  if(!b->read16(DAC_ADDR,4,&gain)||gain!=0x0101||!b->read16(DAC_ADDR,7,&status)||(status&1))return false;  if(!b->read16(INA_ADDR,0xFE,&mfr)||mfr!=0x5449||!b->read16(INA_ADDR,0xFF,&id)||(id&0xFFF0)!=0x2260)return false;- /* 16 averages, 1.1ms bus + 1.1ms shunt each, continuous (~35.2ms update). */- if(!b->write16(INA_ADDR,0,0x4527)||!b->write16(INA_ADDR,5,1024))return false;+ /* 16 averages, 1.1ms bus + 1.1ms shunt each, triggered (~35.2ms conversion). */+ if(!b->write16(INA_ADDR,0,INA_CONFIG)||!b->write16(INA_ADDR,5,INA_CALIBRATION)||+    !b->read16(INA_ADDR,0,&config)||config!=INA_CONFIG||+    !b->read16(INA_ADDR,5,&calibration)||calibration!=INA_CALIBRATION)return false;  return load_dac_write(b,0); }-bool load_ina_read(const load_i2c *b,float *v,float *a){- uint16_t flags=0,shunt=0,bus=0;- if(!b->read16(INA_ADDR,6,&flags)||!(flags&8)||(flags&4))return false;- if(!b->read16(INA_ADDR,1,&shunt)||!b->read16(INA_ADDR,2,&bus))return false;+load_ina_result load_ina_poll(const load_i2c *b,float *v,float *a){+ if(!writable(b)||!v||!a||v==a)return LOAD_INA_ERROR;+ uint16_t flags=0,shunt=0,bus=0,config=0;+ /* Detect device reset/reconfiguration before accepting a new measurement. */+ if(!b->read16(INA_ADDR,0,&config)||config!=INA_CONFIG)return LOAD_INA_ERROR;+ if(!b->read16(INA_ADDR,6,&flags)||(flags&4))return LOAD_INA_ERROR;+ if(!(flags&8))return LOAD_INA_WAIT;+ /* Triggered conversions have stopped before the ready flag is observed;+    both result registers therefore belong to the same completed conversion. */+ if(!b->read16(INA_ADDR,1,&shunt)||!b->read16(INA_ADDR,2,&bus))return LOAD_INA_ERROR;+ if(!b->write16(INA_ADDR,0,INA_CONFIG))return LOAD_INA_ERROR; /* start next conversion */  /* The current conversion below uses shunt volts directly, independent of CAL.     Bus measurement is AFTER the input protection diode, not the DUT connector. */  *v=(float)bus*.00125f;*a=(float)(int16_t)shunt*.000125f;- return true;+ return LOAD_INA_READY; }
firmware/instruments.h+5−1
@@ -9,5 +9,9 @@ typedef struct { } load_i2c; bool load_instruments_init(const load_i2c *); bool load_dac_write(const load_i2c *,uint16_t code);-bool load_ina_read(const load_i2c *,float *load_terminal_v,float *amps);+typedef enum { LOAD_INA_ERROR=-1, LOAD_INA_WAIT=0, LOAD_INA_READY=1 } load_ina_result;+/* WAIT leaves outputs unchanged; never refresh a cached sample timestamp on WAIT.+ * ERROR requires hard-off/reinitialization; READY starts the next conversion.+ * Returned voltage is after the protection diode, not at the DUT connector. */+load_ina_result load_ina_poll(const load_i2c *,float *load_terminal_v,float *amps); #endif
firmware/load_protocol.c+2−2
@@ -99,9 +99,9 @@ static const char *finite_text(float x,char out[32]){ static size_t reply(load_protocol *p,const request *r,const load_sample *s,const char *error,char *out,size_t cap){  if(cap<LOAD_REPLY_BYTES){stop(p,true);return 0;}  const char *states[]={"OFF","ARMED","RUNNING","FAULT"};char v[32],a[32],t[32],rpm[32];- int n=snprintf(out,cap,"{\"id\":%lu,\"ok\":%s,\"error\":\"%s\",\"protocol\":\"adom.load/1\",\"serial\":\"%s\",\"boot\":\"%s\",\"state\":\"%s\",\"enabled\":%s,\"dac_code\":%u,\"faults\":%lu,\"last_sequence\":%lu,\"volts\":%s,\"amps\":%s,\"case_c\":%s,\"rpm\":%s,\"rpm_valid\":%s,\"requested_a\":%.7g,\"applied_a\":%.7g,\"power_limit_w\":%.7g,\"sample_age_ms\":%lu}\n",+ int n=snprintf(out,cap,"{\"id\":%lu,\"ok\":%s,\"error\":\"%s\",\"protocol\":\"adom.load/1\",\"serial\":\"%s\",\"boot\":\"%s\",\"state\":\"%s\",\"enabled\":%s,\"dac_code\":%u,\"faults\":%lu,\"last_sequence\":%lu,\"volts\":%s,\"amps\":%s,\"case_c\":%s,\"rpm\":%s,\"rpm_valid\":%s,\"requested_a\":%.7g,\"applied_a\":%.7g,\"power_limit_w\":%.7g,\"sample_age_ms\":%lu,\"sensors_valid\":%s,\"assembly_qualified\":%s}\n",  (unsigned long)r->id,error?"false":"true",error?error:"",p->serial,p->boot,states[p->core->state],p->core->enable?"true":"false",p->core->dac_code,(unsigned long)p->core->faults,(unsigned long)p->sequence,- finite_text(s->volts,v),finite_text(s->amps,a),finite_text(s->case_c,t),s->fan_rpm_valid&&isfinite(s->fan_rpm)?finite_text(s->fan_rpm,rpm):"null",s->fan_rpm_valid&&isfinite(s->fan_rpm)?"true":"false",(double)p->core->requested_a,(double)p->core->applied_a,(double)p->core->power_limit_w,(unsigned long)(s->now_ms-s->sample_ms));+ finite_text(s->volts,v),finite_text(s->amps,a),finite_text(s->case_c,t),s->fan_rpm_valid&&isfinite(s->fan_rpm)?finite_text(s->fan_rpm,rpm):"null",s->fan_rpm_valid&&isfinite(s->fan_rpm)?"true":"false",(double)p->core->requested_a,(double)p->core->applied_a,(double)p->core->power_limit_w,(unsigned long)(s->now_ms-s->sample_ms),s->sensors_valid?"true":"false",s->assembly_qualified?"true":"false");  if(n<0||(size_t)n>=cap){stop(p,true);return 0;}return (size_t)n; } static const char *dispatch(load_protocol *p,const request *r,const load_sample *s){
firmware/stm32/README.md+4
@@ -46,3 +46,7 @@ No fan drive, load-enable, or assembly qualification is introduced by this input ## ARM acknowledgement freshness  The portable core refuses ARM if the sampled hardware latch is already high. A new ARM sequence requires a verified low latch before the adapter pulses ARM_CLK, then a high acknowledgement within the existing 100 ms timeout. Stale-high latch state cannot authorize a new run. Host core and framed-protocol tests cover this refusal and the normal low-to-high transition. The output-locked MCU image still never executes the functional handshake, and hardware verification remains required.++## Host bring-up monitoring++The control panel can retain a confirmed-OFF connection while voltage/current/temperature are unavailable. Firmware replies explicitly report sensor and assembly qualification; the host keeps unknown readings blank and Enable load disabled. Host-to-compiled-C interoperability covers this path. It does not establish USB enumeration on this board. The register driver is tested separately and is not yet linked to the I2C peripheral.
tests/protocol_peer.c+5
@@ -2,6 +2,8 @@  * No GPIO, USB peripheral, DAC or physical hardware is accessed. */ #include "../firmware/load_protocol.h" #include <stdio.h>+#include <stdlib.h>+#include <math.h> static unsigned stopped; static void hard_off(void *ctx){(*(unsigned *)ctx)++;} int main(void){@@ -9,6 +11,9 @@ int main(void){  if(!load_protocol_init(&p,&c,"LOAD-HOST-SIM","simboot01",hard_off,&stopped))return 1;  char response[LOAD_REPLY_BYTES];int byte;  load_sample s={.now_ms=0,.sample_ms=0,.last_tach_ms=0,.volts=5,.amps=0,.case_c=25,.fan_rpm=7000,.fan_rpm_valid=true,.sensors_valid=true,.hw_ok=true,.assembly_qualified=false};+ if(getenv("LOAD_PEER_MISSING_SENSORS")){+   s.volts=s.amps=s.case_c=NAN;s.sensors_valid=false;s.now_ms=123456;+ }  while((byte=getchar())!=EOF){   size_t n=load_protocol_byte(&p,(unsigned char)byte,&s,response,sizeof(response));   if(n){if(fwrite(response,1,n,stdout)!=n)return 2;fflush(stdout);}
tests/test_controller.py+14−1
@@ -19,7 +19,7 @@ class FakeLink:         return dict(volts=5, amps=.1 if self.on else 0, case_c=25,                     enabled=self.on, dac_code=130 if self.on else 0,                     state='RUNNING' if self.on else 'OFF',requested_a=.1,-                    applied_a=.1 if self.on else 0, rpm=7000, rpm_valid=True, power_limit_w=5)+                    applied_a=.1 if self.on else 0, rpm=7000, rpm_valid=True, power_limit_w=5, sensors_valid=True, assembly_qualified=True)     def close(self):pass  @@ -57,6 +57,19 @@ class ControlTests(unittest.TestCase):         for a in [float('nan'),float('inf'),True,-.1,1.1]:             with self.assertRaises(ValueError):self.c.command('set_current','operator A',amps=a)         self.assertEqual(self.link.calls,[])+    def test_bringup_can_monitor_but_cannot_arm(self):+        old=self.link.status+        self.link.status=lambda: dict(old(), volts=None, amps=None, case_c=None,+            sensors_valid=False, assembly_qualified=False)+        self.c.read()+        self.assertTrue(self.c.snapshot()['connected'])+        self.assertIsNone(self.c.snapshot()['watts'])+        self.assertEqual(self.c.snapshot()['rpm'],7000)+        with self.assertRaisesRegex(ValueError,'qualified assembly'):+            self.c.command('arm','operator A')+        self.assertNotIn('arm',self.link.calls)+        self.c.command('off','operator A')+        self.assertFalse(self.c.snapshot()['enabled'])   if __name__=='__main__':unittest.main()
tests/test_instruments.cadded+88
@@ -0,0 +1,88 @@+#include "../firmware/instruments.h"+#include <assert.h>+#include <math.h>+#include <stddef.h>+#include <stdio.h>+#include <string.h>++/* Register-device fixture, including read-to-clear conversion-ready flag.+ * Acknowledged-but-ignored writes and failed transactions are separate faults. */+static uint16_t dac[256], ina[256];+static unsigned calls, fail_at, delay_total;+static int ignored_addr, ignored_reg;+static uint16_t *device(uint8_t addr) {+  assert(addr == 0x48 || addr == 0x40);+  return addr == 0x48 ? dac : ina;+}+static bool transaction(void) { return ++calls != fail_at; }+static bool write16(uint8_t addr, uint8_t reg, uint16_t value) {+  if (!transaction()) return false;+  if (addr != ignored_addr || reg != ignored_reg) {+    device(addr)[reg] = value;+    if (addr == 0x40 && reg == 0) ina[6] &= (uint16_t)~8u;+  }+  return true;+}+static bool read16(uint8_t addr, uint8_t reg, uint16_t *value) {+  if (!transaction()) return false;+  *value = device(addr)[reg];+  if (addr == 0x40 && reg == 6) ina[6] &= (uint16_t)~8u;+  return true;+}+static void delay_ms(uint32_t ms) { delay_total += ms; }+static const load_i2c bus = {write16, read16, delay_ms};+static void reset(void) {+  memset(dac, 0, sizeof(dac)); memset(ina, 0, sizeof(ina));+  dac[1] = 0x2115; dac[4] = 1; /* power-on gain, before setup */+  ina[0xfe] = 0x5449; ina[0xff] = 0x2260; ina[0] = 0x4127;+  calls = fail_at = delay_total = 0; ignored_addr = ignored_reg = -1;+}+static void configured(void) { reset(); assert(load_instruments_init(&bus)); calls = 0; }+static void sample(void) { ina[6] = 8; ina[1] = 8000; ina[2] = 4000; }+int main(void) {+  reset(); assert(load_instruments_init(&bus));+  assert(dac[8] == 0 && dac[4] == 0x0101 && ina[0] == 0x4523 && ina[5] == 1024);+  assert(delay_total == 10);+  unsigned init_calls = calls;+  for (unsigned i = 1; i <= init_calls; ++i) {+    reset(); fail_at = i; assert(!load_instruments_init(&bus)); assert(dac[8] == 0);+  }+  for (unsigned i = 0; i < 3; ++i) {+    reset(); ignored_addr = i ? 0x40 : 0x48; ignored_reg = i == 0 ? 4 : i == 1 ? 0 : 5;+    assert(!load_instruments_init(&bus));+  }+  reset(); dac[1] = 0x2195; assert(!load_instruments_init(&bus)); /* wrong reset variant */+  reset(); ina[0xfe] = 0; assert(!load_instruments_init(&bus));+  reset(); ina[0xff] = 0x2250; assert(!load_instruments_init(&bus));+  reset(); dac[7] = 1; assert(!load_instruments_init(&bus));+  configured(); assert(load_dac_write(&bus, 4095)); assert(dac[8] == 0xfff0);+  unsigned write_calls = calls;+  for (unsigned i = 1; i <= write_calls; ++i) {+    configured(); fail_at = i; assert(!load_dac_write(&bus, 123));+  }+  configured(); ignored_addr = 0x48; ignored_reg = 8; assert(!load_dac_write(&bus, 123));+  configured(); dac[4] = 1; assert(!load_dac_write(&bus, 123)); assert(dac[8] == 0);+  configured(); dac[3] = 1; assert(!load_dac_write(&bus, 123)); assert(dac[8] == 0);+  configured(); dac[2] = 1; assert(!load_dac_write(&bus, 123)); assert(dac[8] == 0);+  configured(); dac[7] = 1; assert(!load_dac_write(&bus, 123));+  configured(); assert(!load_dac_write(&bus, 4096)); assert(calls == 0);+  float v = -99, a = -99;+  configured(); sample(); assert(load_ina_poll(&bus, &v, &a) == LOAD_INA_READY);+  assert(fabsf(v - 5) < .00001f && fabsf(a - 1) < .00001f);+  unsigned read_calls = calls;+  assert(load_ina_poll(&bus, &v, &a) == LOAD_INA_WAIT); /* same conversion cannot be consumed twice */+  for (unsigned i = 1; i <= read_calls; ++i) {+    configured(); sample(); fail_at = i; v = a = -99;+    assert(load_ina_poll(&bus, &v, &a) == LOAD_INA_ERROR); assert(v == -99 && a == -99);+  }+  configured(); sample(); ina[1] = (uint16_t)(int16_t)-8000;+  assert(load_ina_poll(&bus, &v, &a) == LOAD_INA_READY); assert(fabsf(a + 1) < .00001f);+  configured(); sample(); ina[6] |= 4; assert(load_ina_poll(&bus, &v, &a) == LOAD_INA_ERROR);+  configured(); sample(); ina[0] = 0x4127; assert(load_ina_poll(&bus, &v, &a) == LOAD_INA_ERROR);+  assert(!load_instruments_init(NULL)); assert(!load_dac_write(NULL, 0));+  assert(load_ina_poll(NULL, &v, &a) == LOAD_INA_ERROR); assert(load_ina_poll(&bus, NULL, &a) == LOAD_INA_ERROR);+  assert(load_ina_poll(&bus, &v, &v) == LOAD_INA_ERROR);+  load_i2c missing = {0}; assert(!load_instruments_init(&missing));+  assert(!load_dac_write(&missing, 0)); assert(load_ina_poll(&missing, &v, &a) == LOAD_INA_ERROR);+  puts("Instrument register, readback, signed conversion and transaction-failure tests passed.");+}
tests/test_protocol_interop.py+18−2
@@ -10,8 +10,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'control-panel')) from transport import SerialLoad, LinkError  class ProcessWire:-    def __init__(self, binary):-        self.process = subprocess.Popen([binary], stdin=subprocess.PIPE, stdout=subprocess.PIPE)+    def __init__(self, binary, env=None):+        self.process = subprocess.Popen([binary], stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=env)     def write(self, data):         self.process.stdin.write(data)         self.process.stdin.flush()@@ -27,6 +27,22 @@ class ProcessWire:  @unittest.skipUnless(os.getenv('LOAD_PROTOCOL_PEER'), 'Build host C peer and set LOAD_PROTOCOL_PEER') class ProtocolInterop(unittest.TestCase):+    def test_missing_sensors_from_actual_c_reply(self):+        wire = ProcessWire(os.environ['LOAD_PROTOCOL_PEER'],+                           dict(os.environ, LOAD_PEER_MISSING_SENSORS='1'))+        try:+            device = SerialLoad(wire, 'LOAD-HOST-SIM')+            status = device.connect()+            self.assertFalse(status['sensors_valid'])+            self.assertFalse(status['assembly_qualified'])+            self.assertIsNone(status['volts'])+            self.assertEqual(status['state'], 'OFF')+            self.assertFalse(status['enabled'])+            with self.assertRaisesRegex(LinkError, 'arm_interlock'):+                device.request('arm')+            self.assertEqual(device.status()['state'], 'FAULT')+        finally:+            wire.close()     def test_real_host_to_c_protocol(self):         wire = ProcessWire(os.environ['LOAD_PROTOCOL_PEER'])         try:
tests/test_transport.py+21−1
@@ -19,7 +19,7 @@ class FakeWire:         d = dict(id=q["id"], ok=True, protocol="adom.load/1", serial="LOAD-TEST",                  boot="boot0001", last_sequence=0, enabled=False, state="OFF", dac_code=0,                  volts=5, amps=0, case_c=25, requested_a=0, applied_a=0,-                 power_limit_w=5, sample_age_ms=10, rpm=7000, rpm_valid=True)+                 power_limit_w=5, sample_age_ms=10, rpm=7000, rpm_valid=True, sensors_valid=True, assembly_qualified=True)         return json.dumps(self.change(d)).encode() + b"\n"     def close(self):         pass@@ -70,6 +70,26 @@ class TransportTests(unittest.TestCase):     def test_on_readback_refused(self):         self.wire.change = lambda d: dict(d, enabled=True)         with self.assertRaises(LinkError): self.load.connect()+    def test_output_locked_bringup_accepts_explicit_missing_sensors(self):+        self.wire.change = lambda d: dict(d, volts=None, amps=None, case_c=None,+            sensors_valid=False, assembly_qualified=False, sample_age_ms=123456)+        state = self.load.connect()+        self.assertEqual(state['state'], 'OFF')+        self.assertIsNone(state['volts'])+        self.assertEqual(state['rpm'], 7000)+        self.assertNotIn('arm', [q['cmd'] for q in self.wire.sent])+    def test_unqualified_active_state_is_rejected(self):+        self.load.connect()+        for change in ({'state':'ARMED'}, {'state':'RUNNING'}, {'enabled':True},+                       {'dac_code':1}, {'applied_a':.1}):+            self.wire.change = lambda d: dict(d, sensors_valid=False,+                assembly_qualified=False, volts=None, **change)+            with self.assertRaises(LinkError): self.load.status()+    def test_missing_sensor_data_cannot_claim_validity(self):+        self.load.connect()+        for change in ({'volts':None}, {'sensors_valid':None}, {'assembly_qualified':1}):+            self.wire.change = lambda d: dict(d, **change)+            with self.assertRaises(LinkError): self.load.status()   if __name__ == "__main__": unittest.main()
tools/check_firmware.sh+2−1
@@ -12,6 +12,7 @@ gcc "${cc_args[@]}" firmware/load_core.c firmware/load_protocol.c tests/test_loa "$load_check_dir/protocol" gcc "${cc_args[@]}" demos/fan-safety/trace.c firmware/load_core.c firmware/fan_tach.c -lm -o "$load_check_dir/fan-demo" "$load_check_dir/fan-demo" > "$load_check_dir/fan-trace.json"-gcc "${cc_args[@]}" -c firmware/instruments.c -o "$load_check_dir/instruments.o"+gcc "${cc_args[@]}" firmware/instruments.c tests/test_instruments.c -lm -o "$load_check_dir/instruments"+"$load_check_dir/instruments" gcc "${cc_args[@]}" firmware/load_core.c firmware/load_protocol.c tests/protocol_peer.c -lm -o "$load_check_dir/peer" LOAD_PROTOCOL_PEER="$load_check_dir/peer" python3 -m unittest discover -s tests -p 'test_*.py'