/ verify_g2_ble.py
verify_g2_ble.py
1 #!/usr/bin/env python3 2 """ 3 Verify that the Station G2 receives BLE enable/pair commands and responds. 4 Run with the RNode connected via USB; nothing else should use the serial port. 5 6 This script: 7 1. Reads and prints current WiFi/BT settings from EEPROM (to verify read-back works). 8 2. Sends WiFi off, waits 2 s (let WiFi shut down). 9 3. Sends BLE on (0x01), waits 2 s. 10 4. Sends BLE pairing (0x02). 11 5. Reads EEPROM again to verify WiFi=OFF and BT=ENABLED were written. 12 6. Listens for 45 s and prints any KISS frames (and the PIN if seen). 13 14 Usage: 15 python3 verify_g2_ble.py /dev/ttyACM0 16 """ 17 18 import sys 19 import time 20 21 try: 22 import serial 23 except ImportError: 24 print("Need pyserial: pip install pyserial") 25 sys.exit(1) 26 27 FEND = 0xC0 28 FESC = 0xDB 29 TFEND = 0xDC 30 TFESC = 0xDD 31 32 CMD_WIFI_MODE = 0x6A 33 CMD_BT_CTRL = 0x46 34 CMD_BT_PIN = 0x62 35 CMD_ROM_READ = 0x51 36 37 WR_WIFI_OFF = 0x00 38 BT_CTRL_ON = 0x01 39 BT_CTRL_PAIR = 0x02 40 41 EEPROM_RESERVED = 200 42 ADDR_CONF_BT = 0xB0 43 ADDR_CONF_WIFI = 0xBA 44 BT_ENABLE_BYTE = 0x73 45 WR_WIFI_STA = 0x01 46 WR_WIFI_AP = 0x02 47 48 49 def send_kiss(ser: serial.Serial, cmd: int, payload: bytes) -> None: 50 out = bytearray([FEND, cmd]) 51 for b in payload: 52 if b == FESC: 53 out.extend([FESC, TFESC]) 54 elif b == FEND: 55 out.extend([FESC, TFEND]) 56 else: 57 out.append(b) 58 out.append(FEND) 59 ser.write(bytes(out)) 60 time.sleep(0.05) 61 62 63 def unescape(data: bytearray) -> list[int]: 64 buf = [] 65 i = 0 66 while i < len(data): 67 if data[i] == FESC and i + 1 < len(data): 68 if data[i + 1] == TFEND: 69 buf.append(FEND) 70 elif data[i + 1] == TFESC: 71 buf.append(FESC) 72 else: 73 buf.append(data[i]) 74 i += 2 75 else: 76 buf.append(data[i]) 77 i += 1 78 return buf 79 80 81 def read_rom(ser: serial.Serial, timeout: float = 3.0) -> list[int] | None: 82 """Send CMD_ROM_READ, collect response frame, return 200-byte EEPROM or None. 83 Firmware only triggers the dump when it receives a payload byte after the command.""" 84 ser.write(bytes([FEND, CMD_ROM_READ, 0x00, FEND])) 85 time.sleep(0.15) 86 ser.timeout = 0.05 87 deadline = time.monotonic() + timeout 88 in_frame = False 89 escape = False 90 command = None 91 payload = bytearray() 92 93 while time.monotonic() < deadline: 94 raw = ser.read(512) 95 if not raw: 96 continue 97 for b in raw: 98 b = b & 0xFF 99 if b == FEND: 100 if in_frame and command == CMD_ROM_READ and len(payload) > 0: 101 dec = unescape(payload) 102 if len(dec) >= EEPROM_RESERVED: 103 return dec[:EEPROM_RESERVED] 104 in_frame = True 105 escape = False 106 command = None 107 payload.clear() 108 continue 109 if not in_frame: 110 continue 111 if command is None: 112 command = b 113 continue 114 if escape: 115 if b == TFEND: 116 payload.append(FEND) 117 elif b == TFESC: 118 payload.append(FESC) 119 else: 120 payload.append(b) 121 escape = False 122 elif b == FESC: 123 escape = True 124 else: 125 payload.append(b) 126 return None 127 128 129 def print_settings(rom: list[int], label: str = "Settings") -> None: 130 bt_byte = rom[ADDR_CONF_BT] 131 wifi_byte = rom[ADDR_CONF_WIFI] 132 bt_ok = bt_byte == BT_ENABLE_BYTE 133 if wifi_byte == WR_WIFI_OFF: 134 wifi_str = "OFF" 135 elif wifi_byte == WR_WIFI_STA: 136 wifi_str = "STA" 137 elif wifi_byte == WR_WIFI_AP: 138 wifi_str = "AP" 139 else: 140 wifi_str = f"0x{wifi_byte:02X}" 141 print(f" [{label}] WiFi= {wifi_str} (0xBA=0x{wifi_byte:02X}) | BT= {'ON' if bt_ok else 'off'} (0xB0=0x{bt_byte:02X})") 142 143 144 def main() -> None: 145 port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyACM0" 146 try: 147 ser = serial.Serial(port, 115200, timeout=0.1) 148 except serial.SerialException as e: 149 print(f"Failed to open {port}: {e}") 150 sys.exit(1) 151 152 print("Verify G2 BLE: send commands and listen for responses.") 153 print("Ensure nothing else (rnsd, rnodeconf) is using the port.\n") 154 time.sleep(0.5) 155 156 # 0. Read current settings (verify read-back works) 157 print("Reading current EEPROM settings...") 158 rom_before = read_rom(ser) 159 if rom_before is None: 160 print(" Failed to read ROM. Device may not be responding on this port.") 161 ser.close() 162 sys.exit(1) 163 print_settings(rom_before, "before") 164 print() 165 166 # 1. WiFi off 167 print("Sending: WiFi OFF") 168 send_kiss(ser, CMD_WIFI_MODE, bytes([WR_WIFI_OFF])) 169 time.sleep(2.0) 170 171 # 2. BLE on 172 print("Sending: BLE ON (0x01)") 173 send_kiss(ser, CMD_BT_CTRL, bytes([BT_CTRL_ON])) 174 time.sleep(2.0) 175 176 # 3. BLE pairing 177 print("Sending: BLE PAIR (0x02)") 178 send_kiss(ser, CMD_BT_CTRL, bytes([BT_CTRL_PAIR])) 179 time.sleep(0.3) 180 181 # 4. Read EEPROM again to verify writes 182 print("Reading EEPROM again to verify...") 183 rom_after = read_rom(ser) 184 if rom_after is not None: 185 print_settings(rom_after, "after ") 186 if rom_after[ADDR_CONF_WIFI] != WR_WIFI_OFF: 187 print(" WARNING: WiFi byte not 0x00 (OFF) - write may not have been applied.") 188 if rom_after[ADDR_CONF_BT] != BT_ENABLE_BYTE: 189 print(" WARNING: BT byte not 0x73 (enabled) - write may not have been applied.") 190 else: 191 print(" (ROM read failed after writes)") 192 print() 193 print("Listening for 45 s. Pair from Linux now (bluetoothctl -> pair <MAC>).") 194 print("Any KISS frame from the device will be printed below.") 195 print("If you see 'CMD_BT_PIN' and a 6-digit PIN, pairing path is working.\n") 196 197 deadline = time.monotonic() + 45.0 198 in_frame = False 199 escape = False 200 command = None 201 payload = bytearray() 202 203 while time.monotonic() < deadline: 204 raw = ser.read(256) 205 if not raw: 206 continue 207 for b in raw: 208 b = b & 0xFF 209 if b == FEND: 210 if in_frame and command is not None: 211 if command == CMD_BT_PIN and len(payload) >= 4: 212 dec = unescape(payload) 213 if len(dec) >= 4: 214 pin = (dec[0] << 24) | (dec[1] << 16) | (dec[2] << 8) | dec[3] 215 if 100000 <= pin <= 999999: 216 print(f">>> CMD_BT_PIN received: {pin:06d} <<<") 217 else: 218 # show raw frame for debugging 219 hex_payload = payload.hex().upper() 220 if len(hex_payload) > 60: 221 hex_payload = hex_payload[:60] + "..." 222 print(f" KISS frame: cmd=0x{command:02X} len={len(payload)} data={hex_payload}") 223 in_frame = True 224 escape = False 225 command = None 226 payload.clear() 227 continue 228 if not in_frame: 229 continue 230 if command is None: 231 command = b 232 continue 233 if escape: 234 if b == TFEND: 235 payload.append(FEND) 236 elif b == TFESC: 237 payload.append(FESC) 238 else: 239 payload.append(b) 240 escape = False 241 elif b == FESC: 242 escape = True 243 else: 244 payload.append(b) 245 246 ser.close() 247 print("\nDone. If you saw no KISS frames, the device may not be replying on this port.") 248 print("Check: correct port, firmware with BLE, display for 'Pairing' or PIN.") 249 250 251 if __name__ == "__main__": 252 main()