11import type { SdValue } from "../../../lib/wcars/sdSignals" ;
22import { useSdValue } from "../../../lib/wcars/sdSignals" ;
3+ import { usePackAggregates , usePackTemp } from "../../../lib/wcars/packAggregates" ;
4+ import { predictedPackV , predictedSagV } from "../../../lib/wcars/batteryModel" ;
5+
6+ /** Health of the battery model: how closely the measured (summed) pack
7+ * voltage tracks the OCV-predicted value at the current SoC. A small
8+ * delta is normal during current draw (== sag). At rest, the delta
9+ * should be ≈ 0. */
10+ type Health = "ok" | "drift" | "bad" | "missing" ;
11+ function healthFromDelta ( deltaV : number ) : Health {
12+ const ad = Math . abs ( deltaV ) ;
13+ if ( ad < 2 ) return "ok" ;
14+ if ( ad < 8 ) return "drift" ;
15+ return "bad" ;
16+ }
317
418/** A telemetry readout pinned to a schematic coordinate.
519 * label sits above, value + unit below, coloured by status. */
@@ -31,21 +45,28 @@ function Readout({
3145}
3246
3347export function ElecSchematic ( ) {
34- const v = useSdValue ( "packV" ) ;
48+ const pack = usePackAggregates ( ) ;
49+ // Synthetic SdValue for the computed pack voltage, so the existing
50+ // EcamValueBox status logic still works.
51+ const packVsd : SdValue = {
52+ value : pack . cellCount > 0 ? pack . packVoltage : null ,
53+ label : "" ,
54+ unit : "V" ,
55+ status : pack . cellCount > 0 ? "normal" : "missing" ,
56+ } ;
3557 const a = useSdValue ( "packA" ) ;
3658 const soc = useSdValue ( "packSoc" ) ;
3759 const dcl = useSdValue ( "packDcl" ) ;
3860 const ccl = useSdValue ( "packCcl" ) ;
3961 const busV = useSdValue ( "busV" ) ;
4062 const busA = useSdValue ( "busA" ) ;
41- const hv = useSdValue ( "hvActive" ) ;
42-
43- // Bus is energized per the real HV_Active flag from the safety loop;
44- // fall back to DC bus voltage only if that frame hasn't arrived.
45- const energized =
46- hv . status !== "missing"
47- ? ( hv . value ?? 0 ) >= 0.5
48- : busV . status !== "missing" && ( busV . value ?? 0 ) > 50 ;
63+ const airPos = useSdValue ( "airPos" ) ;
64+ const airNeg = useSdValue ( "airNeg" ) ;
65+
66+ // Bus is energized when both halves of the AIR contactor pair are closed.
67+ const airPosClosed = airPos . status !== "missing" && ( airPos . value ?? 0 ) >= 1 ;
68+ const airNegClosed = airNeg . status !== "missing" && ( airNeg . value ?? 0 ) >= 0.5 ;
69+ const energized = airPosClosed && airNegClosed ;
4970 const flow = energized ? " wcars-flow--on" : "" ;
5071
5172 // SoC bar fill (bottom-up).
@@ -63,10 +84,30 @@ export function ElecSchematic() {
6384 // Switch arm endpoint: closed (horizontal) when energized, else lifted.
6485 const armEnd = energized ? { x : 510 , y : 290 } : { x : 505 , y : 266 } ;
6586
87+ // OCV model: predict the open-circuit pack voltage at the current SoC,
88+ // and the expected sag at the current draw + pack temperature.
89+ const packT = usePackTemp ( ) ;
90+ const socFrac2 = ( soc . value ?? 0 ) / 100 ;
91+ const hasSoc = soc . status !== "missing" && soc . value !== null ;
92+ const hasCells = pack . cellCount > 0 ;
93+ const hasTemp = packT . tempC !== null ;
94+ const hasCurrent = a . status !== "missing" && a . value !== null ;
95+ const predV = hasSoc ? predictedPackV ( socFrac2 ) : null ;
96+ const sagV =
97+ hasSoc && hasCurrent && hasTemp
98+ ? predictedSagV ( a . value ?? 0 , socFrac2 , packT . tempC ?? 25 )
99+ : null ;
100+ const measV = hasCells ? pack . packVoltage : null ;
101+ // At rest (low current), the measured pack V should ≈ OCV-predicted. Under
102+ // load, the difference ≈ sag. We surface the raw delta for honesty; the
103+ // HEALTH field classifies the magnitude.
104+ const deltaV = measV !== null && predV !== null ? measV - predV : null ;
105+ const health : Health = deltaV === null ? "missing" : healthFromDelta ( deltaV ) ;
106+
66107 return (
67108 < div className = "wcars-syn wcars-syn-elec wcars-elec-schem" data-testid = "syn-elec" >
68109 < div className = "wcars-syn-title" > ELEC / BATT</ div >
69- < svg viewBox = "0 0 1000 540" className = "wcars-schem-svg" role = "img" aria-label = "Electrical synoptic" >
110+ < svg viewBox = "0 0 1000 540" className = "wcars-schem-svg" role = "img" aria-label = "Electrical synoptic" preserveAspectRatio = "xMidYMid meet" >
70111 { /* ---- HV BATTERY PACK ---- */ }
71112 < rect className = "wcars-schem-box" x = { 70 } y = { 150 } width = { 310 } height = { 270 } rx = { 10 } />
72113 < text className = "wcars-schem-head" x = { 225 } y = { 184 } textAnchor = "middle" > HV BATT</ text >
@@ -80,14 +121,14 @@ export function ElecSchematic() {
80121 < Readout x = { BAR_X + BAR_W / 2 } y = { BAR_Y + BAR_H + 34 } label = "SOC" sd = { soc } />
81122
82123 { /* Pack readouts */ }
83- < Readout x = { 280 } y = { 232 } label = "HV" sd = { v } />
124+ < Readout x = { 280 } y = { 232 } label = "HV" sd = { packVsd } decimals = { 1 } />
84125 < Readout x = { 280 } y = { 322 } label = "CURR" sd = { a } />
85126
86127 { /* Positive terminal nub */ }
87128 < rect className = "wcars-schem-term" x = { 380 } y = { 272 } width = { 16 } height = { 36 } />
88129
89- { /* ---- AIR (accumulator isolation relay), closed when HV_Active ---- */ }
90- < text className = "wcars-schem-tag" x = { 480 } y = { 252 } textAnchor = "middle" > AIR</ text >
130+ { /* ---- AIR (accumulator isolation relay pair ), closed when both halves are made ---- */ }
131+ < text className = "wcars-schem-tag" x = { 480 } y = { 252 } textAnchor = "middle" > AIR+ / AIR- </ text >
91132 < line className = { `wcars-flow${ flow } ` } x1 = { 396 } y1 = { 290 } x2 = { 450 } y2 = { 290 } />
92133 < circle className = "wcars-schem-node" cx = { 450 } cy = { 290 } r = { 6 } />
93134 < line
@@ -116,6 +157,40 @@ export function ElecSchematic() {
116157 { /* ---- CURRENT LIMITS (envelope) ---- */ }
117158 < Readout x = { 160 } y = { 500 } label = "DISCH LIM" sd = { dcl } anchor = "middle" />
118159 < Readout x = { 300 } y = { 500 } label = "CHG LIM" sd = { ccl } anchor = "middle" />
160+
161+ { /* ---- HEALTH PANEL: MEAS vs PRED vs SAG ---- */ }
162+ < rect className = "wcars-health-panel" x = { 410 } y = { 445 } width = { 530 } height = { 85 } rx = { 6 } />
163+ < text className = "wcars-schem-head wcars-schem-head--small" x = { 420 } y = { 463 } > BATTERY HEALTH (100S)</ text >
164+ < text className = "wcars-schem-tag" x = { 420 } y = { 478 } textAnchor = "start" >
165+ cell Δ { hasCells ? `${ ( ( ( pack . maxVoltage ?? 0 ) - ( pack . minVoltage ?? 0 ) ) * 1000 ) . toFixed ( 0 ) } mV · ${ pack . cellCount } cells` : "—" }
166+ </ text >
167+ < text className = { `wcars-health-cell wcars-health-cell--${ health } ` } x = { 490 } y = { 505 } textAnchor = "middle" >
168+ < tspan className = "wcars-health-lbl" x = { 490 } dy = { 0 } > MEAS</ tspan >
169+ < tspan className = "wcars-health-val" x = { 490 } dy = { 20 } >
170+ { measV === null ? "XX" : `${ measV . toFixed ( 1 ) } V` }
171+ </ tspan >
172+ </ text >
173+ < text className = "wcars-health-cell" x = { 610 } y = { 505 } textAnchor = "middle" >
174+ < tspan className = "wcars-health-lbl" x = { 610 } dy = { 0 } > PRED</ tspan >
175+ < tspan className = "wcars-health-val" x = { 610 } dy = { 20 } >
176+ { predV === null ? "XX" : `${ predV . toFixed ( 1 ) } V` }
177+ </ tspan >
178+ </ text >
179+ < text className = "wcars-health-cell" x = { 730 } y = { 505 } textAnchor = "middle" >
180+ < tspan className = "wcars-health-lbl" x = { 730 } dy = { 0 } > SAG</ tspan >
181+ < tspan className = "wcars-health-val" x = { 730 } dy = { 20 } >
182+ { sagV === null ? "XX" : `${ sagV . toFixed ( 1 ) } V` }
183+ </ tspan >
184+ </ text >
185+ < text className = "wcars-health-cell" x = { 850 } y = { 505 } textAnchor = "middle" >
186+ < tspan className = "wcars-health-lbl" x = { 850 } dy = { 0 } > Δ</ tspan >
187+ < tspan className = "wcars-health-val" x = { 850 } dy = { 20 } >
188+ { deltaV === null ? "XX" : `${ deltaV >= 0 ? "+" : "" } ${ deltaV . toFixed ( 1 ) } V` }
189+ </ tspan >
190+ </ text >
191+ < text className = "wcars-schem-tag" x = { 930 } y = { 463 } textAnchor = "end" >
192+ T { packT . tempC === null ? "XX" : `${ packT . tempC . toFixed ( 1 ) } °C` } · { packT . sensorsRead } /{ packT . totalSensors } thm
193+ </ text >
119194 </ svg >
120195 </ div >
121196 ) ;
0 commit comments