630 lines
29 KiB
Python
630 lines
29 KiB
Python
"""
|
||
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 (<min_val) → expect clamp to min_val.
|
||
# Phase 2 writes a less-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)
|