Skip to content

Instantly share code, notes, and snippets.

@cardil
Created March 26, 2026 23:11
Show Gist options
  • Select an option

  • Save cardil/9610ed40c6bb3fb1595bb53e67423e9c to your computer and use it in GitHub Desktop.

Select an option

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
#!/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