Last active
March 27, 2026 01:19
-
-
Save cardil/1551323387f39a5a311229a7793c870a to your computer and use it in GitHub Desktop.
NUT apc_modbus word-order bug proof — source bit hypothesis test (disproved)
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: Does setting SOURCE_RJ45_PORT bit fix OutletCommand writes? | |
| This script bypasses the NUT driver and sends a raw Modbus write to register | |
| 1538 (OutletCommand_BF) with the SOURCE_RJ45_PORT bit (bit 14) set. | |
| Command: load.on targeting MOG (Main Outlet Group) | |
| - Without source bit: 0x0102 = CMD_OUTPUT_ON | TARGET_MOG | |
| - With source bit: 0x4102 = CMD_OUTPUT_ON | TARGET_MOG | SOURCE_RJ45_PORT | |
| This is safe because load is already on — load.on is a no-op. | |
| MUST stop nut-driver@apc first (it holds /dev/ttyS0). | |
| """ | |
| import sys | |
| import time | |
| try: | |
| from pymodbus.client import ModbusSerialClient | |
| except ImportError: | |
| try: | |
| from pymodbus.client.sync import ModbusSerialClient | |
| except ImportError: | |
| print("ERROR: pymodbus not installed. Run: pip3 install pymodbus") | |
| sys.exit(1) | |
| SERIAL_PORT = "/dev/ttyS0" | |
| BAUDRATE = 9600 | |
| SLAVE_ID = 1 | |
| # Register 1538 = OutletCommand_BF (2 registers = 32 bits) | |
| REG_ADDR = 1538 | |
| REG_COUNT = 2 | |
| # Bit definitions from apc_modbus.h | |
| CMD_OUTPUT_ON = (1 << 1) # bit 1 | |
| TARGET_MOG = (1 << 8) # bit 8 — Main Outlet Group | |
| SOURCE_RJ45_PORT = (1 << 14) # bit 14 | |
| def test_write(client, description, value_32bit): | |
| """Write a 32-bit value to register 1538 as two 16-bit registers.""" | |
| reg_hi = (value_32bit >> 16) & 0xFFFF | |
| reg_lo = value_32bit & 0xFFFF | |
| print(f"\n--- {description} ---") | |
| print(f" Value: 0x{value_32bit:08X} (registers: [0x{reg_lo:04X}, 0x{reg_hi:04X}])") | |
| result = client.write_registers(REG_ADDR, [reg_lo, reg_hi], 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 — Testing SOURCE bit hypothesis on register 1538") | |
| print(f"Port: {SERIAL_PORT}, Baud: {BAUDRATE}, Slave: {SLAVE_ID}") | |
| client = ModbusSerialClient( | |
| port=SERIAL_PORT, | |
| baudrate=BAUDRATE, | |
| parity='N', | |
| stopbits=1, | |
| bytesize=8, | |
| timeout=3, | |
| ) | |
| if not client.connect(): | |
| print("ERROR: Cannot connect to serial port") | |
| sys.exit(1) | |
| print("Connected to serial port") | |
| time.sleep(0.5) # Let the connection settle | |
| # Test 1: load.on WITHOUT source bit (same as NUT driver — should FAIL) | |
| val_no_source = CMD_OUTPUT_ON | TARGET_MOG | |
| r1 = test_write(client, "load.on WITHOUT SOURCE bit (expect FAIL)", val_no_source) | |
| time.sleep(1) | |
| # Test 2: load.on WITH SOURCE_RJ45_PORT bit (hypothesis: should SUCCEED) | |
| val_with_source = CMD_OUTPUT_ON | TARGET_MOG | SOURCE_RJ45_PORT | |
| r2 = test_write(client, "load.on WITH SOURCE_RJ45_PORT bit (hypothesis: SUCCEED)", val_with_source) | |
| time.sleep(1) | |
| # Test 3: If RJ45 didn't work, try USB source bit | |
| if not r2: | |
| val_with_usb = CMD_OUTPUT_ON | TARGET_MOG | (1 << 12) # SOURCE_USB_PORT | |
| r3 = test_write(client, "load.on WITH SOURCE_USB_PORT bit (fallback)", val_with_usb) | |
| time.sleep(1) | |
| # Test 4: Try LOCAL_USER source bit | |
| if not r3: | |
| val_with_local = CMD_OUTPUT_ON | TARGET_MOG | (1 << 13) # SOURCE_LOCAL_USER | |
| test_write(client, "load.on WITH SOURCE_LOCAL_USER bit (fallback 2)", val_with_local) | |
| client.close() | |
| print("\n=== SUMMARY ===") | |
| print(f"Without SOURCE bit: {'PASS ✅' if r1 else 'FAIL ❌'}") | |
| print(f"With SOURCE_RJ45: {'PASS ✅' if r2 else 'FAIL ❌'}") | |
| if r2 and not r1: | |
| print("\n🎯 CONFIRMED: Missing SOURCE_RJ45_PORT bit is the root cause!") | |
| print(" Fix: Add SOURCE_RJ45_PORT to all OutletCommand writes in apc_modbus.c") | |
| elif r1: | |
| print("\n🤔 Both passed — issue may be NUT-specific (framing, timing, etc.)") | |
| else: | |
| print("\n🔍 Neither passed — source bit is NOT the fix; investigate further") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment