5from collections
import deque
8import matplotlib.pyplot
as plt
9from matplotlib.animation
import FuncAnimation
10from os.path
import isfile
14PACKET_TYPE_SENSOR_DATA = 3
15PACKET_HEADER_FORMAT =
"<BBI"
17SENSOR_DATA_PACKET_FORMAT =
"<BB"
18SENSOR_DATA_PACKET_SIZE = 2
19SENSOR_DATA_CHUNK_FORMAT =
"<I"
20SENSOR_DATA_CHUNK_SIZE = 4
21SENSOR_DATAPOINT_FORMAT =
"<BI"
22SENSOR_DATAPOINT_SIZE = 5
26 if len(data) < PACKET_HEADER_SIZE:
29 return struct.unpack(PACKET_HEADER_FORMAT, data[:PACKET_HEADER_SIZE])
35 if len(data) < PACKET_HEADER_SIZE + SENSOR_DATA_PACKET_SIZE:
38 if header
is None or header[0] != PACKET_TYPE_SENSOR_DATA:
40 packet_type, version, timestamp = header
41 offset = PACKET_HEADER_SIZE
43 num_chunks, num_sensors = struct.unpack(
44 SENSOR_DATA_PACKET_FORMAT,
45 data[offset : offset + SENSOR_DATA_PACKET_SIZE],
49 offset += SENSOR_DATA_PACKET_SIZE
50 per_chunk = SENSOR_DATA_CHUNK_SIZE + num_sensors * SENSOR_DATAPOINT_SIZE
53 < PACKET_HEADER_SIZE + SENSOR_DATA_PACKET_SIZE + num_chunks * per_chunk
57 for _
in range(num_chunks):
58 (chunk_ts,) = struct.unpack(
59 SENSOR_DATA_CHUNK_FORMAT, data[offset : offset + SENSOR_DATA_CHUNK_SIZE]
61 offset += SENSOR_DATA_CHUNK_SIZE
63 for _
in range(num_sensors):
64 sid, val = struct.unpack(
65 SENSOR_DATAPOINT_FORMAT, data[offset : offset + SENSOR_DATAPOINT_SIZE]
67 datapoints.append({
"sensor_id": sid,
"data": val})
68 offset += SENSOR_DATAPOINT_SIZE
69 chunks.append({
"timestamp": chunk_ts,
"datapoints": datapoints})
71 {
"packet_type": packet_type,
"version": version,
"timestamp": timestamp},
77def ema(data, alpha=0.5):
80 ema_filtered = data[0]
82 ema_filtered = alpha * d + (1 - alpha) * ema_filtered
88 return np.polyval(poly_coeffs, adc_code)
93 if len(data) < window:
95 window_data = np.array(data[-window:])
96 mean = np.mean(window_data)
97 std = np.std(window_data)
101 if (mean - std_threshold * std) <= d <= (mean + std_threshold * std)
103 return np.mean(filtered_data)
if filtered_data
else mean
107 ema_filtered =
ema(data, alpha=ema_alpha)
109 [ema_filtered], window=ma_window, std_threshold=std_threshold
115instrument_deques = []
116filtered_code_deques = []
118calibration_polynomials = []
119instrument_to_connector = (
124keep_calibrating =
True
125stop_serial_thread = threading.Event()
126pause_reading = threading.Event()
133 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
134 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
137 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
138 except (AttributeError, OSError):
143 sock.bind((bind_address, port))
144 print(f
"Listening for PT data on {bind_address}:{port}")
146 print(f
"Error binding to port {port}: {e}")
148 "Make sure pt_multi_gui is not already running, or that SO_REUSEPORT is supported."
153 while not stop_serial_thread.is_set():
154 if not pause_reading.is_set():
156 data, _ = sock.recvfrom(MAX_PACKET_SIZE)
159 header, chunks = result
163 for dp
in chunk[
"datapoints"]:
164 sid = int(dp[
"sensor_id"])
165 connector = sid
if 1 <= sid <= 10
else (sid + 1)
166 adc_code = int(dp[
"data"])
169 inst_idx = instrument_to_connector.index(connector)
170 if inst_idx < len(instrument_deques):
171 instrument_deques[inst_idx].append(adc_code)
177 pressure_readings = []
178 for j
in range(len(instrument_deques)):
179 if len(instrument_deques[j]) > 0:
181 list(instrument_deques[j])
183 filtered_code_deques[j].append(filtered_code)
184 filtered_codes.append(filtered_code)
186 calculated_pressure = (
188 filtered_code, calibration_polynomials[j]
191 pressure_deques[j].append(calculated_pressure)
193 list(pressure_deques[j])
195 pressure_readings.append(filtered_pressure)
197 code_output =
" | ".join(
199 f
"PT {j+1}: {code:.0f}"
200 for j, code
in enumerate(filtered_codes)
203 print(
"Filtered ADC code readings:", code_output)
205 pressure_output =
" | ".join(
207 f
"PT {j+1}: {pressure:.3f} psi"
208 for j, pressure
in enumerate(pressure_readings)
211 print(
"Calculated pressures:", pressure_output)
213 except socket.timeout:
215 except Exception
as e:
216 print(f
"Error processing data: {e}")
219 except KeyboardInterrupt:
220 print(
"UDP reading interrupted.")
228 pressure_input = input(
229 f
"Enter the {num_gauges} pressure gauge readings (comma-separated): "
231 pressures = [float(p.strip())
for p
in pressure_input.split(
",")]
232 if len(pressures) != num_gauges:
233 print(f
"Please enter exactly {num_gauges} pressure readings.")
237 print(
"Invalid input. Please enter valid numbers separated by commas.")
238 except KeyboardInterrupt:
239 print(
"\nInput interrupted. Please try again.")
249 for gauge_num, pressure
in enumerate(pressures, 1):
250 gauge_pressures[gauge_num] = pressure
251 if gauge_num
not in Y:
253 Y[gauge_num].append(pressure)
256 for pt_num
in range(len(instrument_deques)):
262 if all(len(Y[gauge]) > 1
for gauge
in Y):
263 for pt_num
in range(len(instrument_deques)):
265 gauge_num = gauge_mappings[
269 calibration_polynomials[pt_num] = np.polyfit(
270 np.array(X[pt_num]) + epsilon,
271 np.array(Y[gauge_num]) + epsilon,
275 f
"Updated polynomial coefficients for PT {pt_num + 1}: {calibration_polynomials[pt_num]}"
277 except np.RankWarning:
279 f
"Warning: The fit may be poorly conditioned for PT {pt_num + 1}."
281 except Exception
as e:
282 print(f
"Error fitting polynomial for PT {pt_num + 1}: {e}")
288 "Not enough data to fit a curve yet. Please enter at least one more data point."
293 with open(filename,
"a", newline=
"")
as f:
294 writer = csv.writer(f, delimiter=
",")
298 for pt_num
in range(len(instrument_deques)):
299 gauge_num = gauge_mappings[pt_num + 1]
301 row.extend([X[pt_num][-1], Y[gauge_num][-1]])
302 row.extend(calibration_polynomials[pt_num])
308 num_rows = (len(instrument_deques) + 1) // num_cols
310 fig, axs = plt.subplots(num_rows, num_cols, figsize=(12, 6), squeeze=
False)
312 for pt_num
in range(len(instrument_deques)):
313 x = np.array(X[pt_num])
314 gauge_num = gauge_mappings[pt_num + 1]
315 y = np.array(Y[gauge_num])
317 poly_coeffs = calibration_polynomials[pt_num]
318 trendline_y = np.polyval(poly_coeffs, x)
320 ss_res = np.sum((y - trendline_y) ** 2)
321 ss_tot = np.sum((y - np.mean(y)) ** 2)
322 r_squared = 1 - (ss_res / ss_tot)
324 row, col = divmod(pt_num, num_cols)
325 axs[row, col].scatter(x, y, color=
"purple", label=
"Data Points")
331 label=f
"Trendline (R² = {r_squared:.3f})",
334 axs[row, col].set_title(f
"PT {pt_num + 1} (Gauge {gauge_num})")
335 axs[row, col].set_xlabel(
"ADC Code")
336 axs[row, col].set_ylabel(
"Pressure (psi)")
337 axs[row, col].legend()
346 num_rows = (len(instrument_deques) + 1) // num_cols
348 fig, axs = plt.subplots(num_rows, num_cols, figsize=(12, 6), squeeze=
False)
349 times = deque(maxlen=save_count)
350 pressure_data = [deque(maxlen=save_count)
for _
in range(len(instrument_deques))]
356 times.append(current_time)
358 for j
in range(len(instrument_deques)):
359 if instrument_deques[j]:
362 filtered_value, calibration_polynomials[j]
364 pressure_data[j].append(calculated_pressure)
366 row, col = divmod(j, num_cols)
367 axs[row, col].clear()
370 list(pressure_data[j]),
371 label=f
"Instrument {j+1} Pressure",
375 axs[row, col].set_xlabel(
"Time (seconds)")
376 axs[row, col].set_ylabel(
"Calibrated Pressure (psi)")
377 axs[row, col].legend(loc=
"upper left")
378 axs[row, col].set_title(f
"Live Pressure Readings for Instrument {j+1}")
382 ani = FuncAnimation(fig, update, interval=100, frames=
None, repeat=
True)
386if __name__ ==
"__main__":
390 instrument_count = int(
391 input(
"Please input the number of PTs for calibration: ")
395 print(
"Invalid input. Please enter an integer.")
398 print(
"\nFor each PT, specify which board connector (1-10) it is connected to:")
399 for pt_num
in range(1, instrument_count + 1):
402 conn = int(input(f
"Enter board connector (1-10) for PT {pt_num}: "))
404 instrument_to_connector.append(conn)
407 print(
"Please enter a valid connector between 1 and 10.")
409 print(
"Invalid input. Please enter an integer.")
414 gauge_count = int(input(
"Please input the number of pressure gauges: "))
417 print(
"Invalid input. Please enter an integer.")
420 print(
"\nFor each PT, specify which gauge it's connected to:")
421 for pt_num
in range(1, instrument_count + 1):
425 input(f
"Enter gauge number (1-{gauge_count}) for PT {pt_num}: ")
427 if 1 <= gauge_num <= gauge_count:
428 gauge_mappings[pt_num] = gauge_num
432 f
"Please enter a valid gauge number between 1 and {gauge_count}"
435 print(
"Invalid input. Please enter an integer.")
438 gauge_pressures = {i: 0
for i
in range(1, gauge_count + 1)}
445 "Enter the polynomial order for the fit (e.g., 1 for linear, 2 for quadratic, etc.): "
450 print(
"Invalid input. Please enter an integer.")
455 instrument_deques = [deque(maxlen=data_point_num)
for _
in range(instrument_count)]
456 calibration_polynomials = [[0] * (poly_order + 1)
for _
in range(instrument_count)]
457 filtered_code_deques = [
458 deque(maxlen=data_point_num)
for _
in range(instrument_count)
460 pressure_deques = [deque(maxlen=data_point_num)
for _
in range(instrument_count)]
461 X = [[]
for _
in range(instrument_count)]
462 Y = {i: []
for i
in range(1, gauge_count + 1)}
466 file_base = f
"PT Calibration Attempt {time.strftime('%Y-%m-%d', time.gmtime())}"
470 while isfile(file_base + f
"_test{test_num}" + file_ext):
473 filename = file_base + f
"_test{test_num}" + file_ext
478 with open(filename,
"w", newline=
"")
as f:
479 writer = csv.writer(f, delimiter=
",")
481 for pt_index
in range(instrument_count):
482 connector_id = instrument_to_connector[pt_index]
483 base_label = f
"PT{connector_id}"
485 f
"{base_label} ADC Code",
486 f
"{base_label} Pressure",
489 [f
"{base_label} Coefficient {k}" for k
in range(poly_order + 1)]
491 header.extend(pt_header)
492 writer.writerow(header)
495 udp_thread = threading.Thread(target=read_udp, args=(5006,
"0.0.0.0"))
496 udp_thread.daemon =
True
499 print(
"\nPress Ctrl+C to enter new pressure gauge readings.")
500 print(f
"When prompted, enter {gauge_count} comma-separated pressure values.")
502 calibration_count = 0
503 continue_calibration =
True
504 while continue_calibration:
506 pause_reading.clear()
509 except KeyboardInterrupt:
510 print(
"\nEntering calibration mode...")
515 calibration_count += 1
520 input(
"Would you like to continue calibrating? (y/n): ")
524 if user_input ==
"y":
526 elif user_input ==
"n":
527 continue_calibration =
False
530 print(
"Invalid input. Please enter 'y' or 'n'.")
531 except KeyboardInterrupt:
532 print(
"\nInput interrupted. Please try again.")
535 stop_serial_thread.set()
538 print(
"Calibration complete.")
541 "Would you like to see a live feed of the calibrated pressure readings vs time? (y/n): "
546 if show_live_feed ==
"y":
547 stop_serial_thread.clear()
548 pause_reading.clear()
549 udp_thread = threading.Thread(target=read_udp, args=(5006,
"0.0.0.0"))
550 udp_thread.daemon =
True
555 stop_serial_thread.set()
558print(
"Program terminated.")
filtered_sensor_data(data, window=5, std_threshold=2)
parse_packet_header(data)
plot_data_with_trendline()
apply_dual_filter(data, ema_alpha=0.1, ma_window=250, std_threshold=0.001)
parse_sensor_data_packet(data)
get_gauge_pressures(num_gauges)
calculate_pressure_from_polynomial(adc_code, poly_coeffs)
read_udp(port=5006, bind_address="0.0.0.0")
live_feed(save_count=500)
TOML_NODISCARD_CTOR time() noexcept=default