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.
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.
15from typing
import Dict, List, Tuple
24PACKET_HEADER_FORMAT =
"<BBI"
27SENSOR_DATA_PACKET_FORMAT =
"<BB"
28SENSOR_DATA_PACKET_SIZE = 2
30SENSOR_DATA_CHUNK_FORMAT =
"<I"
31SENSOR_DATA_CHUNK_SIZE = 4
33SENSOR_DATAPOINT_FORMAT =
"<BI"
34SENSOR_DATAPOINT_SIZE = 5
38DEMO_CODE_MAX = (1 << 32) - 1
41PRECOMPUTED_LENGTH = 2000
42PRECOMPUTED_STEP = 0.05
43_DEMO_NORM_ARRAYS: List[List[float]] = []
47 """Sine wave, period 4 s, scaled to [0, 1]."""
48 return 0.5 + 0.5 * np.sin(2 * np.pi * t / 4)
52 """Sawtooth, repeating ramp up, period 4 s."""
57 """Square wave, period 2 s."""
58 return 1.0
if (int(t / 2) % 2) == 0
else 0.0
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))
68 """Two levels with slow transition, period 3 s per level."""
69 return 1.0
if (int(t / 3) % 2) == 0
else 0.0
73 """Triangle wave, period 4 s."""
75 return 2 * x
if x < 0.5
else 2 * (1 - x)
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))
85 """Repeating exponential decay, period 2 s."""
88 lo, hi = np.exp(-2), 1.0
89 return (y - lo) / (hi - lo)
if hi > lo
else 0.5
93 """Short periodic spikes, period 2 s, duty 0.1 s."""
94 return 1.0
if 0 < (t % 2) < 0.1
else 0.0
98 """Slow linear drift + small deterministic variation, period 20 s."""
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))
105 """Triangle wave, period 4 s, normalized [0, 1] (linear 0->1->0)."""
107 return 2 * x
if x < 0.5
else 2 * (1 - x)
111 """Parabola 0->1->0, period 4 s, normalized [0, 1]."""
113 return 4 * phase * (1 - phase)
117 """Smooth cubic 0->1->0, period 4 s, normalized [0, 1]."""
119 return 16 * phase * phase * (1 - phase) * (1 - phase)
122DEMO_SIGNAL_FUNCTIONS = [
128 _demo_signal_triangle,
129 _demo_signal_double_sine,
133 _demo_signal_linear_cycle,
134 _demo_signal_quadratic_cycle,
135 _demo_signal_cubic_cycle,
140DEMO_PT_SPECIAL_SIGNALS = {
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:
152 for sig_fn
in DEMO_SIGNAL_FUNCTIONS:
154 for k
in range(PRECOMPUTED_LENGTH):
155 t = k * PRECOMPUTED_STEP
157 norm =
max(0.0,
min(1.0, norm))
158 arr.append(float(norm))
159 _DEMO_NORM_ARRAYS.append(arr)
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)."""
165 code = float((DEMO_CODE_MIN + DEMO_CODE_MAX) // 2)
167 f = (a * code * code * code) + (b * code * code) + (c * code) + d - psi
168 fp = (3 * a * code * code) + (2 * b * code) + c
172 code =
max(DEMO_CODE_MIN,
min(DEMO_CODE_MAX, int(round(code))))
173 return code & 0xFFFFFFFF
180 chunks: List[Tuple[int, List[Tuple[int, int]]]],
184 max_packet_size: int,
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).
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):
200 per_chunk_size = SENSOR_DATA_CHUNK_SIZE + num_sensors * SENSOR_DATAPOINT_SIZE
202 PACKET_HEADER_SIZE + SENSOR_DATA_PACKET_SIZE + num_chunks * per_chunk_size
204 if total_size > max_packet_size:
206 packet = bytearray(total_size)
209 PACKET_HEADER_FORMAT, packet, offset, packet_type, version, timestamp_ms
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:
219 SENSOR_DATAPOINT_FORMAT,
223 adc_code & 0xFFFFFFFF,
225 offset += SENSOR_DATAPOINT_SIZE
230 pt_calibration: Dict[int, Tuple[float, float, float, float]],
231 demo_start_time: float,
232 stats_start_time: float,
235 max_packet_size: int,
236 num_connectors: int = 10,
237 psi_min: float = 0.0,
238 psi_max: float = 700.0,
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.
246 if not _DEMO_NORM_ARRAYS:
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
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
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]
271 chunks.append((chunk_ts_ms, [(connector_id, code)]))
273 chunks, packet_type, version, ts_ms, max_packet_size
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)
None _precompute_demo_arrays()
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