project
Adom Custom Electronic Load
Public Made by Adomby adom
Two-layer digitally controlled electronic-load prototype. Compact R11 is under active electrical, cooling and probe-access review; not a manufacturing release.
← Commit history
Refresh fan-stall replay for fresh latch acknowledgement and publish reviewed clip
10 files changed
+57−4
demos/fan-safety/README.md+2demos/fan-safety/index.html+1−1demos/fan-safety/trace.c+1−1demos/fan-safety/trace.json+1−1docs/build-clips.md+6docs/clips/fan-latch-stall-replay-poster.jpgdocs/clips/fan-latch-stall-replay.mp4tools/check_firmware.sh+2videos/fan-safety/recording-stop.json+37videos/inventory.json+7−1demos/fan-safety/README.md+2@@ -10,6 +10,8 @@ cc -std=c11 -Wall -Wextra -Werror -fsanitize=address,undefined demos/fan-safety/ python3 -m http.server 45955 --directory demos/fan-safety ``` +The synthetic latch is low when ARM is requested at 100 ms, then high at 101 ms. A stale high latch is rejected by the controller tests.+ At 2000 ms synthetic tach pulses stop. At 2013 ms the decoder invalidates feedback and the policy disables the load command. Pulses return at 3000 ms; after qualification the fan feedback becomes valid again, but the fault remains latched. Assertions verify the trip and absence of automatic restart. These times describe this input schedule and host model only; they do not establish physical shutdown latency or cooling performance. The browser replays saved trace values at 1× simulation time. It does not execute firmware or invent readings. Video capture has its own timing limitations; the displayed simulation clock is authoritative for the replay.
demos/fan-safety/index.html+1−1@@ -6,7 +6,7 @@ <div id="event">Ready to replay the software test.</div><div class="controls"><button id="play">Play replay</button><input aria-label="Replay position" id="seek" type="range" min="0" max="4500" value="0"><span id="time">0.000 s</span></div><footer>1× simulation time. The plot shows the software command, not measured current. Assembly qualification and hardware-latch inputs are synthetic test fixtures. Physical fan behavior, MCU timing, analog response, and shutdown latency remain unverified.</footer> <script> let data,playing=false,start=0,position=0;const $=id=>document.getElementById(id),names=['OFF','ARMED','RUNNING','FAULT'];-function show(t){position=t;let i=data.samples.findLastIndex(s=>s.ms<=t),s=data.samples[Math.max(i,0)];$('rpm').textContent=s.rpmValid?Math.round(s.rpm).toLocaleString()+' RPM':'UNKNOWN';$('rpm').className='value '+(s.rpmValid?'good':'bad');$('amps').textContent=s.setpointA.toFixed(3)+' A';$('state').textContent=names[s.state];$('state').className='value '+(s.state===3?'bad':'good');$('time').textContent=(t/1000).toFixed(3)+' s';$('seek').value=t;let x=60+t/4500*910,y=185-s.setpointA*155;$('cursor').setAttribute('x1',x);$('cursor').setAttribute('x2',x);$('dot').setAttribute('cx',x);$('dot').setAttribute('cy',y);$('curve').setAttribute('d',data.samples.filter(v=>v.ms<=t).map((v,j)=>(j?'L':'M')+(60+v.ms/4500*910)+','+(185-v.setpointA*155)).join(' '));$('event').textContent=t<100?'Waiting for qualified fan feedback.':t<2000?'Load enabled. Current command rises at 1 A/s.':t<data.faultMs?'Tach pulses stopped. Checking freshness…':t<3000?'Fan feedback expired. Enable LOW; DAC code zero.':'Fan feedback recovered. Fault stays latched—no automatic restart.';}+function show(t){position=t;let i=data.samples.findLastIndex(s=>s.ms<=t),s=data.samples[Math.max(i,0)];$('rpm').textContent=s.rpmValid?Math.round(s.rpm).toLocaleString()+' RPM':'UNKNOWN';$('rpm').className='value '+(s.rpmValid?'good':'bad');$('amps').textContent=s.setpointA.toFixed(3)+' A';$('state').textContent=names[s.state];$('state').className='value '+(s.state===3?'bad':'good');$('time').textContent=(t/1000).toFixed(3)+' s';$('seek').value=t;let x=60+t/4500*910,y=185-s.setpointA*155;$('cursor').setAttribute('x1',x);$('cursor').setAttribute('x2',x);$('dot').setAttribute('cx',x);$('dot').setAttribute('cy',y);$('curve').setAttribute('d',data.samples.filter(v=>v.ms<=t).map((v,j)=>(j?'L':'M')+(60+v.ms/4500*910)+','+(185-v.setpointA*155)).join(' '));$('event').textContent=t<100?'Waiting for qualified fan feedback.':t<=100?'ARM requested; waiting for latch acknowledgement.':t<2000?'Load enabled. Current command rises at 1 A/s.':t<data.faultMs?'Tach pulses stopped. Checking freshness…':t<3000?'Fan feedback expired. Enable LOW; DAC code zero.':'Fan feedback recovered. Fault stays latched—no automatic restart.';} function frame(now){if(!playing)return;let t=Math.min(4500,now-start);show(t);if(t>=4500){playing=false;$('play').textContent='Replay again';}else requestAnimationFrame(frame);} $('play').onclick=()=>{if(!data)return;if(playing){playing=false;$('play').textContent='Continue';return;}if(position>=4500)position=0;playing=true;start=performance.now()-position;$('play').textContent='Pause';requestAnimationFrame(frame);};$('seek').oninput=e=>{playing=false;$('play').textContent='Continue';show(+e.target.value);};fetch('trace.json').then(r=>r.json()).then(d=>{data=d;show(0)}).catch(()=>{$('event').textContent='Trace could not be loaded.'}); </script></html>
demos/fan-safety/trace.c+1−1@@ -13,7 +13,7 @@ int main(void){ if(spinning){while(next_edge<=t*1000){fan_tach_edge(&f,next_edge);last_edge=next_edge;next_edge+=4286;}} else next_edge=(t+1)*1000; float rpm=0;bool valid=fan_tach_read(&f,t*1000,&rpm);- load_sample s={.now_ms=t,.sample_ms=t,.last_tach_ms=last_edge/1000,.volts=5,.amps=c.enable?c.applied_a:0,.case_c=30,.fan_rpm=rpm,.fan_rpm_valid=valid,.sensors_valid=true,.hw_ok=true,.latch_q=true,.assembly_qualified=true};+ load_sample s={.now_ms=t,.sample_ms=t,.last_tach_ms=last_edge/1000,.volts=5,.amps=c.enable?c.applied_a:0,.case_c=30,.fan_rpm=rpm,.fan_rpm_valid=valid,.sensors_valid=true,.hw_ok=true,.latch_q=t>100,.assembly_qualified=true}; load_heartbeat(&c,t); if(t==100){assert(load_set_current(&c,.75f));assert(load_arm(&c,&s));armed=true;} load_tick(&c,&s);
demos/fan-safety/trace.json+1−1@@ -9,7 +9,7 @@ {"ms":70,"rpm":6999.5,"rpmValid":true,"setpointA":0.000,"enabled":false,"dacCode":0,"state":0,"faults":0}, {"ms":80,"rpm":6999.5,"rpmValid":true,"setpointA":0.000,"enabled":false,"dacCode":0,"state":0,"faults":0}, {"ms":90,"rpm":6999.5,"rpmValid":true,"setpointA":0.000,"enabled":false,"dacCode":0,"state":0,"faults":0},-{"ms":100,"rpm":6999.5,"rpmValid":true,"setpointA":0.000,"enabled":false,"dacCode":0,"state":2,"faults":0},+{"ms":100,"rpm":6999.5,"rpmValid":true,"setpointA":0.000,"enabled":false,"dacCode":0,"state":1,"faults":0}, {"ms":110,"rpm":6999.5,"rpmValid":true,"setpointA":0.010,"enabled":true,"dacCode":12,"state":2,"faults":0}, {"ms":120,"rpm":6999.5,"rpmValid":true,"setpointA":0.020,"enabled":true,"dacCode":25,"state":2,"faults":0}, {"ms":130,"rpm":6999.5,"rpmValid":true,"setpointA":0.030,"enabled":true,"dacCode":38,"state":2,"faults":0},
docs/build-clips.md+6@@ -134,3 +134,9 @@ The clip passed a full decode and contact-sheet review. The published wiki hero <video width="100%" playsinline controls preload="metadata" poster="docs/clips/diode-thermal-landing-poster.jpg" src="docs/clips/diode-thermal-landing-2x.mp4"></video> Nine preflighted vias appear around D1 in the native editor. Playback is 2x; the raw take is 22.77 seconds encoded. Full decode and contact-sheet review passed. Values clutter the view, so use a short action excerpt in the final film. Recorder sample-timeline warnings are retained, and encoded duration is not wall-clock proof. The saved board subsequently passed native DRC with zero errors and zero opens; this footage does not establish cooling performance.++## Fresh ARM acknowledgement and fan-stall replay++<video width="100%" playsinline controls preload="metadata" poster="docs/clips/fan-latch-stall-replay-poster.jpg" src="docs/clips/fan-latch-stall-replay.mp4"></video>++Actual browser replay of the portable C controller and tach decoder with synthetic inputs. The ARM request starts with the latch low; feedback acknowledges it on the next simulated millisecond. Tach loss shuts the command off, and recovered fan feedback does not clear the fault. Software simulation is labeled throughout. The 6.8-second excerpt trims idle time; it is not physical shutdown timing. Full decode and contact-sheet review passed. Recorder timeline warnings remain with the raw take.
docs/clips/fan-latch-stall-replay-poster.jpgadded⋯ 1 unchanged line ⋯
docs/clips/fan-latch-stall-replay.mp4added⋯ 1 unchanged line ⋯
tools/check_firmware.sh+2@@ -10,6 +10,8 @@ gcc "${cc_args[@]}" firmware/fan_tach.c tests/test_fan_tach.c -lm -o "$load_chec "$load_check_dir/tach" gcc "${cc_args[@]}" firmware/load_core.c firmware/load_protocol.c tests/test_load_protocol.c -lm -o "$load_check_dir/protocol" "$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/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'
videos/fan-safety/recording-stop.jsonadded+37@@ -0,0 +1,37 @@+{+ "_hint": "The MP4 is finalized (moov written). pull_file {path} brings it to the caller; the janitor prunes the recordings dir by age/size, so pull it promptly.",+ "abVersion": "2.1.173",+ "audio": false,+ "audioFrames": 0,+ "autoStopped": false,+ "bytes": 1579884,+ "captureItemClosed": false,+ "captureQuiet": null,+ "droppedFrames": 0,+ "durationMs": 1098,+ "elapsedMs": 9530,+ "encodedFrames": 225,+ "error": "",+ "fileExists": true,+ "finaliseMs": 833,+ "framePath": "gpu",+ "frames": 127,+ "gpuZeroCopy": true,+ "height": 1528,+ "kind": "window",+ "lastFrameAgoMs": 1818,+ "output": "{\"data\":{\"bytes\":1579884,\"elapsedMs\":9530,\"frames\":127,\"path\":\"C:\\\\Users\\\\john\\\\AppData\\\\Local\\\\Temp\\\\adom-bridge-recordings\\\\window-5969372-20261009-201302.mp4\",\"recordingId\":\"rec-12e39fffa636444f847086c6910edb26\"},\"message\":\"Stopped recording rec-12e39fffa636444f847086c6910edb26: 127 frames, 9530 ms, 1579884 bytes -> C:\\\\Users\\\\john\\\\AppData\\\\Local\\\\Temp\\\\adom-bridge-recordings\\\\window-5969372-20261009-201302.mp4\"}",+ "path": "C:/Users/john/AppData/Local/Temp/adom-bridge-recordings/window-5969372-20261009-201302.mp4",+ "rateControl": "vbr",+ "recordingId": "rec-12e39fffa636444f847086c6910edb26",+ "schemaVersion": 1,+ "state": "finalised",+ "status": "ok",+ "success": true,+ "timeline": {+ "reason": "file has more video samples than were written (127 written, fragment 3 needs 180)",+ "repaired": false+ },+ "timeoutSeconds": 30,+ "width": 2560+}
videos/inventory.json+7−1@@ -1,5 +1,5 @@ {- "rawTakeCount": 58,+ "rawTakeCount": 59, "reviewedExcerptCount": 16, "countingRule": "Unique raw window recording filenames in this project; excludes derivative action/speed exports and reference projects. Raw count includes rejected takes.", "takes": [@@ -350,6 +350,12 @@ "durationSeconds": 22.766667, "topic": "Nine D1 thermal vias landed through native KiCad IPC", "review": "Full decode and contact sheet reviewed: vias visibly added around D1; 2x excerpt prepared. Native recorder sample-timeline warning retained; encoded duration is not an engineering stopwatch."+ },+ {+ "file": "videos/fan-safety/window-5969372-20261009-201302.mp4",+ "durationSeconds": 11.2,+ "topic": "Host simulation replay after fresh latch acknowledgement fix; tach loss and latched shutdown",+ "review": "Full decode and contact sheet reviewed; 6.8 s excerpt at normal encoded speed. Recorder timeline sample mismatch retained; not measured hardware timing." } ] }