STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
sense_conversions.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Sense conversions: K-type thermocouple, Pt100/Pt1000 RTD, load cell.
4Ported from diablo_server/frontend/lib/sense-conversions.ts (DiabloAvionics sense_testing_gui.py).
5
6Used by calibration_server.py as fallback when RobustCalibrationFramework has no calibration
7points, or for quick raw→physical conversion. Single source of truth for calibration service.
8"""
9
10from __future__ import annotations
11
12import json
13import math
14from pathlib import Path
15from typing import Optional
16
17# K-type thermocouple: voltage (V) -> temperature (°C), ITS-90 rational polynomial (Mosaic/NIST-style)
18# Each row: (v_min_mV, v_max_mV, T0, V0, p1, p2, p3, p4, q1, q2, q3)
19K_TYPE_INVERSE: list[tuple[float, ...]] = [
20 (
21 -6.404,
22 -3.554,
23 -121.47164,
24 -4.1790858,
25 36.069513,
26 30.722076,
27 7.791386,
28 0.52593997,
29 0.93939547,
30 0.2779128,
31 0.02516334,
32 ),
33 (
34 -3.554,
35 4.096,
36 -8.7935962,
37 -0.34489914,
38 25.678719,
39 -0.49887904,
40 -0.44705222,
41 -0.044869202,
42 0.00023893439,
43 -0.02039775,
44 -0.0018424107,
45 ),
46 (
47 4.096,
48 16.397,
49 310.18976,
50 12.631386,
51 24.061949,
52 4.0158622,
53 0.26853917,
54 -0.0097188544,
55 0.16995872,
56 0.011413069,
57 -0.00039275155,
58 ),
59 (
60 16.397,
61 33.275,
62 605.72562,
63 25.148718,
64 23.539401,
65 0.046547228,
66 0.0134444,
67 0.0005923685,
68 0.00083445513,
69 0.0004612144,
70 0.00002548812,
71 ),
72 (
73 33.275,
74 69.553,
75 1018.4705,
76 41.99385,
77 25.783239,
78 -1.8363403,
79 0.05617666,
80 0.000185324,
81 -0.074803355,
82 0.002384186,
83 0,
84 ),
85]
86
87# Pt100/Pt1000 RTD: R(T) = R0*(1 + A*T + B*T^2) for T >= 0; solve for T (IEC 60751)
88PT_A = 3.9083e-3
89PT_B = -5.775e-7
90PT1000_R0 = 1000.0
91PT100_R0 = 100.0
92RTD_EXCITATION_UA = 1000.0
93
94ADC32_FULL_SCALE = 2147483648.0
95
96
97def _uint32_to_int32(u: int) -> int:
98 """Interpret uint32 as int32 (for 32-bit signed ADC codes)."""
99 u = u & 0xFFFFFFFF
100 return u - 0x100000000 if u > 0x7FFFFFFF else u
101
102
103# ── K-type thermocouple ────────────────────────────────────────────────────
104
105
106def k_type_voltage_to_temp_c(v_volts: float) -> Optional[float]:
107 """
108 Convert K-type thermocouple voltage (V) to temperature (°C).
109 Returns None if out of range.
110 """
111 v_mv = v_volts * 1000.0
112 for row in K_TYPE_INVERSE:
113 v_lo, v_hi, t0, v0, p1, p2, p3, p4, q1, q2, q3 = row
114 if v_lo <= v_mv <= v_hi:
115 x = v_mv - v0
116 num = p1 + x * (p2 + x * (p3 + x * p4))
117 den = 1.0 + x * (q1 + x * (q2 + x * q3))
118 if abs(den) < 1e-20:
119 return None
120 return t0 + (x * num) / den
121 return None
122
123
125 adc_code: int,
126 adc_ref_voltage: float = 2.5,
127 adc_full_scale: float = ADC32_FULL_SCALE,
128) -> Optional[float]:
129 """
130 Convert K-type TC raw ADC code to temperature (°C).
131 Assumes 32-bit signed ADC: voltage = (adc / adc_full_scale) * adc_ref_voltage.
132 """
133 code = _uint32_to_int32(adc_code & 0xFFFFFFFF)
134 v_volts = (code / adc_full_scale) * adc_ref_voltage
135 return k_type_voltage_to_temp_c(v_volts)
136
137
138# ── RTD (Pt100/Pt1000) ──────────────────────────────────────────────────────
139
140
141def rtd_resistance_to_temp_c(r_ohm: float, r0: float = PT1000_R0) -> Optional[float]:
142 """
143 Convert RTD resistance (Ω) to temperature (°C). IEC 60751.
144 Supports Pt100 (r0=100) and Pt1000 (r0=1000) via r0 parameter.
145 Returns None if out of range.
146 """
147 rr = r_ohm / r0
148 d = PT_A * PT_A - 4 * PT_B * (1.0 - rr)
149 if d < 0:
150 return None
151 sqrt_d = math.sqrt(d)
152 t = (-PT_A + sqrt_d) / (2 * PT_B)
153 if -400 <= t <= 1100:
154 return t
155 return None
156
157
159 v_volts: float,
160 r0: float = PT1000_R0,
161 excitation_ua: float = RTD_EXCITATION_UA,
162) -> Optional[float]:
163 """
164 Convert RTD differential voltage (V) to temperature (°C).
165 excitation_ua = IDAC current in µA (default 1000).
166 """
167 if excitation_ua <= 0:
168 return None
169 r_ohm = (abs(v_volts) * 1e6) / excitation_ua
170 return rtd_resistance_to_temp_c(r_ohm, r0)
171
172
174 raw_value: float,
175 r0: float = PT100_R0,
176 scale_to_ohms: float = 0.001,
177) -> Optional[float]:
178 """
179 Convert raw resistance counts (often 24-bit or 32-bit milliohms) to temperature (°C).
180 Default assumes value is milliohms (scale_to_ohms=0.001).
181 """
182 if raw_value is None or not math.isfinite(raw_value):
183 return None
184 r_ohm = raw_value * scale_to_ohms
185 return rtd_resistance_to_temp_c(r_ohm, r0)
186
187
189 adc_code: int,
190 r0: float = PT1000_R0,
191 excitation_ua: float = RTD_EXCITATION_UA,
192 adc_ref_voltage: float = 2.5,
193 adc_full_scale: float = ADC32_FULL_SCALE,
194) -> Optional[float]:
195 """
196 Convert RTD raw ADC code to temperature (°C).
197 Assumes ADC measures differential voltage across RTD with known excitation current.
198 """
199 code = _uint32_to_int32(adc_code & 0xFFFFFFFF)
200 v_volts = (code / adc_full_scale) * adc_ref_voltage
201 return rtd_voltage_to_temp_c(v_volts, r0, excitation_ua)
202
203
204# ── Load cell ──────────────────────────────────────────────────────────────
205
206
208 code_uint32: int,
209 sensitivity_mv_per_v: float,
210 pga_gain: float,
211 full_scale_value: float,
212) -> Optional[float]:
213 """
214 Ratiometric load-cell force from raw 32-bit ADC code.
215 Reference = excitation, so voltage cancels.
216 full_scale_value is in desired units (e.g. lbf, kg).
217 Returns None if invalid params.
218 """
219 if pga_gain <= 0 or sensitivity_mv_per_v <= 0:
220 return None
221 code_int32 = _uint32_to_int32(code_uint32 & 0xFFFFFFFF)
222 code_fs = (sensitivity_mv_per_v / 1000.0) * pga_gain * ADC32_FULL_SCALE
223 if code_fs <= 0:
224 return None
225 return (code_int32 / code_fs) * full_scale_value
226
227
228# ── Convenience: raw conversion by sensor type ───────────────────────────────
229
230
231def _load_pt_polynomial(channel_id: int) -> Optional[list[float]]:
232 """
233 Load calibration polynomial coefficients [A, B, C, D] for a PT channel from the
234 latest JSON file in tools/calibration/calibrations/.
235 Polynomial: psi = A*adc^3 + B*adc^2 + C*adc + D
236 """
237 cal_dir = Path(__file__).parent / "calibrations"
238 try:
239 files = sorted(
240 [
241 f
242 for f in cal_dir.iterdir()
243 if f.suffix == ".json" and "learned_prior" not in f.name
244 ],
245 reverse=True,
246 )
247 for fp in files:
248 try:
249 data = json.loads(fp.read_text())
250 polys = data.get("calibration_polynomials", {})
251 key = str(channel_id)
252 if key in polys and len(polys[key]) >= 4:
253 return list(polys[key][:4])
254 except Exception:
255 continue
256 except Exception:
257 pass
258 return None
259
260
262 adc_code: int,
263 full_scale_psi: float,
264 sense_resistor_ohms: float,
265 adc_ref_voltage: float = 2.5,
266) -> float:
267 """
268 4-20 mA HP PT: psi = (i_ma - 4) / 16 * full_scale_psi.
269 v_sense = (adc/2^31)*adc_ref_voltage, i_ma = v_sense/sense_resistor_ohms * 1000.
270 Wire codes are unsigned 0..2^31-1 (match FSW calibration_main convert_hp_pt_to_pressure).
271 """
272 ADC_MAX = 2147483648.0
273 I_MIN_MA, I_SPAN_MA = 4.0, 16.0
274 adc_u = adc_code & 0xFFFFFFFF
275 if adc_u >= int(ADC_MAX):
276 return 0.0
277 if sense_resistor_ohms <= 0.0 or not math.isfinite(adc_ref_voltage):
278 return 0.0
279 v_sense = (float(adc_u) / ADC_MAX) * adc_ref_voltage
280 i_ma = (v_sense / sense_resistor_ohms) * 1000.0
281 if not math.isfinite(v_sense) or not math.isfinite(i_ma):
282 return 0.0
283 if i_ma < I_MIN_MA:
284 return 0.0
285 # Clamp >20 mA to 20 mA (do not snap to full_scale — that discontinuity caused plot spikes).
286 if i_ma > 20.0:
287 i_ma = 20.0
288 psi = ((i_ma - I_MIN_MA) / I_SPAN_MA) * full_scale_psi
289 if not math.isfinite(psi):
290 return 0.0
291 return max(0.0, min(psi, full_scale_psi * 1.05))
292
293
294def pt_adc_to_psi(adc_code: int, channel_id: int) -> Optional[float]:
295 """
296 Convert raw ADC code to PSI using the stored cubic calibration polynomial.
297 Returns None if no calibration is available for the channel.
298 Polynomial: psi = A*adc^3 + B*adc^2 + C*adc + D (coeffs[0..3] = A, B, C, D)
299 """
300 coeffs = _load_pt_polynomial(channel_id)
301 if coeffs is None:
302 return None
303 A, B, C, D = coeffs
304 x = float(adc_code)
305 return A * x * x * x + B * x * x + C * x + D
306
307
309 stype: str,
310 raw_value: int,
311 *,
312 channel_id: Optional[int] = None,
313 # RTD (raw is ADC counts; convert via voltage)
314 rtd_r0: float = PT1000_R0,
315 rtd_adc_ref_v: float = 2.5,
316 rtd_excitation_ua: float = RTD_EXCITATION_UA,
317 # TC
318 tc_adc_ref_v: float = 2.5,
319 # LC
320 lc_sensitivity_mv_per_v: float = 2.0,
321 lc_pga_gain: float = 128.0,
322 lc_full_scale_value: float = 100.0,
323) -> Optional[float]:
324 """
325 Convert raw value to physical unit by sensor type.
326 Returns PSI for PT (if calibration available), °C for TC/RTD, force (lbf/kg) for LC,
327 or None if conversion fails.
328 RTD raw_value is ADC counts (not milliohms); uses raw_rtd_adc_to_temp_c.
329 """
330 if stype == "PT":
331 return pt_adc_to_psi(raw_value, channel_id or 0)
332 if stype == "RTD":
334 raw_value,
335 r0=rtd_r0,
336 adc_ref_voltage=rtd_adc_ref_v,
337 excitation_ua=rtd_excitation_ua,
338 )
339 if stype == "TC":
340 return k_type_adc_to_temp_c(raw_value, adc_ref_voltage=tc_adc_ref_v)
341 if stype == "LC":
342 return code_to_force(
343 raw_value,
344 sensitivity_mv_per_v=lc_sensitivity_mv_per_v,
345 pga_gain=lc_pga_gain,
346 full_scale_value=lc_full_scale_value,
347 )
348 return None
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
Optional[float] code_to_force(int code_uint32, float sensitivity_mv_per_v, float pga_gain, float full_scale_value)
Optional[float] raw_rtd_to_temp_c(float raw_value, float r0=PT100_R0, float scale_to_ohms=0.001)
Optional[float] rtd_voltage_to_temp_c(float v_volts, float r0=PT1000_R0, float excitation_ua=RTD_EXCITATION_UA)
Optional[float] pt_adc_to_psi(int adc_code, int channel_id)
Optional[float] raw_rtd_adc_to_temp_c(int adc_code, float r0=PT1000_R0, float excitation_ua=RTD_EXCITATION_UA, float adc_ref_voltage=2.5, float adc_full_scale=ADC32_FULL_SCALE)
float hp_pt_adc_to_psi(int adc_code, float full_scale_psi, float sense_resistor_ohms, float adc_ref_voltage=2.5)
Optional[float] k_type_voltage_to_temp_c(float v_volts)
int _uint32_to_int32(int u)
Optional[float] k_type_adc_to_temp_c(int adc_code, float adc_ref_voltage=2.5, float adc_full_scale=ADC32_FULL_SCALE)
Optional[list[float]] _load_pt_polynomial(int channel_id)
Optional[float] raw_to_physical(str stype, int raw_value, *, Optional[int] channel_id=None, float rtd_r0=PT1000_R0, float rtd_adc_ref_v=2.5, float rtd_excitation_ua=RTD_EXCITATION_UA, float tc_adc_ref_v=2.5, float lc_sensitivity_mv_per_v=2.0, float lc_pga_gain=128.0, float lc_full_scale_value=100.0)
Optional[float] rtd_resistance_to_temp_c(float r_ohm, float r0=PT1000_R0)