etc~ scripts and random things |
| git clone https://github.com/djnnvx/etc.git |
| log | files | refs | archive |
bluetoothinator.py (8661B)
1 !/usr/bin/env python3 2 """Connect to a Bluetooth device and get a shell to poke at it. 3 4 Commands: 5 sdp browse services 6 rfcomm <ch> [hex] open a channel, read, optionally send bytes first 7 l2cap <psm> try an L2CAP connect 8 scan classic inquiry 9 info bond and connection state 10 name remote name request 11 class [hex] show or set our class of device 12 myname [str] show or set our name 13 myaddr [addr] show or set our BD_ADDR 14 fresh take a new random BD_ADDR 15 id show our full identity 16 reset restore the adapter identity we started with 17 pair | unpair 18 raw <cmd...> run a bluetoothctl command 19 q 20 """ 21 import argparse, ctypes, errno, os, random, re, select, shlex, socket, struct, subprocess, sys, time 22 23 try: 24 import readline 25 except ImportError: # free-threaded builds ship without it 26 readline = None 27 28 AF_BLUETOOTH, RFCOMM, L2CAP = 31, 3, 0 29 HCI = "hci0" 30 CMDS = "sdp rfcomm l2cap scan info name class myname myaddr fresh id reset pair unpair raw q".split() 31 HISTFILE = os.path.expanduser("~/.bluetoothinator_history") 32 libc = ctypes.CDLL("libc.so.6", use_errno=True) 33 34 35 def sh(cmd, t=90): 36 try: 37 r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=t) 38 return re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", r.stdout + r.stderr).strip() 39 except subprocess.TimeoutExpired: 40 return "<timeout>" 41 42 43 def ident(): 44 cfg = sh(f"hciconfig -a {HCI}") 45 g = lambda r: (re.search(r, cfg) or [None, "?"])[1] 46 return g(r"BD Address: (\S+)"), g(r"Name: '(.*)'"), g(r"Class: (0x[0-9a-f]+)"), g(r"Device Class: (.+)") 47 48 49 def idline(): 50 return "{} '{}' {} {}".format(*ident()) 51 52 53 def hexeq(a, b): 54 try: 55 return int(a, 16) == int(b, 16) 56 except ValueError: 57 return False 58 59 60 def rname(bd): 61 return sh(f"hcitool -i {HCI} name {bd}", 15) or "no name response" 62 63 64 def up(): 65 for _ in range(10): 66 if "UP RUNNING" in sh(f"hciconfig {HCI}"): 67 return True 68 sh(f"hciconfig {HCI} up") 69 time.sleep(0.5) 70 return False 71 72 73 def set_addr(addr, cod): 74 """Must start from an adapter that is up. Cycling a down adapter drops the 75 pending address and the controller comes back on its factory one.""" 76 if not up(): 77 return "adapter will not come up, refusing to touch the address" 78 idx = re.sub(r"\D", "", HCI) or "0" 79 last = "" 80 for _ in range(3): 81 # ponytail: sleeps tuned against MediaTek, stretch them if a slower dongle races 82 last = sh(f"hciconfig {HCI} reset; sleep 3; hciconfig {HCI} down; sleep 1; " 83 f"btmgmt --index {idx} public-addr {addr}; hciconfig {HCI} up; sleep 2", 40) 84 if ident()[0].upper() == addr.upper(): 85 up() 86 if cod and cod != "?": 87 sh(f"hciconfig {HCI} class {cod}") 88 # bluetoothd re-init can silently put the factory address back. 89 time.sleep(3) 90 if ident()[0].upper() != addr.upper(): 91 continue 92 return f"{addr} ok" 93 up() 94 return f"failed to set {addr}, now {ident()[0]} ({last})" 95 96 97 def bdbytes(bd): 98 return bytes(int(x, 16) for x in reversed(bd.split(":"))) 99 100 101 def connect(sock, sa, t): 102 buf = ctypes.create_string_buffer(sa, len(sa)) 103 if libc.connect(sock.fileno(), buf, len(sa)) != 0: 104 e = ctypes.get_errno() 105 if e != errno.EINPROGRESS: 106 return errno.errorcode.get(e, str(e)) 107 if not select.select([], [sock], [], t)[1]: 108 return "TIMEOUT" 109 if (err := sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)): 110 return errno.errorcode.get(err, str(err)) 111 return "OPEN" 112 113 114 def dump(data): 115 for i in range(0, len(data), 16): 116 c = data[i:i + 16] 117 txt = "".join(chr(b) if 32 <= b < 127 else "." for b in c) 118 print(f" {i:04x} {c.hex(' '):<47} {txt}") 119 120 121 def rfcomm(bd, ch, send=b"", t=8.0): 122 s = socket.socket(AF_BLUETOOTH, socket.SOCK_STREAM, RFCOMM) 123 s.setblocking(False) 124 try: 125 r = connect(s, struct.pack("<H6sBx", AF_BLUETOOTH, bdbytes(bd), ch), t) 126 print(f" connect: {r}") 127 if r != "OPEN": 128 return 129 if send: 130 s.setblocking(True) 131 s.send(send) 132 s.setblocking(False) 133 print(f" sent {len(send)}B") 134 data, end = b"", time.time() + 4 135 while time.time() < end: 136 if not select.select([s], [], [], max(0, end - time.time()))[0]: 137 break 138 d = s.recv(4096) 139 if not d: 140 break 141 data += d 142 print(f" got {len(data)}B") 143 if data: 144 dump(data) 145 except OSError as e: 146 print(f" {e}") 147 finally: 148 s.close() 149 150 151 def l2cap(bd, psm, t=5.0): 152 s = socket.socket(AF_BLUETOOTH, socket.SOCK_SEQPACKET, L2CAP) 153 s.setblocking(False) 154 try: 155 print(f" connect: {connect(s, struct.pack('<HH6sHB', AF_BLUETOOTH, psm, bdbytes(bd), 0, 0), t)}") 156 finally: 157 s.close() 158 159 160 def history(): 161 if not readline: 162 return 163 try: 164 readline.read_history_file(HISTFILE) 165 except OSError: 166 pass 167 readline.set_completer(lambda t, i: ([c for c in CMDS if c.startswith(t)] + [None])[i]) 168 readline.parse_and_bind("tab: complete") 169 170 171 172 def main(): 173 global HCI 174 a = argparse.ArgumentParser() 175 a.add_argument("bdaddr", nargs="?") 176 a.add_argument("--class", dest="cod", default="0x240418") 177 a.add_argument("--name") 178 a.add_argument("--addr") 179 a.add_argument("--dev", default="hci0") 180 o = a.parse_args() 181 if not o.bdaddr: 182 a.error("bdaddr required") 183 HCI = o.dev 184 bd = o.bdaddr 185 orig = ident() 186 187 if o.addr: 188 print(set_addr(o.addr, o.cod)) 189 if o.name: 190 sh(f"bluetoothctl system-alias {shlex.quote(o.name)}") 191 sh(f"hciconfig {HCI} class {o.cod}") 192 time.sleep(1) 193 addr, name, got, dev = ident() 194 if not hexeq(got, o.cod): 195 print(f"warning: class is {got}, not {o.cod}") 196 197 print(__doc__) 198 print(f"us {addr} '{name}' {got} {dev}") 199 print(f"target {bd} {rname(bd)}\n") 200 history() 201 202 while True: 203 try: 204 line = input(f"{bd}> ").strip() 205 except (EOFError, KeyboardInterrupt): 206 break 207 if not line: 208 continue 209 cmd, *args = line.split() 210 211 if cmd in ("q", "quit", "exit"): 212 break 213 elif cmd == "sdp": 214 out = sh(f"sdptool -i {HCI} browse {bd}") 215 for m in re.finditer(r"Service Name: (.+)|Channel: (\d+)|PSM: (\d+)", out): 216 print(" " + m.group(0)) 217 elif cmd == "rfcomm" and args: 218 rfcomm(bd, int(args[0]), bytes.fromhex(args[1]) if len(args) > 1 else b"") 219 elif cmd == "l2cap" and args: 220 l2cap(bd, int(args[0], 0)) 221 elif cmd == "scan": 222 print(sh(f"hcitool -i {HCI} scan --flush", 40) or " nothing found") 223 elif cmd == "info": 224 print(sh(f"bluetoothctl info {bd}")) 225 elif cmd == "name": 226 print(f" {rname(bd)}") 227 elif cmd == "class": 228 if args: 229 sh(f"hciconfig {HCI} class {args[0]}") 230 print(" " + sh(f"hciconfig -a {HCI} | grep -E 'Class:|Device Class:'")) 231 elif cmd == "myname": 232 if args: 233 sh(f"bluetoothctl system-alias {shlex.quote(' '.join(args))}") 234 print(f" {ident()[1]}") 235 elif cmd == "myaddr": 236 if args: 237 print(" " + set_addr(args[0], ident()[2])) 238 print(f" {ident()[0]}") 239 elif cmd == "fresh": 240 new = "DE:AD:" + ":".join(f"{random.randrange(256):02X}" for _ in range(4)) 241 print(" " + set_addr(new, ident()[2])) 242 print(f" {ident()[0]}") 243 elif cmd == "id": 244 print(" " + idline()) 245 elif cmd == "reset": 246 if ident()[0] != orig[0]: 247 set_addr(orig[0], orig[2]) 248 sh("bluetoothctl reset-alias") 249 sh(f"hciconfig {HCI} class {orig[2]}") 250 time.sleep(1) 251 print(" " + idline()) 252 elif cmd == "pair": 253 print(sh(f"bluetoothctl --timeout 25 -- pair {bd}", 40)) 254 elif cmd == "unpair": 255 print(sh(f"bluetoothctl remove {bd}")) 256 elif cmd == "raw" and args: 257 print(sh(f"bluetoothctl {' '.join(args)}")) 258 else: 259 print(" ?") 260 261 if readline: 262 try: 263 readline.write_history_file(HISTFILE) 264 except OSError: 265 pass 266 267 268 if __name__ == "__main__": 269 sys.exit(main())