@@ -245,7 +245,66 @@ def classify_addr(addr: str, addr_type: int) -> Tuple[str, Optional[str]]:
245245
246246
247247# =====================================================================
248- # Phase 1 — Discovery
248+ # Phase 1 — Discovery (BleakScanner fallback — no root required)
249+ # =====================================================================
250+ def discover_ibeacons_bleak (duration : float ,
251+ min_rssi : Optional [int ]) -> List [Dict [str , Any ]]:
252+ """
253+ BleakScanner-based iBeacon discovery. No raw socket / root required.
254+ Parses Apple manufacturer data (company ID 0x004C, type 0x02, len 0x15).
255+ addr_type is returned as 0 (public) or 1 (random) from BlueZ props.
256+ """
257+ seen : Dict [str , Dict [str , Any ]] = {}
258+
259+ async def _scan () -> None :
260+ scanner = BleakScanner ()
261+ await scanner .start ()
262+ await asyncio .sleep (duration )
263+ await scanner .stop ()
264+ for dev , adv in scanner .discovered_devices_and_advertisement_data .values ():
265+ rssi = getattr (adv , "rssi" , - 999 ) or - 999
266+ if min_rssi is not None and rssi < min_rssi :
267+ continue
268+ payload = adv .manufacturer_data .get (APPLE_COMPANY_ID , b"" )
269+ if len (payload ) < 23 :
270+ continue
271+ if payload [0 ] != IBEACON_TYPE or payload [1 ] != IBEACON_LENGTH :
272+ continue
273+ uuid = _uuid_str (payload [2 :18 ])
274+ major = struct .unpack (">H" , payload [18 :20 ])[0 ]
275+ minor = struct .unpack (">H" , payload [20 :22 ])[0 ]
276+ tx_power = struct .unpack ("<b" , payload [22 :23 ])[0 ]
277+
278+ try :
279+ props = dev .details if isinstance (dev .details , dict ) else {}
280+ props = props .get ("props" , props )
281+ addr_type_str = props .get ("AddressType" , "random" )
282+ addr_type = 0 if addr_type_str == "public" else 1
283+ except Exception :
284+ addr_type = 1
285+
286+ key = f"{ dev .address } |{ uuid } |{ major } |{ minor } "
287+ rec = seen .get (key )
288+ if rec is None :
289+ rec = {
290+ "addr" : dev .address .upper (), "addr_type" : addr_type ,
291+ "evt_types" : set (), "rssi_min" : rssi , "rssi_max" : rssi ,
292+ "count" : 0 , "uuid" : uuid , "major" : major ,
293+ "minor" : minor , "tx_power" : tx_power ,
294+ }
295+ seen [key ] = rec
296+ rec ["rssi_min" ] = min (rec ["rssi_min" ], rssi )
297+ rec ["rssi_max" ] = max (rec ["rssi_max" ], rssi )
298+ rec ["count" ] += 1
299+
300+ asyncio .run (_scan ())
301+ out = list (seen .values ())
302+ out .sort (key = lambda r : - r ["rssi_max" ])
303+ return out
304+
305+
306+ # =====================================================================
307+ # Phase 1 — Discovery (raw HCI — needs CAP_NET_RAW or root)
249308# =====================================================================
250309def discover_ibeacons (dev_id : int , duration : float ,
251310 min_rssi : Optional [int ]) -> List [Dict [str , Any ]]:
0 commit comments