# 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 `_modbus_limiter_report_.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 `_modbus_limiter_report_.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