From 0744c060f169712fb6200e0f9c31c5c5b4f310fa Mon Sep 17 00:00:00 2001 From: Gertjan Date: Thu, 12 Mar 2026 22:39:06 +0100 Subject: [PATCH] This version is stable --- scanner.py | 305 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 scanner.py diff --git a/scanner.py b/scanner.py new file mode 100644 index 0000000..a01eb32 --- /dev/null +++ b/scanner.py @@ -0,0 +1,305 @@ +import gc +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import re +import telnetlib3 +import csv +import time +import pyodbc +import math +from datetime import datetime + +data_rows = [] # global 2-D list +state = { + "ampl": 1600, + "remaining_receive_lines": 0, + "freq": 10000.0, + "freq_step_multiply": 0.85, + "stop_freq": 0.1, + "initializing": 1, + "noise_scan": False +} + +PATTERN = re.compile( + r"Va=(?P-?\d+\.?\d*)\s+" + r"Vp=(?P-?\d+\.?\d*)\s+\|\s+" + r"Ia=(?P-?\d+\.?\d*)\s+" + r"Ip=(?P-?\d+\.?\d*)\s+\|\s+" + r"ZR=(?P-?\d+\.?\d*)\s+" + r"ZX=(?P-?\d+\.?\d*)" + r".*?irq=\d+\s+" # Skip to 'irq=', match the first digits and space + r"(?P[0-9a-fA-F]+)-" # First hex + r"(?P[0-9a-fA-F]+)\s+" # Second hex + r"(?P[0-9a-fA-F]+)-" # Third hex + r"(?P[0-9a-fA-F]+)" # Fourth hex +) + +# Database connection parameters +conn_str = ( + "DRIVER={FreeTDS};" + "SERVER=10.1.20.18;" + "PORT=1433;" + "DATABASE=H2_HealthMonitoring01;" + "UID=rh\\sa_H2_HealthMon01;" + "PWD=AAo6EM2pttfV5EVZrWqB;" + "TDS_Version=7.4;" +) + +def dump_into_database(): + try: + with pyodbc.connect(conn_str) as conn: + cursor = conn.cursor() + sweep_insert_time = datetime.now() + print("dump into database\n") + # Prepare the data: SQL Server expects the columns in order. + if state["noise_scan"]==False: + # regular scan data + formatted_rows = [ + [sweep_insert_time, r[0], r[5], r[6], r[1], r[2], r[3], r[4]] + for r in data_rows + ] + sql = """ + INSERT INTO SequenceValues (StartTimeOfSweep, Freq, ZR, ZX, Vampl, Vphase, Iampl, Iphase) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) \ + """ + else: + # noise scan data + formatted_rows = [ + [sweep_insert_time, r[0], r[1], r[3]] + for r in data_rows + ] + sql = """ + INSERT INTO SequenceNoiseFloor (StartTimeOfSweep, Freq, Vampl, Iampl) + VALUES (?, ?, ?, ?) \ + """ + + print("execute\n") + + # Execute in bulk + cursor.fast_executemany = False # needs less RAM + cursor.executemany(sql, formatted_rows) + + print("commit\n") + conn.commit() + print(f"Successfully inserted {len(formatted_rows)} rows.") + + except Exception as e: + print(f"Database error: {e}") + +def dump_csv(filename="data.csv"): + df = pd.DataFrame( + data_rows, + columns=["Freq", "Va", "Vp", "Ia", "Ip", "ZR", "ZX", "adc_vmin", "adc_vmax", "adc_imin", "adc_imax"] + ) + df.to_csv(filename, index=False) + +def append_line_to_file(column_nr, filename): + # Extract freq column (index 0) + # Extract ZR column (index 5) + # Extract ZX column (index 6) + zr_values = [row[column_nr] for row in data_rows if len(row) > column_nr] + if not zr_values: + print("No ZR data to write") + return + with open(filename, "a", newline="") as f: + writer = csv.writer(f) + writer.writerow(zr_values) + +def append_Nyquist_run(filename): + # Flatten ZR/ZX pairs into one row + row = [] + for r in data_rows: + if len(r) > 6: + row.extend([r[5], r[6]]) + if not row: + print("No ZR/ZX data to write") + return + with open(filename, "a", newline="") as f: + writer = csv.writer(f) + writer.writerow(row) + + +def extract_to_dataframe(line): + # Extract Va, Vp, Ia, Ip, ZR, ZX from a line and append them as a row to the global data_rows list. + global state + match = PATTERN.search(line) + if not match: + return # silently ignore malformed lines + row = [ + float(state["freq"]), + float(match.group("Va")), + float(match.group("Vp")), + float(match.group("Ia")), + float(match.group("Ip")), + float(match.group("ZR")), + float(match.group("ZX")), + int(match.group("adc_vmin"), 16), + int(match.group("adc_vmax"), 16), + int(match.group("adc_imin"), 16), + int(match.group("adc_imax"), 16), + ] + print("uncorr:\r") + print(row) + row[1] = row[1]/(2*3.14159*row[0]*0.00005 + 1) # compensate Va for pole at 20kHz + row[5] = row[1]/row[3] * math.cos(0.01745*(row[2]-row[4])) + row[6] = row[1]/row[3] * math.sin(0.01745*(row[2]-row[4])) + print("corr:\r") + print(row) + data_rows.append(row) + +def process_line(line): + extract_to_dataframe(line) + # Example: print last row + if data_rows: + print("Last row:", data_rows[-1]) + +def get_dataframe(): + return pd.DataFrame( + data_rows, + columns=["Va", "Vp", "Ia", "Ip", "ZR", "ZX", "adc_vmin", "adc_vmax", "adc_imin", "adc_imax"] + ) + +class TelnetReader: + def __init__(self, host, port=23, timeout=10): + self.host = host + self.port = port + self.timeout = timeout + self.tn = None + + def connect(self): + print(f"Connecting to {self.host}:{self.port} ...") + self.tn = telnetlib3.Telnet(self.host, self.port, self.timeout) + print("Connected.") + + def disconnect(self): + if self.tn: + self.tn.close() + self.tn = None + print("Disconnected.") + + def read_loop(self): + global state + # Main loop: reads incoming lines forever + try: + while state["freq"] > state["stop_freq"]: + line = self.tn.read_until(b"\n") # read line + if not line: + break + + decoded = line.decode("utf-8", errors="ignore").strip() + self.process_line(decoded) # extract all info from line + + except KeyboardInterrupt: + print("Interrupted by user.") + print(data_rows) + if state["freq"]>0.00001: # check if scan endeed normally (e.g. no abort due to clipping) + #dump_csv("measurements.csv") + dump_into_database() + #append_line_to_file(0, "scan_freq.csv") + #append_line_to_file(1, "scan_Va.csv") + #append_line_to_file(2, "scan_Vp.csv") + #append_line_to_file(3, "scan_Ia.csv") + #append_line_to_file(4, "scan_Ip.csv") + #append_line_to_file(5, "scan_ZR.csv") + #append_line_to_file(6, "scan_ZX.csv") + #append_Nyquist_run("scan_Nyquist.csv") + #state["noise_scan"] = not state["noise_scan"] + else: + print("scan aborted\r") + gc.collect() # clean up internal memory (garbage collect) + + def process_line(self, line): + global state +# print(f"RAW: {line}") + if not line.startswith("Va="): # skip lines that are not to be analyzed + return + if state["remaining_receive_lines"] > 0 : + state["remaining_receive_lines"] -= 1 # we are waiting for settling - just skip the line + else: + # prepare new frequency measurement + if state["initializing"] == 1: + # since we just start a frequency scan, let's set the R, Max and Amplitude + response = f"\rr516\r" # set Resistance value for scaling HAL sensor + self.tn.write(response.encode("utf-8")) + response = f"m1600\r" # set max amplitude value + self.tn.write(response.encode("utf-8")) + # for noise scans the amplitude is zero, else the state["ampl"] + if state["noise_scan"]==True: + response = f"a0\r" # zero amplitude for noise measurement + else: + response = f"a{state["ampl"]:.1f}\r" # set amplitude + self.tn.write(response.encode("utf-8")) + # there will be a lot of lines, but they will be skipped as they do not match the pattern + else: # regular loop (not initializing) + extract_to_dataframe(line) # capture the measurement +# print(f"minmax: {data_rows[7]},{data_rows[8]},{data_rows[9]},{data_rows[10] }\n") + if data_rows and (data_rows[-1][7]<5 or data_rows[-1][8]>250 or data_rows[-1][9]<5 or data_rows[-1][10]>250): + #response = f"a{state["ampl"]:.1f}\r" # send ampl to trigger sweep + #self.tn.write(response.encode("utf-8")) + # there will be a lot of lines, but they will be skipped as they do not match the pattern + state["freq"] = 0 # force ending of the scan, and write no data in the database + # calculate next freq + if state["freq"] > 1.0: + freq_multiplier = state["freq_step_multiply"] # calc next freq + else: + if state["freq"] > 0.01: + freq_multiplier = state["freq_step_multiply"] ** 2 # skip 1/2 steps to speed up + else: + freq_multiplier = state["freq_step_multiply"] ** 4 # skip 3/4 steps to speed up + if state["noise_scan"]==True: + state["freq"] *= freq_multiplier ** 2 # noise scans step twice as quickly + else: + state["freq"] *= freq_multiplier + if (state["freq"]>40) and (state["freq"]<660) and ((state["freq"]%50<2.5) or (-state["freq"]%50<2.5)): + state["freq"] *= freq_multiplier # if near a 50Hz harmonic, skip to the next frequency + while (state["freq"]<0.6) and ((state["freq"]%0.053<0.008) or (-state["freq"]%0.053<0.008)): + state["freq"] *= freq_multiplier # if near a 0.052Hz harmonic, skip to the next frequency + if state["freq"] <= state["stop_freq"]: + print("Reached stop frequency") + else: + if state["freq"] > 1: + response = f"\rf{state["freq"]:.1f}\r" # new freq + else: + response = f"\rf{state["freq"]:.3f}\r" # new freq + self.tn.write(response.encode("utf-8")) + bandwidth = state["freq"]/20 + if bandwidth > 0.5 : + bandwidth = 0.5 + response = f"b{bandwidth:.3f}\r" # new freq + self.tn.write(response.encode("utf-8")) + state["remaining_receive_lines"] = 1 + 4/bandwidth + print(line) + print(state) + state["initializing"] = 0 + + # Example: + # value = self.extract_value(line) + # self.store_value(value) + + # Example placeholder methods + def extract_value(self, line): + pass + + def store_value(self, value): + pass + + +if __name__ == "__main__": + reader = TelnetReader(host="localhost", port=2002) +# reader = TelnetReader(host="10.1.122.152", port=2002) +# reader = TelnetReader(host="192.168.1.196", port=2002) + + try: + reader.connect() + while True: + data_rows.clear() + state["ampl"] = 1600; + state["remaining_receive_lines"] = 0; + state["freq"] = 1000; + state["freq_step_multiply"] = 0.95; + state["stop_freq"] = 0.1; + state["initializing"] = 1; + reader.read_loop() + finally: + reader.disconnect()