@@ -85,37 +85,71 @@ def create_connection(mac: str, device_type: str) -> WearableConnection:
8585 return OmiConnection (mac )
8686
8787
88- async def scan_for_device (config : dict ):
89- """Scan BLE and return the first matching known or auto-discovered device .
88+ async def scan_all_devices (config : dict ) -> list [ dict ] :
89+ """Scan BLE and return all matching known or auto-discovered devices .
9090
91- Returns a dict with keys: mac, name, type — or None .
91+ Returns a list of dicts with keys: mac, name, type, rssi .
9292 """
9393 known = {d ["mac" ]: d for d in config .get ("devices" , [])}
9494 auto_discover = config .get ("auto_discover" , True )
9595
9696 logger .info ("Scanning for wearable devices..." )
97- discovered = await BleakScanner .discover (timeout = 5.0 )
97+ discovered = await BleakScanner .discover (timeout = 5.0 , return_adv = True )
9898
99- # Check known devices first
100- for d in discovered :
99+ devices = []
100+ for d , adv in discovered . values () :
101101 if d .address in known :
102102 entry = known [d .address ]
103- logger .info ("Found known device: %s [%s]" , entry .get ("name" , d .name ), d .address )
104- return {
103+ devices .append ({
105104 "mac" : d .address ,
106- "name" : entry .get ("name" , d .name ),
105+ "name" : entry .get ("name" , d .name or "Unknown" ),
107106 "type" : entry .get ("type" , detect_device_type (d .name or "" )),
108- }
107+ "rssi" : adv .rssi ,
108+ })
109+ elif auto_discover and d .name :
110+ lower = d .name .casefold ()
111+ if "omi" in lower or "neo" in lower or "friend" in lower :
112+ devices .append ({
113+ "mac" : d .address ,
114+ "name" : d .name ,
115+ "type" : detect_device_type (d .name ),
116+ "rssi" : adv .rssi ,
117+ })
118+
119+ devices .sort (key = lambda x : x .get ("rssi" , - 999 ), reverse = True )
120+ return devices
121+
122+
123+ async def scan_for_device (config : dict ):
124+ """Scan BLE and return the first matching device, or None."""
125+ devices = await scan_all_devices (config )
126+ return devices [0 ] if devices else None
127+
109128
110- # Auto-discover any recognised OMI/Neo device
111- if auto_discover :
112- for d in discovered :
113- if d . name and ( "omi" in d . name . casefold () or "neo" in d . name . casefold () or "friend" in d . name . casefold ()):
114- dtype = detect_device_type ( d . name )
115- logger . info ( "Auto-discovered %s device: %s [%s]" , dtype , d . name , d . address )
116- return { " mac" : d . address , "name" : d . name , "type" : dtype }
129+ def prompt_device_selection ( devices : list [ dict ]) -> dict | None :
130+ """Show an interactive numbered list and let the user pick a device."""
131+ print ( f" \n Found { len ( devices ) } device(s): \n " )
132+ print ( f" { '#' :<4 } { 'Name' :<20 } { 'MAC' :<20 } { 'Type' :<8 } { 'RSSI' } " )
133+ print ( " " + "-" * 60 )
134+ for i , d in enumerate ( devices , 1 ):
135+ print ( f" { i :<4 } { d [ 'name' ]:<20 } { d [ ' mac' ]:<20 } { d [ 'type' ]:<8 } { d . get ( 'rssi' , '?' ) } " )
117136
118- return None
137+ print ()
138+ while True :
139+ try :
140+ choice = input ("Select device [1]: " ).strip ()
141+ if not choice :
142+ idx = 0
143+ else :
144+ idx = int (choice ) - 1
145+ if 0 <= idx < len (devices ):
146+ return devices [idx ]
147+ print (f" Please enter a number between 1 and { len (devices )} " )
148+ except ValueError :
149+ print (f" Please enter a number between 1 and { len (devices )} " )
150+ except (EOFError , KeyboardInterrupt ):
151+ print ()
152+ return None
119153
120154
121155async def connect_and_stream (device : dict , backend_enabled : bool = True ) -> None :
@@ -241,22 +275,38 @@ async def queue_to_stream():
241275 await backend_queue .put (None )
242276
243277
244- async def run () -> None :
278+ async def run (target_mac : str | None = None ) -> None :
245279 config = load_config ()
246280 scan_interval = config .get ("scan_interval" , 10 )
247281 backend_enabled = check_config ()
248282
249283 logger .info ("Local wearable client started — scanning for devices..." )
250284
251285 while True :
252- device = await scan_for_device (config )
286+ devices = await scan_all_devices (config )
287+
288+ device = None
289+ if target_mac :
290+ # --device flag: connect to specific MAC
291+ device = next ((d for d in devices if d ["mac" ].casefold () == target_mac .casefold ()), None )
292+ if not device :
293+ logger .debug ("Target device %s not found, retrying in %ds..." , target_mac , scan_interval )
294+ elif len (devices ) == 1 :
295+ device = devices [0 ]
296+ elif len (devices ) > 1 :
297+ device = prompt_device_selection (devices )
298+ if device is None :
299+ logger .info ("No device selected, exiting." )
300+ return
301+
253302 if device :
254303 logger .info ("Connecting to %s [%s] (type=%s)" , device ["name" ], device ["mac" ], device ["type" ])
255304 await connect_and_stream (device , backend_enabled = backend_enabled )
256305 logger .info ("Device disconnected, resuming scan..." )
257306 else :
258307 logger .debug ("No devices found, retrying in %ds..." , scan_interval )
259- await asyncio .sleep (scan_interval )
308+
309+ await asyncio .sleep (scan_interval )
260310
261311
262312async def scan_and_print () -> None :
@@ -311,7 +361,8 @@ def build_parser() -> argparse.ArgumentParser:
311361 sub = parser .add_subparsers (dest = "command" )
312362
313363 sub .add_parser ("menu" , help = "Launch menu bar app (default)" )
314- sub .add_parser ("run" , help = "Headless mode — scan, connect, and stream (for launchd)" )
364+ run_parser = sub .add_parser ("run" , help = "Headless mode — scan, connect, and stream (for launchd)" )
365+ run_parser .add_argument ("--device" , metavar = "MAC" , help = "Connect to a specific device by MAC address" )
315366 sub .add_parser ("scan" , help = "One-shot scan — print nearby devices and exit" )
316367 sub .add_parser ("install" , help = "Install macOS launchd agent (auto-start on login)" )
317368 sub .add_parser ("uninstall" , help = "Remove macOS launchd agent" )
@@ -327,7 +378,7 @@ def main() -> None:
327378 command = args .command or "menu" # Default to menu mode
328379
329380 if command == "run" :
330- asyncio .run (run ())
381+ asyncio .run (run (target_mac = getattr ( args , "device" , None ) ))
331382
332383 elif command == "menu" :
333384 from menu_app import run_menu_app
0 commit comments