Created
March 26, 2026 23:11
-
-
Save cardil/9610ed40c6bb3fb1595bb53e67423e9c to your computer and use it in GitHub Desktop.
NUT apc_modbus word-order bug proof — confirms big-endian vs little-endian register write issue
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Bug 4 — Test: Confirm word-order bug in apc_modbus OutletCommand writes. | |
| The NUT driver's _apc_modbus_from_uint64() stores 32-bit values in big-endian | |
| word order: [MSW, LSW]. But APC UPS expects little-endian: [LSW, MSW]. | |
| For command value 0x0102 (load.on + MOG): | |
| - NUT sends: [0x0000, 0x0102] → FAILS (value in wrong register) | |
| - Correct: [0x0102, 0x0000] → WORKS (value in register 1538) | |
| This test confirms the bug by trying both orderings. | |
| MUST stop nut-driver@apc first (it holds /dev/ttyS0). | |
| """ | |
| import sys | |
| import time | |
| try: | |
| from pymodbus.client import ModbusSerialClient | |
| except ImportError: | |
| from pymodbus.client.sync import ModbusSerialClient | |
| SERIAL_PORT = "/dev/ttyS0" | |
| BAUDRATE = 9600 | |
| SLAVE_ID = 1 | |
| REG_ADDR = 1538 | |
| # load.on + MOG = 0x0102 (safe — load already on) | |
| CMD_VALUE = 0x0102 | |
| def test_write(client, desc, regs): | |
| """Write two 16-bit registers to 1538.""" | |
| print(f"\n--- {desc} ---") | |
| print(f" Registers: [{hex(regs[0])}, {hex(regs[1])}]") | |
| print(f" Register 1538 = {hex(regs[0])}, Register 1539 = {hex(regs[1])}") | |
| result = client.write_registers(REG_ADDR, regs, slave=SLAVE_ID) | |
| if result.isError(): | |
| print(f" RESULT: ❌ FAIL — {result}") | |
| return False | |
| else: | |
| print(f" RESULT: ✅ SUCCESS") | |
| return True | |
| def main(): | |
| print("Bug 4 — Word order test for register 1538") | |
| client = ModbusSerialClient( | |
| port=SERIAL_PORT, baudrate=BAUDRATE, | |
| parity='N', stopbits=1, bytesize=8, timeout=3, | |
| ) | |
| if not client.connect(): | |
| print("ERROR: Cannot connect"); sys.exit(1) | |
| print("Connected") | |
| time.sleep(0.5) | |
| # Test A: Little-endian [LSW, MSW] — how pymodbus naturally does it (SHOULD WORK) | |
| r_le = test_write(client, "Little-endian [LSW, MSW] (correct for APC)", | |
| [CMD_VALUE, 0x0000]) | |
| time.sleep(1) | |
| # Test B: Big-endian [MSW, LSW] — how NUT driver sends it (SHOULD FAIL) | |
| r_be = test_write(client, "Big-endian [MSW, LSW] (how NUT sends it)", | |
| [0x0000, CMD_VALUE]) | |
| time.sleep(1) | |
| client.close() | |
| print("\n=== SUMMARY ===") | |
| print(f"Little-endian [LSW, MSW]: {'PASS ✅' if r_le else 'FAIL ❌'}") | |
| print(f"Big-endian [MSW, LSW]: {'PASS ✅' if r_be else 'FAIL ❌'}") | |
| if r_le and not r_be: | |
| print("\n🎯 CONFIRMED: NUT driver has a WORD ORDER BUG!") | |
| print(" _apc_modbus_from_uint64() stores [MSW, LSW] but APC expects [LSW, MSW]") | |
| print(" Fix: Reverse the loop in _apc_modbus_from_uint64() for Modbus writes") | |
| elif r_le and r_be: | |
| print("\n🤔 Both work — word order is not the issue") | |
| elif not r_le and not r_be: | |
| print("\n🔍 Neither works — different problem") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment