init
This commit is contained in:
+57
@@ -0,0 +1,57 @@
|
|||||||
|
### Python
|
||||||
|
# Byte-compiled files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Type checkers
|
||||||
|
.mypy_cache/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
# Windows thumbnail cache files
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
[Dd]esktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# Report files
|
||||||
|
*.csv
|
||||||
|
*.xlsx
|
||||||
|
*.xls
|
||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
# Modbus Range Limiter Test
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Verify that `LimitModbusRange()` in the firmware works correctly: when a value outside the allowed range is written to a holding register, the value read back after a config rewrite must be clamped to the defined min or max.
|
||||||
|
|
||||||
|
The script supports both device types and auto-detects via holding register 163 (`HARDWARE_REV_HI`):
|
||||||
|
- `0x0021` → **TPU** (fenghuang-dsp)
|
||||||
|
- `0x0022` → **RPU** (garuda-dsp)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rewrite Trigger Mechanism
|
||||||
|
|
||||||
|
### RPU (garuda-dsp)
|
||||||
|
Write the `MAIN_FORCE_NORMAL_MODE` password to two consecutive holding registers:
|
||||||
|
- Addr 48 (`MODBUS_FORCE_PASSWORD_LO`) = `0xFEED`
|
||||||
|
- Addr 49 (`MODBUS_FORCE_PASSWORD_HI`) = `0xCEE5`
|
||||||
|
|
||||||
|
Firmware detects the password → calls `modbus_to_config()` (which includes `LimitModbusRange()`) → `write_all_flash()` → `soft_reset()`.
|
||||||
|
|
||||||
|
### TPU (fenghuang-dsp)
|
||||||
|
Write value 1 to holding coil bit 49 (`MODBUS_ALL_CFG_CMD_BIT`) via FC5.
|
||||||
|
|
||||||
|
In the main loop, `update_modbus_input()` detects `write_flash == 1` → calls `modbus_to_config()` (which includes `LimitModbusRange()`) → `write_all_flash()` → `soft_reset()`.
|
||||||
|
|
||||||
|
> **Note:** Both devices call `DINT` inside `write_all_flash()` before writing to flash, which means the device cannot send an ACK for the trigger write. `NoResponseError` on the trigger write is expected and indicates the rewrite was successfully initiated. The script polls until the device finishes booting (up to `REWRITE_WAIT` seconds).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Registers Under Test
|
||||||
|
|
||||||
|
All registers that appear inside `LimitModbusRange()` are tested, including:
|
||||||
|
- `uint16` / `int16` registers clamped by `LimitUpLoUint16()` / `LimitUpLoInt16()`
|
||||||
|
- `float32` registers clamped by `LimitUpLoFloat()` — stored as two consecutive holding registers via `EndianConvert32Bits` (big-endian word order). Read with FC3 / written with FC16.
|
||||||
|
|
||||||
|
For **TPU float32** calibration registers the allowed range is a fixed percentage of the nominal constant from `global_variables.h`:
|
||||||
|
- Gain (A): `[nominal × 0.9, nominal × 1.1]`
|
||||||
|
- Offset (B) with positive nominal: `[nominal × 0.8, nominal × 1.2]`
|
||||||
|
- Offset (B) with negative nominal: limits are computed the same way but the upper and lower bounds are numerically inverted (firmware stores them in the same order regardless of sign).
|
||||||
|
- `PFC_current_rms_cali_B` is excluded because its nominal is `0.0` (limits collapse to `[0, 0]`).
|
||||||
|
|
||||||
|
Float readback comparison uses a relative tolerance of `FLOAT_EPSILON = 1e-4` (0.01%) to account for IEEE 754 rounding.
|
||||||
|
|
||||||
|
Limit sources:
|
||||||
|
- **RPU**: `garuda-dsp/app/app_modbus/app_modbus.h`, `global_variables.h`
|
||||||
|
- **TPU**: `fenghuang-dsp/app/app_modbus/app_modbus.h`, `global_variables.h`, `sec_control.h`, `fan.h`, `app_inv.h`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Procedure (batch approach)
|
||||||
|
|
||||||
|
1. **Phase 0** — Read and save all current default values.
|
||||||
|
2. **Phase 1** — Write all *below-min* values to their registers → single trigger → read all back.
|
||||||
|
- PASS if readback == `min_val`
|
||||||
|
- FAIL if readback != `min_val`
|
||||||
|
3. **Phase 2** — Write all *above-max* values to their registers → single trigger → read all back.
|
||||||
|
- PASS if readback == `max_val`
|
||||||
|
- FAIL if readback != `max_val`
|
||||||
|
4. **Restore** — Write all default values back → trigger rewrite to restore flash to its original state.
|
||||||
|
|
||||||
|
The batch approach (write all, then trigger once) is used instead of triggering per register to minimise total reboot wait time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CSV Report
|
||||||
|
|
||||||
|
File is named `<device>_modbus_limiter_report_<timestamp>.csv` and saved in the same directory as the script.
|
||||||
|
|
||||||
|
| Column | Content |
|
||||||
|
|--------|---------|
|
||||||
|
| 1 | Register name |
|
||||||
|
| 2 | Data type (`uint16` / `int16` / `float32`) |
|
||||||
|
| 3 | Original default value |
|
||||||
|
| 4 | Allowed minimum value |
|
||||||
|
| 5 | Allowed maximum value |
|
||||||
|
| 6 | Value written below min |
|
||||||
|
| 7 | Readback after rewrite (below-min) |
|
||||||
|
| 8 | Result: `PASS` / `FAIL` / `ERR` / `N/A` |
|
||||||
|
| 9 | Value written above max |
|
||||||
|
| 10 | Readback after rewrite (above-max) |
|
||||||
|
| 11 | Result: `PASS` / `FAIL` / `ERR` / `N/A` |
|
||||||
|
|
||||||
|
`N/A` = not applicable (e.g. `uint16` with `min=0` cannot go below zero, or `int16` with `max=32767`).
|
||||||
|
`ERR` = Modbus read/write error.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Requirements
|
||||||
|
|
||||||
|
- Python 3.10+
|
||||||
|
- `minimalmodbus` (`pip install minimalmodbus`)
|
||||||
|
- RS-485 / USB-to-serial connection to the device (RPU or TPU)
|
||||||
|
- Slave ID: `0x10` (16), Baud: 9600, Mode: RTU
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_______________________________________________________________________________
|
||||||
|
|
||||||
|
# Kiểm tra Modbus Range Limiter (Tiếng Việt)
|
||||||
|
|
||||||
|
## Mục đích
|
||||||
|
|
||||||
|
Kiểm tra hàm `LimitModbusRange()` trong firmware có hoạt động đúng không: khi người dùng ghi giá trị vượt ngoài ngưỡng cho phép vào holding register, sau khi firmware thực hiện rewrite config, giá trị đọc lại phải bị kẹp (clamp) đúng về min hoặc max đã định nghĩa.
|
||||||
|
|
||||||
|
Script hỗ trợ cả hai loại thiết bị và tự động nhận dạng thông qua holding register 163 (`HARDWARE_REV_HI`):
|
||||||
|
- `0x0021` → **TPU** (fenghuang-dsp)
|
||||||
|
- `0x0022` → **RPU** (garuda-dsp)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cơ chế trigger rewrite theo từng thiết bị
|
||||||
|
|
||||||
|
### RPU (garuda-dsp)
|
||||||
|
Ghi password `MAIN_FORCE_NORMAL_MODE` vào 2 holding register liên tiếp:
|
||||||
|
- Addr 48 (`MODBUS_FORCE_PASSWORD_LO`) = `0xFEED`
|
||||||
|
- Addr 49 (`MODBUS_FORCE_PASSWORD_HI`) = `0xCEE5`
|
||||||
|
|
||||||
|
Firmware phát hiện password → gọi `modbus_to_config()` (bao gồm `LimitModbusRange()`) → `write_all_flash()` → `soft_reset()`.
|
||||||
|
|
||||||
|
### TPU (fenghuang-dsp)
|
||||||
|
Ghi giá trị 1 vào holding coil bit 49 (`MODBUS_ALL_CFG_CMD_BIT`) qua FC5.
|
||||||
|
|
||||||
|
Ở vòng lặp chính, `update_modbus_input()` phát hiện `write_flash == 1` → gọi `modbus_to_config()` (bao gồm `LimitModbusRange()`) → `write_all_flash()` → `soft_reset()`.
|
||||||
|
|
||||||
|
> **Lưu ý:** Cả hai thiết bị đều gọi `DINT` bên trong `write_all_flash()` trước khi ghi flash, khiến thiết bị không kịp gửi ACK cho lệnh trigger. `NoResponseError` là bình thường và có nghĩa là rewrite đã được kích hoạt thành công. Script sẽ polling cho đến khi thiết bị boot xong trở lại (tối đa `REWRITE_WAIT` giây).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Danh sách register được test
|
||||||
|
|
||||||
|
Tất cả register nằm trong `LimitModbusRange()` đều được test, bao gồm:
|
||||||
|
- `uint16` / `int16`: kẹp bởi `LimitUpLoUint16()` / `LimitUpLoInt16()`
|
||||||
|
- `float32`: kẹp bởi `LimitUpLoFloat()` — lưu dưới dạng 2 holding register liên tiếp thông qua `EndianConvert32Bits` (big-endian word order). Đọc bằng FC3, ghi bằng FC16.
|
||||||
|
|
||||||
|
Đối với **TPU float32** register hiệu chỉnh (calibration), giới hạn được tính từ hằng số nominal trong `global_variables.h`:
|
||||||
|
- Gain (A): `[nominal × 0.9, nominal × 1.1]`
|
||||||
|
- Offset (B) nominal dương: `[nominal × 0.8, nominal × 1.2]`
|
||||||
|
- Offset (B) nominal âm: giới hạn tính theo cùng công thức nhưng giới hạn trên và dưới bị đảo ngược về mặt số học (firmware lưu theo cùng thứ tự bất kể dấu).
|
||||||
|
- `PFC_current_rms_cali_B` bị loại trừ vì nominal = `0.0` (giới hạn thu về `[0, 0]`).
|
||||||
|
|
||||||
|
So sánh giá trị float dùng dung sai tương đối `FLOAT_EPSILON = 1e-4` (0.01%) để tính đến sai số làm tròn IEEE 754.
|
||||||
|
|
||||||
|
Nguồn giới hạn:
|
||||||
|
- **RPU**: `garuda-dsp/app/app_modbus/app_modbus.h`, `global_variables.h`
|
||||||
|
- **TPU**: `fenghuang-dsp/app/app_modbus/app_modbus.h`, `global_variables.h`, `sec_control.h`, `fan.h`, `app_inv.h`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quy trình test (batch)
|
||||||
|
|
||||||
|
1. **Phase 0** — Đọc và lưu tất cả giá trị default hiện tại.
|
||||||
|
2. **Phase 1** — Ghi tất cả giá trị *dưới ngưỡng min* vào register → trigger rewrite một lần duy nhất → đọc lại toàn bộ.
|
||||||
|
- PASS nếu giá trị đọc lại == `min_val`
|
||||||
|
- FAIL nếu giá trị đọc lại != `min_val`
|
||||||
|
3. **Phase 2** — Ghi tất cả giá trị *vượt ngưỡng max* vào register → trigger rewrite một lần duy nhất → đọc lại toàn bộ.
|
||||||
|
- PASS nếu giá trị đọc lại == `max_val`
|
||||||
|
- FAIL nếu giá trị đọc lại != `max_val`
|
||||||
|
4. **Restore** — Ghi lại tất cả giá trị default → trigger rewrite để khôi phục flash về trạng thái ban đầu.
|
||||||
|
|
||||||
|
Dùng cách batch (ghi tất cả rồi trigger một lần) thay vì từng register một để giảm thời gian chờ reboot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Báo cáo CSV
|
||||||
|
|
||||||
|
File được đặt tên `<device>_modbus_limiter_report_<timestamp>.csv` cùng thư mục với script.
|
||||||
|
|
||||||
|
| Cột | Nội dung |
|
||||||
|
|-----|----------|
|
||||||
|
| 1 | Tên register |
|
||||||
|
| 2 | Kiểu dữ liệu (`uint16` / `int16` / `float32`) |
|
||||||
|
| 3 | Giá trị default ban đầu |
|
||||||
|
| 4 | Giá trị min cho phép |
|
||||||
|
| 5 | Giá trị max cho phép |
|
||||||
|
| 6 | Giá trị dưới min được ghi vào |
|
||||||
|
| 7 | Giá trị đọc lại sau rewrite (below-min) |
|
||||||
|
| 8 | Kết quả: `PASS` / `FAIL` / `ERR` / `N/A` |
|
||||||
|
| 9 | Giá trị vượt max được ghi vào |
|
||||||
|
| 10 | Giá trị đọc lại sau rewrite (above-max) |
|
||||||
|
| 11 | Kết quả: `PASS` / `FAIL` / `ERR` / `N/A` |
|
||||||
|
|
||||||
|
`N/A` = không áp dụng (ví dụ: `uint16` với `min=0` không thể ghi giá trị âm, hoặc `int16` với `max=32767`).
|
||||||
|
`ERR` = lỗi đọc/ghi Modbus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Yêu cầu môi trường
|
||||||
|
|
||||||
|
- Python 3.10+
|
||||||
|
- `minimalmodbus` (`pip install minimalmodbus`)
|
||||||
|
- Kết nối RS-485 / USB-to-serial đến thiết bị (RPU hoặc TPU)
|
||||||
|
- Slave ID: `0x10` (16), Baud: 9600, Mode: RTU
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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, None, 1281),
|
||||||
|
('CV_current_limit', 7, 'int16', 0, 1280, None, 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, None, 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),
|
||||||
|
('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, None, 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, None, 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, None, 451.0),
|
||||||
|
('PFC_voltage_limit', 42, 'float32', 0.0, 450.0, None, 451.0),
|
||||||
|
('PFC_current_limit', 44, 'float32', 0.0, 40.0, None, 41.0),
|
||||||
|
|
||||||
|
# ----- Protection current / voltage limits (float32) -----
|
||||||
|
('PFC_current_max', 48, 'float32', 0.0, 40.0, None, 41.0),
|
||||||
|
('INV_current_max', 50, 'float32', 0.0, 40.0, None, 41.0),
|
||||||
|
('INV_current_min', 170, 'float32', 0.0, 40.0, None, 41.0),
|
||||||
|
('DC_bus_voltage_min', 52, 'float32', 0.0, 450.0, None, 451.0),
|
||||||
|
('DC_bus_voltage_max', 54, 'float32', 0.0, 450.0, None, 451.0),
|
||||||
|
# MODBUS_LIMIT_AC_IN_VOLTAGE_MAX_CEIL = 400.0
|
||||||
|
('AC_in_voltage_min', 56, 'float32', 0.0, 400.0, None, 401.0),
|
||||||
|
('AC_in_voltage_max', 58, 'float32', 0.0, 400.0, None, 401.0),
|
||||||
|
|
||||||
|
# ----- No-batt coupling INV current (float32) -----
|
||||||
|
('No_batt_coupling_inv_current', 168, 'float32', 0.0, 40.0, None, 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), functioncode=16, 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 trigger_rewrite_tpu(instr: minimalmodbus.Instrument) -> None:
|
||||||
|
"""Write coil bit 49 (MODBUS_ALL_CFG_CMD_BIT) → modbus_to_config() + write_all_flash() + soft_reset()."""
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
TESTS = RPU_TESTS
|
||||||
|
trigger_fn = trigger_rewrite_rpu
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
Reference in New Issue
Block a user