← Commit history
docs/build-clips.md+22
@@ -19,3 +19,25 @@ Native KiCad orbit and side views, at normal speed. USB now faces out of the boa ![3D review contact sheet](docs/clips/placement-3d-review-sheet.jpg)  Raw source, recorder timing caveats and review notes are indexed in [the shot log](../videos/shotlog.json). [AI Flow clips](clips.md) retain the earlier automatic captures, including excluded static footage.++## Mouser: two browser tools, one sourcing task++Pup's page shows the actual Access Denied response. The native browser reached Mouser, and normal exact-part search recovered the product and manufacturer CAD link. These short result clips were captured after discovery for comparison; they are not presented as an uninterrupted original navigation. No login or private credentials are shown.++### Pup: access denied++<video controls preload="metadata" poster="/docs/clips/mouser-pup-denied-poster.jpg" src="/docs/clips/mouser-pup-denied.mp4"></video>++### Native browser: exact fan product++<video controls preload="metadata" poster="/docs/clips/mouser-native-product-poster.jpg" src="/docs/clips/mouser-native-product.mp4"></video>++### Native browser: manufacturer CAD link++<video controls preload="metadata" poster="/docs/clips/mouser-native-cad-poster.jpg" src="/docs/clips/mouser-native-cad.mp4"></video>++### GLB fan: off, 3000 RPM, 7000 RPM, off++<video controls preload="metadata" poster="/docs/clips/fan-motion-states-poster.jpg" src="/docs/clips/fan-motion-states.mp4"></video>++The fan sequence is **SIMULATED**, shown at the labeled **0.02× slow-motion scale**. Only the rotor moves; the housing stays fixed. It demonstrates the proposed component-motion extension, not measured fan operation or cooling qualification. [Fan component assets](https://wiki.adom.inc/adom/mf40200v1-1000u-g99).
docs/clips/fan-motion-states-poster.jpgadded
⋯ 1 unchanged line ⋯
docs/clips/fan-motion-states.mp4added
⋯ 1 unchanged line ⋯
docs/clips/mouser-native-cad-poster.jpgadded
⋯ 1 unchanged line ⋯
docs/clips/mouser-native-cad.mp4added
⋯ 1 unchanged line ⋯
docs/clips/mouser-native-product-poster.jpgadded
⋯ 1 unchanged line ⋯
docs/clips/mouser-native-product.mp4added
⋯ 1 unchanged line ⋯
docs/clips/mouser-pup-denied-poster.jpgadded
⋯ 1 unchanged line ⋯
docs/clips/mouser-pup-denied.mp4added
⋯ 1 unchanged line ⋯
docs/fan-feedback.mdadded+13
@@ -0,0 +1,13 @@+# Fan feedback and load interlock++Selected candidate: Sunon MF40200V1-1000U-G99, 5 V, nominal 7000 RPM ±15%, three wires including frequency-generator feedback. Manufacturer specification D04114960G-01, page 8, retrieved 2026-10-08 through Mouser's native-browser product page, shows two pulses per revolution and an open-collector output. Maximum pull-up supply 6 V, sink current 5 mA, low output 0.5 V. The existing 10 kΩ pull-up to 3.3 V draws at most 0.33 mA. J9: 1=5V_AUX, 2=GND, 3=FAN_TACH to MCU PA7. Verify wire colors and connector assembly against the drawing before energizing.++The fan runs whenever auxiliary power is present. Keep the load disabled during spin-up. The portable policy now refuses ARM unless qualified RPM is valid and within a provisional 5000–10000 RPM window; it latches F_FAN and clears the DAC/enable when speed leaves that window, becomes invalid/nonfinite, or no tach edge arrives for 250 ms. Returning fan motion never automatically rearms the load. These limits are conservative bring-up settings, not a validated airflow rating.++The new portable fan_tach module calculates RPM = 30,000,000 / period_us from rising edges, requires eight consecutive periods in the 3000–6000 µs window, resets qualification on out-of-window timing, handles 32-bit timer wrap and invalidates readings 15 ms after the last edge. The MCU adapter must wire timer/EXTI capture and atomically snapshot this ISR-owned state before passing fan_rpm and fan_rpm_valid to the load core. A tach wire stuck high or low, unplugged fan, stalled rotor and implausible pulse frequency must all fault. The firmware HAL and physical timing tests remain unfinished. The dashboard carries measured RPM; disconnected readings remain empty.++Host C tests cover slow, zero, implausibly fast, nonfinite and unqualified RPM, missing edges and latched shutdown. Physical validation must additionally test startup, locked rotor, supply variation, blocked airflow and tach disconnection at load. RPM alone does not prove airflow: case-temperature shutdown remains independent, and the heatsink/duct must pass thermal testing before raising the 5 W ceiling.++The reusable [fan component page](https://wiki.adom.inc/adom/mf40200v1-1000u-g99) contains the symbol, mechanical mounting pattern, nominal STEP, animated GLB, thumbnails and fan-specific state skill. Its preview is simulated and does not switch this board's always-powered fan. The proposed motion contract separates `Fan.Housing` and `Fan.Rotor`; `Fan.Rotor.Revolution` plays at RPM/60 cycles per second, with explicit presentation scaling. Measured telemetry must carry freshness/validity; unknown feedback must never look like confirmed OFF.++Shared integration is pending owner review: [AIFlow PR 32](https://wiki.adom.inc/adom/adom-aiflow/prs/32), [component widgets PR 1](https://wiki.adom.inc/adom/component-widgets/prs/1), [wiki authoring PR 8](https://wiki.adom.inc/adom/adom-wiki-skillpack/prs/8), and [Probing issue 1](https://wiki.adom.inc/adom/adom-aiflow-probing/issues/1). These are proposals and a working reference preview, not a released probing simulator capability.
firmware/fan_tach.cadded+16
@@ -0,0 +1,16 @@+#include "fan_tach.h"+#include <string.h>+void fan_tach_init(fan_tach *f){memset(f,0,sizeof(*f));}+void fan_tach_edge(fan_tach *f,uint32_t now){+ if(!f->seen){f->seen=true;f->last_edge_us=now;return;}+ uint32_t period=now-f->last_edge_us;f->last_edge_us=now;+ /* 2 pulses/rev. Invalid timing restarts qualification, never becomes a healthy edge. */+ if(period<3000 || period>6000){f->count=0;f->next=0;return;}+ f->periods_us[f->next]=period;f->next=(f->next+1)%8;if(f->count<8)f->count++;+}+bool fan_tach_read(const fan_tach *f,uint32_t now,float *rpm){+ *rpm=0;+ if(!f->seen || f->count<8 || now-f->last_edge_us>15000)return false;+ uint32_t sum=0;for(unsigned i=0;i<8;i++)sum+=f->periods_us[i];+ *rpm=30000000.0f*8.0f/(float)sum;return true;+}
firmware/fan_tach.hadded+10
@@ -0,0 +1,10 @@+#ifndef ADOM_FAN_TACH_H+#define ADOM_FAN_TACH_H+#include <stdbool.h>+#include <stdint.h>+typedef struct { uint32_t last_edge_us, periods_us[8]; unsigned count, next; bool seen; } fan_tach;+void fan_tach_init(fan_tach *f);+/* Call for rising edges only from a microsecond timer. Snapshot atomically outside ISR. */+void fan_tach_edge(fan_tach *f, uint32_t now_us);+bool fan_tach_read(const fan_tach *f, uint32_t now_us, float *rpm);+#endif
firmware/load_core.c+3−1
@@ -10,7 +10,9 @@ static uint32_t checks(const load_core *c,const load_sample *s){  uint32_t f=0;  if(!s->sensors_valid || !isfinite(s->volts) || !isfinite(s->amps) || !isfinite(s->case_c) || age(s->now_ms,s->sample_ms)>50) f|=F_SENSOR;  if(!s->hw_ok) f|=F_HARDWARE;- if(age(s->now_ms,s->last_tach_ms)>250) f|=F_FAN;+ /* Provisional bring-up window for the 7000 RPM fan; validate cooling physically.+    A recent edge alone cannot prove useful speed (or reject noise). */+ if(!s->fan_rpm_valid || !isfinite(s->fan_rpm) || s->fan_rpm<5000 || s->fan_rpm>10000 || age(s->now_ms,s->last_tach_ms)>250) f|=F_FAN;  if(age(s->now_ms,c->lease_ms)>1000) f|=F_LINK;  if(s->case_c>=55 || s->case_c< -20) f|=F_THERMAL;  if(s->volts<4.5f || s->volts>24 || s->amps< -0.02f || s->amps>3.15f) f|=F_ENVELOPE;
firmware/load_core.h+2
@@ -7,6 +7,8 @@ enum { F_SENSOR=1, F_HARDWARE=2, F_FAN=4, F_LINK=8, F_THERMAL=16, F_ENVELOPE=32 typedef struct {  uint32_t now_ms, sample_ms, last_tach_ms;  float volts, amps, case_c;+ /* Measured over multiple tach periods; valid only after spin-up qualification. */+ float fan_rpm; bool fan_rpm_valid;  bool sensors_valid, hw_ok, latch_q, assembly_qualified; } load_sample; typedef struct {
tests/test_fan_tach.cadded+17
@@ -0,0 +1,17 @@+#include "../firmware/fan_tach.h"+#include <assert.h>+#include <math.h>+#include <stdio.h>+int main(void){+ fan_tach f;float rpm;fan_tach_init(&f);assert(!fan_tach_read(&f,0,&rpm));+ uint32_t t=0;fan_tach_edge(&f,t);+ for(int i=0;i<7;i++){t+=4286;fan_tach_edge(&f,t);assert(!fan_tach_read(&f,t,&rpm));}+ t+=4286;fan_tach_edge(&f,t);assert(fan_tach_read(&f,t,&rpm));assert(fabsf(rpm-7000)<1);+ assert(!fan_tach_read(&f,t+15001,&rpm)&&rpm==0); /* stale/stuck wire */+ t+=100;fan_tach_edge(&f,t);assert(!fan_tach_read(&f,t,&rpm)); /* glitch resets */+ for(int i=0;i<8;i++){t+=4286;fan_tach_edge(&f,t);}assert(fan_tach_read(&f,t,&rpm));+ t+=7000;fan_tach_edge(&f,t);assert(!fan_tach_read(&f,t,&rpm)); /* below window */+ fan_tach_init(&f);t=UINT32_MAX-1000;fan_tach_edge(&f,t);+ for(int i=0;i<8;i++){t+=4286;fan_tach_edge(&f,t);}assert(fan_tach_read(&f,t,&rpm)); /* wrap */+ puts("fan_tach: qualification, RPM, stale input, noise, slow fan and timer wrap passed");+}
tests/test_load_core.c+10−1
@@ -2,7 +2,7 @@ #include <assert.h> #include <math.h> #include <stdio.h>-static load_sample good(uint32_t t){return (load_sample){.now_ms=t,.sample_ms=t,.last_tach_ms=t,.volts=5,.amps=0,.case_c=25,.sensors_valid=true,.hw_ok=true,.latch_q=true,.assembly_qualified=true};}+static load_sample good(uint32_t t){return (load_sample){.now_ms=t,.sample_ms=t,.last_tach_ms=t,.fan_rpm=7000,.fan_rpm_valid=true,.volts=5,.amps=0,.case_c=25,.sensors_valid=true,.hw_ok=true,.latch_q=true,.assembly_qualified=true};} int main(void){  load_core c;load_init(&c,0);load_sample s=good(0);  assert(!c.enable&&c.dac_code==0);@@ -23,6 +23,15 @@ int main(void){  for(unsigned t=10;t<=260;t+=10){s=good(t);s.last_tach_ms=0;load_tick(&c,&s);}assert(c.faults&F_FAN);  load_init(&c,0);s=good(0);assert(load_arm(&c,&s));s=good(10);s.case_c=55;load_tick(&c,&s);assert(c.faults&F_THERMAL);  load_init(&c,0);s=good(0);assert(load_arm(&c,&s));s=good(10);load_tick(&c,&s);s=good(20);s.latch_q=false;load_tick(&c,&s);assert(c.faults&F_HARDWARE);+ /* Fan evidence must be qualified before arm, and stays required while running. */+ const float bad_rpm[]={0,4999,10001,NAN,INFINITY};+ for(unsigned i=0;i<sizeof(bad_rpm)/sizeof(bad_rpm[0]);i++){+  load_init(&c,0);s=good(0);s.fan_rpm=bad_rpm[i];assert(!load_arm(&c,&s));+  s=good(0);assert(load_arm(&c,&s));s=good(10);s.fan_rpm=bad_rpm[i];load_tick(&c,&s);+  assert(c.state==LOAD_FAULT&&(c.faults&F_FAN)&&!c.enable&&!c.dac_code);+  s=good(20);load_tick(&c,&s);assert(c.state==LOAD_FAULT); /* no automatic restart */+ }+ load_init(&c,0);s=good(0);s.fan_rpm_valid=false;assert(!load_arm(&c,&s));  load_init(&c,0);  load_off(&c);assert(c.state==LOAD_OFF&&!c.enable&&c.requested_a==0);  puts("load_core: safety policy tests passed (host C build; no MCU/HAL validation)");
videos/shotlog.json+51
@@ -37,6 +37,57 @@       "status": "contact sheet reviewed; board and changing views visible; final edit not approved",       "duration_wall_s": 9.711,       "note": "Unrouted board, no cooling assembly. Header side view confirms upright pins; generic package dimensional audit remains."+    },+    {+      "id": "mouser-native-06",+      "file": "raw/window-6491100-20261008-120324.mp4",+      "topic": "Native browser recovered Mouser product page",+      "note": "Static result shot; attempted scroll did not execute. Use a short hold only.",+      "duration_s": 25.85,+      "sha256": "39aab9be1d607b13b13f19898b03ace8a678bf20d150785fd0f786b0107cf5d7",+      "status": "captured; final edit not approved"+    },+    {+      "id": "mouser-pup-denial-07",+      "file": "raw/window-5573630-20261008-120401.mp4",+      "topic": "Pup actual Mouser Access Denied page",+      "note": "Captured after native recovery for editorial comparison; not original chronological discovery.",+      "duration_s": 12.1,+      "sha256": "6cf30dda27e0d601dadbe07309b51c8d53e6b41fa697803dba7b3e0b5b89f239",+      "status": "captured; final edit not approved"+    },+    {+      "id": "mouser-native-cad-08",+      "file": "raw/window-6491100-20261008-120542.mp4",+      "topic": "Native browser product to manufacturer CAD link walkthrough",+      "note": "Later walkthrough of the recovered page; review action before final edit.",+      "duration_s": 13.75,+      "sha256": "13a3a01bec0c1ac08021ebf64a6e73bfc278286c413fdbe89f5a8aef23700ea7",+      "status": "captured; final edit not approved"+    },+    {+      "id": "fan-motion-09",+      "file": "raw/window-5573630-20261008-121840.mp4",+      "topic": "Simulated fan GLB state motion",+      "status": "exclude: dark geometry and oversized axes; superseded",+      "sha256": "6de92fbd672d4ee2cfac4e661fc407504678cf57c1b239c5da4062145d6aaef0",+      "note": "Playback time scale 0.02 is explicitly labeled. Numeric RPM is simulated, not measured from hardware."+    },+    {+      "id": "fan-motion-10",+      "file": "raw/window-5573630-20261008-122523.mp4",+      "topic": "Simulated fan GLB state motion",+      "status": "exclude: superseded by complete off-on-off sequence",+      "sha256": "7274b103bb73eb4a58294a6c768a354cfde229caf3fa99f70d1574591e656408",+      "note": "Playback time scale 0.02 is explicitly labeled. Numeric RPM is simulated, not measured from hardware."+    },+    {+      "id": "fan-motion-11",+      "file": "raw/window-5573630-20261008-122709.mp4",+      "topic": "Simulated fan GLB state motion",+      "status": "contact sheet reviewed; real GLB off, 3000, 7000, off actions; final film not approved",+      "sha256": "1415789cca2a8acf54dbde7f51247c4240c4c28a06b702c05b7021649f635ced",+      "note": "Playback time scale 0.02 is explicitly labeled. Numeric RPM is simulated, not measured from hardware."     }   ],   "failed_captures": [