/ 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()