""" Modbus Range Limiter Test Script (RPU + TPU) ============================================= Auto-detects device type by reading HARDWARE_REV_HI (holding reg 163): 0x0021 → TPU (fenghuang-dsp) 0x0022 → RPU (garuda-dsp) RPU trigger: write MAIN_FORCE_NORMAL_MODE password (addrs 48/49). Firmware calls modbus_to_config() → LimitModbusRange() → write_all_flash() → soft_reset(). TPU trigger: write holding coil bit 49 (MODBUS_ALL_CFG_CMD_BIT, FC5). update_modbus_input() detects write_flash==1 → modbus_to_config() → LimitModbusRange() → write_all_flash() → soft_reset(). Both write_all_flash() implementations call DINT then soft_reset() — the device reboots immediately; the Modbus ACK for the trigger write may never arrive. NoResponseError on the trigger write is expected and means the rewrite WAS triggered. Data types: 'uint16' — single holding register, unsigned 16-bit (FC3 read / FC6 write) 'int16' — single holding register, signed 16-bit 'float32' — two consecutive holding registers, IEEE 754 single-precision, stored big-endian (high word first) via EndianConvert32Bits. Read with FC3 / write with FC16. Comparison uses FLOAT_EPSILON tolerance. TPU float calibration limits are fixed constants from fenghuang-dsp/app/main/global_variables.h: Gain limits = [nominal × 0.9, nominal × 1.1] Offset limits = [nominal × 0.8, nominal × 1.2] (For negative offset defaults this gives an inverted range; firmware clamping is verified as-is.) PFC_current_rms_cali_B excluded (offset nominal = 0.0 → trivial limits). Test phases (batch approach): Phase 0: Read and save all defaults. Phase 1: Write ALL below-min values → single trigger → read all back. Phase 2: Write ALL above-max values → single trigger → read all back. Restore: Write all defaults back → trigger. Output: timestamped CSV report. """ import csv import os import time import minimalmodbus # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- SLAVE_ID = 16 # DEFAULT_SLAVE_ID = 0x10 BAUD = 9600 TIMEOUT = 2 # seconds REWRITE_WAIT = 10.0 # write_all_flash() → soft_reset() → full reboot; allow time FLOAT_EPSILON = 1e-4 # relative tolerance for float readback comparison (0.01%) # RPU trigger: MAIN_FORCE_NORMAL_MODE password (garuda-dsp modbus_mapping.h) ADDR_PASSWORD_LO = 48 # MODBUS_FORCE_PASSWORD_LO ADDR_PASSWORD_HI = 49 # MODBUS_FORCE_PASSWORD_HI PASSWORD_LO = 0xFEED PASSWORD_HI = 0xCEE5 # TPU trigger: holding coil bit 49 (fenghuang-dsp modbus_mapping.h) TPU_BIT_WRITE_FLASH = 49 # MODBUS_ALL_CFG_CMD_BIT → main_app.write_flash # TPU state register: input reg 61 (MODBUS_MAIN_STATE), read via FC4. # Firmware only processes write_flash in these states (app_modbus.c update_modbus_input). ADDR_TPU_STATE = 61 TPU_TRIGGER_ALLOWED_STATES = { 2, # TPU_READY 10, # TPU_INITIALIZATION 15, # TPU_DEBUG_CONFIG 16, # TPU_TEST_MODE 0xEE, # TPU_ERROR } # Auto-detection register (holding reg 163, same address for both RPU and TPU) ADDR_HARDWARE_REV_HI = 163 # MODBUS_HARDWARE_REVISION_HI HW_REV_TPU = 0x0021 HW_REV_RPU = 0x0022 # --------------------------------------------------------------------------- # RPU register test list (garuda-dsp) # --------------------------------------------------------------------------- # Columns: (label, addr, data_type, min_val, max_val, below_min_input, above_max_input) # # Limits source: garuda-dsp app/app_modbus/app_modbus.h + global_variables.h RPU_TESTS = [ # ----- VI operation settings ----- ('Voltage_setting_Q128', 12, 'uint16', 0, 8320, None, 8321), ('Current_setting_Q128', 14, 'uint16', 0, 3840, None, 3841), ('Power_limit_ref', 15, 'uint16', 0, 1500, None, 1501), ('No_Battery_Voltage_ref_Q128', 141, 'uint16', 0, 8320, None, 8321), # ----- Protection thresholds ----- ('Cutoff_voltage_Q128', 50, 'uint16', 0, 8320, None, 8321), ('OVP_setting_Q128', 11, 'uint16', 0, 8320, None, 8321), ('Cutoff_over_current_Q128', 10, 'uint16', 0, 4480, None, 4481), ('Cutoff_minimum_current_Q128', 51, 'uint16', 0, 640, None, 641), # ----- Precharge / CV current ----- ('Precharge_current', 6, 'int16', 0, 1280, -1, 1281), ('CV_current_limit', 7, 'int16', 0, 1280, -1, 1281), # ----- SR control ----- ('SR_limit_lo', 16, 'uint16', 0, 1280, None, 1281), ('SR_limit_hi', 17, 'uint16', 0, 1280, None, 1281), ('SR_duty_ref', 89, 'uint16', 20, 50, 19, 51), ('SR_delay_ref', 91, 'int16', 0, 100, -1, 101), # ----- ADC calibration ----- ('Adc_cali_a_Iout', 20, 'int16', 950, 1100, 949, 1101), ('Adc_cali_b_Iout', 21, 'int16', -32767, 32767, -32768, None), ('Adc_cali_a_Vdcbus', 22, 'int16', 950, 1100, 949, 1101), ('Adc_cali_b_Vdcbus', 23, 'int16', -32767, 32767, -32768, None), ('Adc_cali_a_Vbatt', 24, 'int16', 950, 1100, 949, 1101), ('Adc_cali_b_Vbatt', 25, 'int16', -32767, 32767, -32768, None), # ----- CAN / Comm ----- ('CAN_bit_rate_kbps', 100, 'uint16', 100, 1000, 99, 1001), ('CAN_protocol_select', 125, 'uint16', 0, 101, None, 102), # ('comm_protocol', 126, 'uint16', 0, 1, None, 2), # EXCLUDED: comm_protocol=1 disables Modbus → device becomes unreachable after rewrite ('CAN_node_ID', 138, 'uint16', 1, 127, 0, 128), # ----- BLE ----- ('BLE_Conn_mode', 142, 'uint16', 0, 2, None, 3), ('RSSI_auto_threshold', 105, 'int16', -120, 0, -121, 1), ('RSSI_max_voltage_Q128', 106, 'uint16', 0, 8320, None, 8321), # ----- Temperature thresholds — dev ----- ('Dev_temp_thres_stage_1', 42, 'int16', -40, 150, -41, 151), ('Dev_temp_thres_stage_2', 43, 'int16', -40, 150, -41, 151), ('Dev_temp_thres_stage_3', 44, 'int16', -40, 150, -41, 151), # ----- Temperature thresholds — coil ----- ('Coil_temp_thres_stage_1', 107, 'int16', -40, 150, -41, 151), ('Coil_temp_thres_stage_2', 108, 'int16', -40, 150, -41, 151), ('Coil_temp_thres_stage_3', 109, 'int16', -40, 150, -41, 151), # ----- Temperature thresholds — MCU ----- ('MCU_temp_thres_stage_1', 92, 'int16', -40, 150, -41, 151), ('MCU_temp_thres_stage_2', 93, 'int16', -40, 150, -41, 151), ('MCU_temp_thres_stage_3', 94, 'int16', -40, 150, -41, 151), # ----- Current derating (%) ----- ('Current_derating_1', 45, 'uint16', 0, 50, None, 51), ('Current_derating_2', 46, 'uint16', 0, 50, None, 51), # ----- Ramp control ----- ('Ramp_control_mA_per_sec', 111, 'uint16', 0, 50000, None, 50001), ('Ramp_slow_mA_per_sec', 136, 'uint16', 0, 50000, None, 50001), ('Ramp_current_dV', 137, 'uint16', 0, 1280, None, 1281), # ----- Discovery / height ----- ('Discovery_Timeout_sec', 128, 'uint16', 0, 120, None, 121), ('RPU_Height_mm', 98, 'uint16', 0, 1500, None, 1501), ] # --------------------------------------------------------------------------- # TPU register test list (fenghuang-dsp) # --------------------------------------------------------------------------- # Limits source: fenghuang-dsp app/app_modbus/app_modbus.h, global_variables.h, # sec_control.h, fan.h, app_inv.h # # float32: two consecutive holding registers (EndianConvert32Bits → BYTEORDER_BIG). # Written via FC16 (write multiple registers). # # Calibration gain (A): limits = [nominal × 0.9, nominal × 1.1] # Calibration offset (B) with positive nominal: limits = [nominal × 0.8, nominal × 1.2] # Calibration offset (B) with negative nominal: limits are inverted in firmware # (lo_limit = nominal×0.8, up_limit = nominal×1.2 where lo > up numerically). # min_val = lo_limit (expected clamp when src too negative), # max_val = up_limit (expected clamp when src too positive/less negative). # PFC_current_rms_cali_B excluded (nominal = 0.0). TPU_TESTS = [ # ----- PFC power (uint16) ----- ('Power_limit', 47, 'uint16', 0, 1900, None, 1901), # ----- Coil voltage max (uint16) ----- ('Coil_voltage_max', 78, 'uint16', 0, 1000, None, 1001), # ----- RPU setpoints (uint16) ----- ('RPU_current_set_Q128', 98, 'uint16', 0, 3840, None, 3841), ('RPU_voltage_set_Q128', 99, 'uint16', 0, 8320, None, 8321), # ----- Fan control (uint16 / int16) ----- ('Fan_mode', 69, 'uint16', 0, 255, None, 256), ('Fan_duty_set', 70, 'uint16', 0, 100, None, 101), ('Fan_temp_lo', 71, 'int16', -40, 80, -41, 81), ('Fan_temp_hi', 72, 'int16', -40, 80, -41, 81), # ----- Moving-away detection (uint16) ----- ('Moving_away_time_ms', 3, 'uint16', 50, 500, 49, 501), ('Move_away_iout_thres', 84, 'uint16', 0, 50, None, 51), # ----- OVP (uint16) ----- ('OVP_iinv_pk_threshold', 75, 'uint16', 0, 50, None, 51), ('OVP_phase_threshold', 76, 'uint16', 0, 500, None, 501), ('OVP_count_threshold', 77, 'uint16', 0, 10, None, 11), # ----- Phase command min (uint16) ----- ('Phase_cmd_min', 101, 'uint16', 0, 750, None, 751), # ----- No-batt coupling ramp (uint16) ----- ('No_batt_coupling_ramp_phase', 109, 'uint16', 0, 300, None, 301), # ----- BLE (uint16) ----- ('BLE_discon_timeout_ms', 2, 'uint16', 0, 60000, None, 60001), ('BLE_mode', 5, 'uint16', 0, 2, None, 3), # ----- Current derating (uint16, stored as integer cast from float) ----- ('Current_derating_1', 107, 'uint16', 0, 50, None, 51), ('Current_derating_2', 108, 'uint16', 0, 50, None, 51), # ----- INV temperature thresholds (int16) ----- ('INV_temp_thres_1', 60, 'int16', -40, 150, -41, 151), ('INV_temp_thres_2', 61, 'int16', -40, 150, -41, 151), ('INV_temp_thres_3', 62, 'int16', -40, 150, -41, 151), # ----- PFC temperature thresholds (int16) ----- ('PFC_temp_thres_1', 63, 'int16', -40, 150, -41, 151), ('PFC_temp_thres_2', 64, 'int16', -40, 150, -41, 151), ('PFC_temp_thres_3', 65, 'int16', -40, 150, -41, 151), # ----- Coil temperature thresholds (int16) ----- ('Coil_temp_thres_1', 66, 'int16', -40, 150, -41, 151), ('Coil_temp_thres_2', 67, 'int16', -40, 150, -41, 151), ('Coil_temp_thres_3', 68, 'int16', -40, 150, -41, 151), # ----- MCU temperature thresholds (int16) ----- ('MCU_temp_thres_1', 104, 'int16', -40, 150, -41, 151), ('MCU_temp_thres_2', 105, 'int16', -40, 150, -41, 151), ('MCU_temp_thres_3', 106, 'int16', -40, 150, -41, 151), # ----- Coupling estimation frequencies (float32) ----- # INV_FREQ_MIN = 60.0 Hz, INV_FREQ_MAX = 150.0 Hz (app_inv.h) ('Coupling_est_f_lo', 6, 'float32', 60.0, 150.0, 59.0, 151.0), ('Coupling_est_f_hi', 8, 'float32', 60.0, 150.0, 59.0, 151.0), ('Coupling_est_f3', 102, 'float32', 60.0, 150.0, 59.0, 151.0), ('Coupling_est_iinv_ref', 10, 'float32', 0.0, 40.0, -1.0, 41.0), # ----- INV frequency settings (float32) ----- ('INV_freq_set', 28, 'float32', 60.0, 150.0, 59.0, 151.0), ('INV_freq_min', 30, 'float32', 60.0, 150.0, 59.0, 151.0), ('INV_freq_max', 32, 'float32', 60.0, 150.0, 59.0, 151.0), ('INV_current_limit', 34, 'float32', 0.0, 40.0, -1.0, 41.0), # ----- PFC voltage / current (float32) ----- # MODBUS_LIMIT_PFC_DC_BUS_VOLTAGE_MAX_CEIL = 450.0, PFC_CURRENT_MAX_CEIL = 40.0 ('PFC_vdc_ref', 40, 'float32', 0.0, 450.0, -1.0, 451.0), ('PFC_voltage_limit', 42, 'float32', 0.0, 450.0, -1.0, 451.0), ('PFC_current_limit', 44, 'float32', 0.0, 40.0, -1.0, 41.0), # ----- Protection current / voltage limits (float32) ----- ('PFC_current_max', 48, 'float32', 0.0, 40.0, -1.0, 41.0), ('INV_current_max', 50, 'float32', 0.0, 40.0, -1.0, 41.0), ('INV_current_min', 170, 'float32', 0.0, 40.0, -1.0, 41.0), ('DC_bus_voltage_min', 52, 'float32', 0.0, 450.0, -1.0, 451.0), ('DC_bus_voltage_max', 54, 'float32', 0.0, 450.0, -1.0, 451.0), # MODBUS_LIMIT_AC_IN_VOLTAGE_MAX_CEIL = 400.0 ('AC_in_voltage_min', 56, 'float32', 0.0, 400.0, -1.0, 401.0), ('AC_in_voltage_max', 58, 'float32', 0.0, 400.0, -1.0, 401.0), # ----- No-batt coupling INV current (float32) ----- ('No_batt_coupling_inv_current', 168, 'float32', 0.0, 40.0, -1.0, 41.0), # ----- ADC calibration gain (A) registers (float32) ----- # Limits = [nominal × 0.9, nominal × 1.1] (compile-time constants in firmware) # Nominal values from global_variables.h: # ANA_PFC_CURRENT_GAIN = 0.022127795 # ANA_INV_CURRENT_GAIN = 0.024940704 # ANA_DC_BUS_VOLTAGE_GAIN = 0.118472142 # ANA_AC_IN_VOLTAGE_GAIN = 0.118678347 # ANA_COIL_VOLTAGE_GAIN = 0.191968026 # ANA_PFC_CURRENT_RMS_GAIN = 1.0 ('PFC_current_cali_A', 12, 'float32', 0.019915, 0.024341, 0.018809, 0.025447), ('INV_current_cali_A', 16, 'float32', 0.022447, 0.027435, 0.021200, 0.028683), ('DC_bus_voltage_cali_A', 20, 'float32', 0.106625, 0.130319, 0.100701, 0.136243), ('AC_in_voltage_cali_A', 24, 'float32', 0.106811, 0.130546, 0.100877, 0.136480), ('Coil_voltage_cali_A', 80, 'float32', 0.172771, 0.211165, 0.163173, 0.220763), ('PFC_current_rms_cali_A', 110, 'float32', 0.9, 1.1, 0.85, 1.15), # ----- ADC calibration offset (B) registers (float32) ----- # Limits = [nominal × 0.8, nominal × 1.2] # Nominal values from global_variables.h: # ANA_PFC_CURRENT_OFFSET = -0.469129801 (negative → inverted range in firmware) # ANA_INV_CURRENT_OFFSET = -51.38759228 (negative → inverted range in firmware) # ANA_DC_BUS_VOLTAGE_OFFSET = 0.391120149 (positive) # ANA_AC_IN_VOLTAGE_OFFSET = 0.639814597 (positive) # ANA_COIL_VOLTAGE_OFFSET = -7.280746292 (negative → inverted range in firmware) # ANA_PFC_CURRENT_RMS_OFFSET = 0.0 (excluded: trivial limits) # # For negative nominals: min_val = lo_limit = nominal×0.8 (less negative), # max_val = up_limit = nominal×1.2 (more negative). # Phase 1 writes a more-negative value (max_val) → expect clamp to max_val. ('PFC_current_cali_B', 14, 'float32', -0.375304, -0.562956, -0.65, -0.30), ('INV_current_cali_B', 18, 'float32', -41.11007, -61.66511, -70.0, -30.0), ('DC_bus_voltage_cali_B', 22, 'float32', 0.312896, 0.469344, 0.293, 0.490), ('AC_in_voltage_cali_B', 26, 'float32', 0.511852, 0.767778, 0.480, 0.800), ('Coil_voltage_cali_B', 82, 'float32', -5.824597, -8.736896, -10.0, -4.0), ] # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _is_float(data_type: str) -> bool: return data_type == 'float32' def read_reg(instr: minimalmodbus.Instrument, addr: int, data_type: str): if _is_float(data_type): return instr.read_float(addr, functioncode=3, number_of_registers=2, byteorder=minimalmodbus.BYTEORDER_BIG) signed = (data_type == 'int16') return instr.read_register(addr, number_of_decimals=0, functioncode=3, signed=signed) def write_reg(instr: minimalmodbus.Instrument, addr: int, value, data_type: str) -> None: if _is_float(data_type): instr.write_float(addr, float(value), number_of_registers=2, byteorder=minimalmodbus.BYTEORDER_BIG) return signed = (data_type == 'int16') instr.write_register(addr, int(value), number_of_decimals=0, functioncode=6, signed=signed) def _check_result(readback, expected, data_type: str) -> str: if readback == 'ERR': return 'ERR' if _is_float(data_type): if abs(expected) < 1e-9: ok = abs(readback - expected) < FLOAT_EPSILON else: ok = abs(readback - expected) / abs(expected) < FLOAT_EPSILON return 'PASS' if ok else 'FAIL' return 'PASS' if readback == expected else 'FAIL' def _wait_for_device(instr: minimalmodbus.Instrument, poll_addr: int) -> None: deadline = time.monotonic() + REWRITE_WAIT while time.monotonic() < deadline: time.sleep(0.5) try: instr.read_register(poll_addr, number_of_decimals=0, functioncode=3, signed=False) return except minimalmodbus.ModbusException: pass print(" (trigger: device did not respond within REWRITE_WAIT — may not have triggered)") def trigger_rewrite_rpu(instr: minimalmodbus.Instrument) -> None: """Write MAIN_FORCE_NORMAL_MODE password → modbus_to_config() + write_all_flash() + soft_reset().""" instr.write_register(ADDR_PASSWORD_LO, PASSWORD_LO, 0, functioncode=6) try: instr.write_register(ADDR_PASSWORD_HI, PASSWORD_HI, 0, functioncode=6) print(" (trigger: PASSWORD_HI ack received)") except minimalmodbus.NoResponseError: print(" (trigger: NoResponseError — device started flash write immediately)") _wait_for_device(instr, ADDR_PASSWORD_LO) def _wait_for_tpu_trigger_state(instr: minimalmodbus.Instrument) -> bool: """Poll input reg 61 (MODBUS_MAIN_STATE) until state allows write_flash trigger. Returns True if state reached, False if timed out. Required because firmware only processes write_flash in specific states. """ deadline = time.monotonic() + REWRITE_WAIT while time.monotonic() < deadline: try: state = instr.read_register(ADDR_TPU_STATE, number_of_decimals=0, functioncode=4, signed=False) if state in TPU_TRIGGER_ALLOWED_STATES: print(f" (trigger: TPU state=0x{state:02X} — ready to accept trigger)") return True print(f" (trigger: TPU state=0x{state:02X} — waiting for allowed state...)") except minimalmodbus.ModbusException: pass time.sleep(0.5) print(" (trigger: WARNING — TPU state never reached allowed state; trigger may be ignored)") return False def trigger_rewrite_tpu(instr: minimalmodbus.Instrument) -> None: """Write coil bit 49 (MODBUS_ALL_CFG_CMD_BIT) → modbus_to_config() + write_all_flash() + soft_reset().""" _wait_for_tpu_trigger_state(instr) try: instr.write_bit(TPU_BIT_WRITE_FLASH, 1, functioncode=5) print(" (trigger: coil bit write ack received)") except minimalmodbus.NoResponseError: print(" (trigger: NoResponseError — device started flash write immediately)") _wait_for_device(instr, ADDR_HARDWARE_REV_HI) def detect_device_type(instr: minimalmodbus.Instrument) -> str: rev_hi = instr.read_register(ADDR_HARDWARE_REV_HI, number_of_decimals=0, functioncode=3, signed=False) print(f" HARDWARE_REV_HI = 0x{rev_hi:04X} ({rev_hi})") if rev_hi == HW_REV_TPU: return 'TPU' elif rev_hi == HW_REV_RPU: return 'RPU' else: print(f" WARNING: unknown HW_REV_HI — defaulting to RPU") return 'RPU' # --------------------------------------------------------------------------- # Main test logic # --------------------------------------------------------------------------- def run_tests(port: str) -> tuple[str, list[dict]]: instr = minimalmodbus.Instrument(port=port, slaveaddress=SLAVE_ID, mode='rtu') instr.serial.baudrate = BAUD instr.serial.timeout = TIMEOUT print(f"Connected to {port} slave=0x{SLAVE_ID:02X} ({SLAVE_ID}) baud={BAUD}") # ------------------------------------------------------------------------- # Auto-detect device type # ------------------------------------------------------------------------- print("\n[Detection] Reading HARDWARE_REV_HI...") device_type = detect_device_type(instr) if device_type == 'TPU': TESTS = TPU_TESTS trigger_fn = trigger_rewrite_tpu time.sleep(5) else: TESTS = RPU_TESTS trigger_fn = trigger_rewrite_rpu time.sleep(5) print(f" Device type: {device_type} — {len(TESTS)} registers in test list") # ------------------------------------------------------------------------- # Phase 0: Save all defaults # ------------------------------------------------------------------------- print("\n[Phase 0] Saving default values...") defaults: dict[int, any] = {} for _, addr, data_type, *_ in TESTS: if addr not in defaults: try: defaults[addr] = read_reg(instr, addr, data_type) val = defaults[addr] print(f" addr {addr:3d}: {val:.6g}" if _is_float(data_type) else f" addr {addr:3d}: {val}") except minimalmodbus.ModbusException as e: print(f" addr {addr:3d}: ERROR — {e}") defaults[addr] = None # ------------------------------------------------------------------------- # Phase 1: Write ALL below-min values → single rewrite → read all back # ------------------------------------------------------------------------- print("\n[Phase 1] Writing all below-min values...") for (label, addr, data_type, min_val, max_val, below_input, above_input) in TESTS: if below_input is not None and defaults.get(addr) is not None: try: write_reg(instr, addr, below_input, data_type) fmt = f"{below_input:.6g}" if _is_float(data_type) else str(below_input) print(f" addr {addr:3d} ({label}): wrote {fmt}") except minimalmodbus.ModbusException as e: print(f" addr {addr:3d} ({label}): write ERROR — {e}") print("\n[Phase 1] Triggering rewrite...") trigger_fn(instr) time.sleep(5) print("[Phase 1] Reading back...") below_readbacks: dict[int, any] = {} for (_, addr, data_type, *_) in TESTS: if addr not in below_readbacks: try: below_readbacks[addr] = read_reg(instr, addr, data_type) except minimalmodbus.ModbusException: below_readbacks[addr] = 'ERR' # ------------------------------------------------------------------------- # Phase 2: Write ALL above-max values → single rewrite → read all back # ------------------------------------------------------------------------- print("\n[Phase 2] Writing all above-max values...") for (label, addr, data_type, min_val, max_val, below_input, above_input) in TESTS: if above_input is not None and defaults.get(addr) is not None: try: write_reg(instr, addr, above_input, data_type) fmt = f"{above_input:.6g}" if _is_float(data_type) else str(above_input) print(f" addr {addr:3d} ({label}): wrote {fmt}") except minimalmodbus.ModbusException as e: print(f" addr {addr:3d} ({label}): write ERROR — {e}") print("\n[Phase 2] Triggering rewrite...") trigger_fn(instr) time.sleep(5) print("[Phase 2] Reading back...") above_readbacks: dict[int, any] = {} for (_, addr, data_type, *_) in TESTS: if addr not in above_readbacks: try: above_readbacks[addr] = read_reg(instr, addr, data_type) except minimalmodbus.ModbusException: above_readbacks[addr] = 'ERR' # ------------------------------------------------------------------------- # Restore: write all defaults → single rewrite # ------------------------------------------------------------------------- print("\n[Restore] Writing all defaults...") for (_, addr, data_type, *_) in TESTS: default = defaults.get(addr) if default is not None: try: write_reg(instr, addr, default, data_type) except minimalmodbus.ModbusException as e: print(f" addr {addr:3d}: restore write ERROR — {e}") trigger_fn(instr) time.sleep(5) print("[Restore] Done.") # ------------------------------------------------------------------------- # Compile results # ------------------------------------------------------------------------- results = [] for (label, addr, data_type, min_val, max_val, below_input, above_input) in TESTS: default = defaults.get(addr) def _fmt(v): if isinstance(v, float): return f"{v:.6g}" return v row = { 'label': label, 'data_type': data_type, 'default': _fmt(default) if default is not None else 'ERR', 'min': _fmt(min_val), 'max': _fmt(max_val), 'below_input': 'N/A', 'below_readback': 'N/A', 'below_result': 'N/A', 'above_input': 'N/A', 'above_readback': 'N/A', 'above_result': 'N/A', } if below_input is not None and default is not None: rb = below_readbacks.get(addr, 'ERR') row['below_input'] = _fmt(below_input) row['below_readback'] = _fmt(rb) if rb != 'ERR' else 'ERR' row['below_result'] = _check_result(rb, min_val, data_type) if above_input is not None and default is not None: rb = above_readbacks.get(addr, 'ERR') row['above_input'] = _fmt(above_input) row['above_readback'] = _fmt(rb) if rb != 'ERR' else 'ERR' row['above_result'] = _check_result(rb, max_val, data_type) results.append(row) print(f"\n{'─'*60}") print("All tests complete.") return device_type, results # --------------------------------------------------------------------------- # CSV report # --------------------------------------------------------------------------- def write_csv(device_type: str, results: list[dict]) -> None: import datetime script_dir = os.path.dirname(os.path.abspath(__file__)) ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') prefix = device_type.lower() csv_path = os.path.join(script_dir, f'{prefix}_modbus_limiter_report_{ts}.csv') with open(csv_path, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) writer.writerow([ 'Register', 'Data type', 'Default', 'Min', 'Max', 'Below-min input', 'Readback (below-min)', 'Below-min result', 'Above-max input', 'Readback (above-max)', 'Above-max result', ]) for row in results: writer.writerow([ row['label'], row['data_type'], row['default'], row['min'], row['max'], row['below_input'], row['below_readback'], row['below_result'], row['above_input'], row['above_readback'], row['above_result'], ]) total = pass_count = fail_count = na_count = err_count = 0 for row in results: for key in ('below_result', 'above_result'): v = row[key] if v == 'N/A': na_count += 1 continue total += 1 if v == 'PASS': pass_count += 1 elif v == 'FAIL': fail_count += 1 else: err_count += 1 print(f"\nReport saved: {csv_path}") print(f"Results — PASS: {pass_count} FAIL: {fail_count} ERROR: {err_count} N/A: {na_count} (total tests: {total})") # --------------------------------------------------------------------------- if __name__ == '__main__': port_num = input("Enter COM port number (e.g. 73 for COM73): ").strip() port = f"COM{port_num}" print(f"Using port: {port}") device_type, results = run_tests(port) if results: write_csv(device_type, results)