Add BLE layer: firmware, coordinator support, dashboard tab
This commit is contained in:
@@ -89,6 +89,26 @@ def init_db(path: str) -> sqlite3.Connection:
|
||||
rssi INTEGER
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS ble_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
received_at TEXT NOT NULL,
|
||||
node_id TEXT,
|
||||
node_ts INTEGER,
|
||||
mac TEXT,
|
||||
addr_type TEXT,
|
||||
name TEXT,
|
||||
rssi INTEGER,
|
||||
mfr_id INTEGER,
|
||||
mfr_data TEXT
|
||||
)
|
||||
""")
|
||||
# Safe migration: add ble_drops to heartbeat table if not present yet
|
||||
try:
|
||||
conn.execute("ALTER TABLE heartbeat_events ADD COLUMN ble_drops INTEGER")
|
||||
conn.commit()
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
# Indexes for common query patterns
|
||||
conn.executescript("""
|
||||
CREATE INDEX IF NOT EXISTS idx_beacon_received_at ON beacon_events (received_at);
|
||||
@@ -107,6 +127,10 @@ def init_db(path: str) -> sqlite3.Connection:
|
||||
CREATE INDEX IF NOT EXISTS idx_assoc_node_id ON assoc_events (node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_assoc_src ON assoc_events (src);
|
||||
CREATE INDEX IF NOT EXISTS idx_assoc_bssid ON assoc_events (bssid);
|
||||
CREATE INDEX IF NOT EXISTS idx_ble_received_at ON ble_events (received_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_ble_node_id ON ble_events (node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ble_mac ON ble_events (mac);
|
||||
CREATE INDEX IF NOT EXISTS idx_ble_mfr_id ON ble_events (mfr_id);
|
||||
""")
|
||||
conn.commit()
|
||||
return conn
|
||||
@@ -136,8 +160,8 @@ def store_heartbeat(conn: sqlite3.Connection, ev: dict, received_at: str):
|
||||
conn.execute("""
|
||||
INSERT INTO heartbeat_events
|
||||
(received_at, node_id, node_ts, uptime_ms, free_heap, wifi_rssi,
|
||||
probe_drops, deauth_drops, assoc_drops)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
probe_drops, deauth_drops, assoc_drops, ble_drops)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
received_at,
|
||||
ev.get("node_id"),
|
||||
@@ -148,6 +172,7 @@ def store_heartbeat(conn: sqlite3.Connection, ev: dict, received_at: str):
|
||||
ev.get("probe_drops"),
|
||||
ev.get("deauth_drops"),
|
||||
ev.get("assoc_drops"),
|
||||
ev.get("ble_drops"),
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
@@ -270,6 +295,37 @@ def validate_assoc(ev: dict) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def store_ble(conn: sqlite3.Connection, ev: dict, received_at: str):
|
||||
mfr_id = ev.get("mfr_id")
|
||||
conn.execute("""
|
||||
INSERT INTO ble_events
|
||||
(received_at, node_id, node_ts, mac, addr_type, name, rssi, mfr_id, mfr_data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
received_at,
|
||||
ev.get("node_id"),
|
||||
ev.get("ts"),
|
||||
ev.get("mac"),
|
||||
"public" if ev.get("addr_type") == 0 else "random",
|
||||
ev.get("name", ""),
|
||||
ev.get("rssi"),
|
||||
mfr_id if mfr_id is not None and mfr_id >= 0 else None,
|
||||
ev.get("mfr_data", ""),
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def validate_ble(ev: dict) -> str | None:
|
||||
for field in ("node_id", "mac", "rssi"):
|
||||
if ev.get(field) is None:
|
||||
return f"missing field '{field}'"
|
||||
if not _check_mac(ev["mac"]):
|
||||
return f"bad mac format: {ev['mac']}"
|
||||
if not _check_rssi(ev["rssi"]):
|
||||
return f"rssi out of range: {ev['rssi']}"
|
||||
return None
|
||||
|
||||
|
||||
def validate_probe(ev: dict) -> str | None:
|
||||
"""Returns an error string if invalid, else None."""
|
||||
for field in ("node_id", "src_mac", "rssi"):
|
||||
@@ -283,6 +339,8 @@ def validate_probe(ev: dict) -> str | None:
|
||||
|
||||
# ─── Packet handling ─────────────────────────────────────────────────────────
|
||||
|
||||
MAGENTA = "\033[95m"
|
||||
|
||||
IMP_COLOR = {
|
||||
"high": "\033[92m",
|
||||
"normal": "\033[0m",
|
||||
@@ -303,7 +361,21 @@ def handle_packet(data: bytes, addr: tuple, conn: sqlite3.Connection):
|
||||
received_at = datetime.datetime.utcnow().isoformat(timespec="seconds")
|
||||
pkt_type = ev.get("type", "beacon")
|
||||
|
||||
if pkt_type in ("assoc", "reassoc"):
|
||||
if pkt_type == "ble_adv":
|
||||
err = validate_ble(ev)
|
||||
if err:
|
||||
print(f"[DROP] {addr[0]} ble_adv — {err}")
|
||||
return
|
||||
store_ble(conn, ev, received_at)
|
||||
node = ev.get("node_id", "?")
|
||||
mac = ev.get("mac", "?")
|
||||
name = ev.get("name") or "<anon>"
|
||||
rssi = ev.get("rssi", 0)
|
||||
mfr_id = ev.get("mfr_id", -1)
|
||||
at = "pub" if ev.get("addr_type") == 0 else "rnd"
|
||||
mfr_str = f"mfr=0x{mfr_id:04X}" if mfr_id is not None and mfr_id >= 0 else "mfr=—"
|
||||
print(f"{MAGENTA}[{received_at}] {node} BLE {mac} ({at}) \"{name}\" {rssi:>4}dBm {mfr_str}{RESET}")
|
||||
elif pkt_type in ("assoc", "reassoc"):
|
||||
err = validate_assoc(ev)
|
||||
if err:
|
||||
print(f"[DROP] {addr[0]} {pkt_type} — {err}")
|
||||
|
||||
Reference in New Issue
Block a user