STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
demo_sensor_sender.py
Go to the documentation of this file.
1"""
2Fake sensor data generator for demo mode.
3Builds UDP packets in the same format as the real boards so the GUI receives and
4decodes them exactly like real hardware.
5
6Buffer layout matches Diablo::create_sensor_data_packet() in DAQv2-Comms:
7 - DiabloPackets.h: PacketHeader, SensorDataPacket, SensorDataChunk, SensorDatapoint
8 - DiabloPacketUtils.cpp: write order = header -> body_header -> for each chunk
9 (chunk timestamp -> num_sensors × datapoint)
10No Qt or socket usage here.
11"""
12
13import struct
14import time
15from typing import Dict, List, Tuple
16
17import numpy as np
18
19# Same as DAQv2-Comms.h
20MAX_PACKET_SIZE = 512
21
22# Packet layout: must match DiabloPackets.h packed structs (little-endian)
23# PacketHeader: packet_type (uint8_t), version (uint8_t), timestamp (uint32_t)
24PACKET_HEADER_FORMAT = "<BBI"
25PACKET_HEADER_SIZE = 6
26# SensorDataPacket: num_chunks (uint8_t), num_sensors (uint8_t)
27SENSOR_DATA_PACKET_FORMAT = "<BB"
28SENSOR_DATA_PACKET_SIZE = 2
29# SensorDataChunk: timestamp (uint32_t)
30SENSOR_DATA_CHUNK_FORMAT = "<I"
31SENSOR_DATA_CHUNK_SIZE = 4
32# SensorDatapoint: sensor_id (uint8_t), data (uint32_t)
33SENSOR_DATAPOINT_FORMAT = "<BI"
34SENSOR_DATAPOINT_SIZE = 5
35
36# Calibration CSVs use ADC codes from ~2e8 to ~2e9; clamp range must allow 0–800+ psi
37DEMO_CODE_MIN = 0
38DEMO_CODE_MAX = (1 << 32) - 1 # uint32 max
39
40# Precomputed demo signals: arrays of normalized [0,1] for psi range mapping
41PRECOMPUTED_LENGTH = 2000
42PRECOMPUTED_STEP = 0.05 # seconds per sample
43_DEMO_NORM_ARRAYS: List[List[float]] = []
44
45
46def _demo_signal_sine(t: float) -> float:
47 """Sine wave, period 4 s, scaled to [0, 1]."""
48 return 0.5 + 0.5 * np.sin(2 * np.pi * t / 4)
49
50
51def _demo_signal_ramp(t: float) -> float:
52 """Sawtooth, repeating ramp up, period 4 s."""
53 return (t % 4) / 4
54
55
56def _demo_signal_square(t: float) -> float:
57 """Square wave, period 2 s."""
58 return 1.0 if (int(t / 2) % 2) == 0 else 0.0
59
60
61def _demo_signal_noise(t: float) -> float:
62 """Smoothed deterministic noise in [0, 1]."""
63 x = 0.5 + 0.4 * np.sin(t * 1.3) * np.cos(t * 2.7) + 0.05 * np.sin(t * 5.1)
64 return max(0.0, min(1.0, x))
65
66
67def _demo_signal_step(t: float) -> float:
68 """Two levels with slow transition, period 3 s per level."""
69 return 1.0 if (int(t / 3) % 2) == 0 else 0.0
70
71
72def _demo_signal_triangle(t: float) -> float:
73 """Triangle wave, period 4 s."""
74 x = (t % 4) / 4
75 return 2 * x if x < 0.5 else 2 * (1 - x)
76
77
78def _demo_signal_double_sine(t: float) -> float:
79 """Beat pattern from two close frequencies."""
80 x = 0.5 * np.sin(2 * np.pi * t / 3) + 0.5 * np.sin(2 * np.pi * t / 3.2)
81 return 0.5 + 0.5 * max(-1, min(1, x))
82
83
84def _demo_signal_decay(t: float) -> float:
85 """Repeating exponential decay, period 2 s."""
86 u = t % 2
87 y = np.exp(-u)
88 lo, hi = np.exp(-2), 1.0
89 return (y - lo) / (hi - lo) if hi > lo else 0.5
90
91
92def _demo_signal_pulse(t: float) -> float:
93 """Short periodic spikes, period 2 s, duty 0.1 s."""
94 return 1.0 if 0 < (t % 2) < 0.1 else 0.0
95
96
97def _demo_signal_drift(t: float) -> float:
98 """Slow linear drift + small deterministic variation, period 20 s."""
99 drift = (t / 20) % 1
100 noise = 0.1 * np.sin(t * 2.1) * np.cos(t * 1.7)
101 return max(0.0, min(1.0, 0.1 + 0.8 * drift + noise))
102
103
104def _demo_signal_linear_cycle(t: float) -> float:
105 """Triangle wave, period 4 s, normalized [0, 1] (linear 0->1->0)."""
106 x = (t % 4) / 4
107 return 2 * x if x < 0.5 else 2 * (1 - x)
108
109
110def _demo_signal_quadratic_cycle(t: float) -> float:
111 """Parabola 0->1->0, period 4 s, normalized [0, 1]."""
112 phase = (t % 4) / 4
113 return 4 * phase * (1 - phase)
114
115
116def _demo_signal_cubic_cycle(t: float) -> float:
117 """Smooth cubic 0->1->0, period 4 s, normalized [0, 1]."""
118 phase = (t % 4) / 4
119 return 16 * phase * phase * (1 - phase) * (1 - phase)
120
121
122DEMO_SIGNAL_FUNCTIONS = [
123 _demo_signal_sine,
124 _demo_signal_ramp,
125 _demo_signal_square,
126 _demo_signal_noise,
127 _demo_signal_step,
128 _demo_signal_triangle,
129 _demo_signal_double_sine,
130 _demo_signal_decay,
131 _demo_signal_pulse,
132 _demo_signal_drift,
133 _demo_signal_linear_cycle,
134 _demo_signal_quadratic_cycle,
135 _demo_signal_cubic_cycle,
136]
137
138
139# PT 4 -> linear, PT 1 -> quadratic, PT 5 -> cubic (indices into _DEMO_NORM_ARRAYS), 0-800 psi
140DEMO_PT_SPECIAL_SIGNALS = {
141 4: (10, 0.0, 800.0), # linear_cycle
142 1: (11, 0.0, 800.0), # quadratic_cycle
143 5: (12, 0.0, 800.0), # cubic_cycle
144}
145
146
148 """Fill _DEMO_NORM_ARRAYS with fixed arrays of normalized [0,1] (run once at import)."""
149 global _DEMO_NORM_ARRAYS
150 if _DEMO_NORM_ARRAYS:
151 return
152 for sig_fn in DEMO_SIGNAL_FUNCTIONS:
153 arr = []
154 for k in range(PRECOMPUTED_LENGTH):
155 t = k * PRECOMPUTED_STEP
156 norm = sig_fn(t)
157 norm = max(0.0, min(1.0, norm))
158 arr.append(float(norm))
159 _DEMO_NORM_ARRAYS.append(arr)
160
161
162def _psi_to_code(psi: float, a: float, b: float, c: float, d: float) -> int:
163 """Inverse of cubic calibration: find ADC code that yields given psi (Newton-Raphson)."""
164 # psi = a*code^3 + b*code^2 + c*code + d
165 code = float((DEMO_CODE_MIN + DEMO_CODE_MAX) // 2)
166 for _ in range(15):
167 f = (a * code * code * code) + (b * code * code) + (c * code) + d - psi
168 fp = (3 * a * code * code) + (2 * b * code) + c
169 if abs(fp) < 1e-20:
170 break
171 code = code - f / fp
172 code = max(DEMO_CODE_MIN, min(DEMO_CODE_MAX, int(round(code))))
173 return code & 0xFFFFFFFF
174
175
176# _precompute_demo_arrays() <-- Lazy loaded in build_demo_packet
177
178
180 chunks: List[Tuple[int, List[Tuple[int, int]]]],
181 packet_type: int,
182 version: int,
183 timestamp_ms: int,
184 max_packet_size: int,
185) -> bytes:
186 """
187 Build a sensor data UDP packet. Byte layout is identical to
188 Diablo::create_sensor_data_packet() in DAQv2-Comms (DiabloPacketUtils.cpp):
189 header (6) -> num_chunks, num_sensors (2) -> for each chunk: timestamp (4)
190 then num_sensors × (sensor_id (1), data (4)).
191 chunks: list of (chunk_timestamp_ms, [(sensor_id, adc_code_uint32), ...])
192 All chunks must have the same number of datapoints (num_sensors).
193 """
194 if not chunks:
195 return b""
196 num_chunks = len(chunks)
197 num_sensors = len(chunks[0][1])
198 if not all(len(c[1]) == num_sensors for c in chunks):
199 return b""
200 per_chunk_size = SENSOR_DATA_CHUNK_SIZE + num_sensors * SENSOR_DATAPOINT_SIZE
201 total_size = (
202 PACKET_HEADER_SIZE + SENSOR_DATA_PACKET_SIZE + num_chunks * per_chunk_size
203 )
204 if total_size > max_packet_size:
205 return b""
206 packet = bytearray(total_size)
207 offset = 0
208 struct.pack_into(
209 PACKET_HEADER_FORMAT, packet, offset, packet_type, version, timestamp_ms
210 )
211 offset += PACKET_HEADER_SIZE
212 struct.pack_into(SENSOR_DATA_PACKET_FORMAT, packet, offset, num_chunks, num_sensors)
213 offset += SENSOR_DATA_PACKET_SIZE
214 for chunk_ts, datapoints in chunks:
215 struct.pack_into(SENSOR_DATA_CHUNK_FORMAT, packet, offset, chunk_ts)
216 offset += SENSOR_DATA_CHUNK_SIZE
217 for sensor_id, adc_code in datapoints:
218 struct.pack_into(
219 SENSOR_DATAPOINT_FORMAT,
220 packet,
221 offset,
222 sensor_id,
223 adc_code & 0xFFFFFFFF,
224 )
225 offset += SENSOR_DATAPOINT_SIZE
226 return bytes(packet)
227
228
230 pt_calibration: Dict[int, Tuple[float, float, float, float]],
231 demo_start_time: float,
232 stats_start_time: float,
233 packet_type: int,
234 version: int,
235 max_packet_size: int,
236 num_connectors: int = 10,
237 psi_min: float = 0.0,
238 psi_max: float = 700.0,
239) -> bytes:
240 """
241 Build one UDP packet with synthetic PT data for demo mode.
242 Same structure as real boards: one chunk per connector (1..num_connectors),
243 one datapoint per chunk. Normalized signals [0,1] are mapped to [psi_min, psi_max]
244 and converted to ADC codes via inverse calibration so displayed psi ranges 0-700 for all.
245 """
246 if not _DEMO_NORM_ARRAYS:
248
249 now = time.time()
250 elapsed = now - demo_start_time
251 idx = int(elapsed * 20) % PRECOMPUTED_LENGTH
252 ts_ms = int((now - stats_start_time) * 1000) & 0xFFFFFFFF
253 base_ms = int(now * 1000) & 0xFFFFFFFF
254 default_code = (DEMO_CODE_MIN + DEMO_CODE_MAX) // 2
255 chunks = []
256 for connector_id in range(1, num_connectors + 1):
257 chunk_ts_ms = (base_ms - (num_connectors - connector_id)) & 0xFFFFFFFF
258 if connector_id in pt_calibration:
259 if connector_id in DEMO_PT_SPECIAL_SIGNALS:
260 signal_index, pmin, pmax = DEMO_PT_SPECIAL_SIGNALS[connector_id]
261 norm = _DEMO_NORM_ARRAYS[signal_index][idx]
262 psi = pmin + (pmax - pmin) * norm
263 else:
264 signal_index = (connector_id - 1) % 10
265 norm = _DEMO_NORM_ARRAYS[signal_index][idx]
266 psi = psi_min + (psi_max - psi_min) * norm
267 a, b, c, d = pt_calibration[connector_id]
268 code = _psi_to_code(psi, a, b, c, d)
269 else:
270 code = default_code
271 chunks.append((chunk_ts_ms, [(connector_id, code)]))
273 chunks, packet_type, version, ts_ms, max_packet_size
274 )
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
bytes build_demo_packet(Dict[int, Tuple[float, float, float, float]] pt_calibration, float demo_start_time, float stats_start_time, int packet_type, int version, int max_packet_size, int num_connectors=10, float psi_min=0.0, float psi_max=700.0)
float _demo_signal_ramp(float t)
float _demo_signal_decay(float t)
float _demo_signal_cubic_cycle(float t)
float _demo_signal_square(float t)
bytes create_sensor_data_packet(List[Tuple[int, List[Tuple[int, int]]]] chunks, int packet_type, int version, int timestamp_ms, int max_packet_size)
float _demo_signal_step(float t)
float _demo_signal_pulse(float t)
float _demo_signal_triangle(float t)
float _demo_signal_noise(float t)
float _demo_signal_sine(float t)
float _demo_signal_double_sine(float t)
float _demo_signal_linear_cycle(float t)
float _demo_signal_drift(float t)
int _psi_to_code(float psi, float a, float b, float c, float d)
float _demo_signal_quadratic_cycle(float t)
TOML_NODISCARD_CTOR time() noexcept=default