Add BLE layer: firmware, coordinator support, dashboard tab
This commit is contained in:
@@ -131,6 +131,49 @@ def _node_status(last_seen: str | None, threshold: int = ONLINE_SECS) -> str:
|
||||
|
||||
# ─── API data builders ────────────────────────────────────────────────────────
|
||||
|
||||
# ─── BLE classification ──────────────────────────────────────────────────────
|
||||
|
||||
BLE_COMPANIES: dict[int, str] = {
|
||||
0x004C: "Apple",
|
||||
0x0075: "Samsung",
|
||||
0x0006: "Microsoft",
|
||||
0x00E0: "Google",
|
||||
0x0059: "Nordic Semiconductor",
|
||||
0x0138: "Garmin",
|
||||
0x0499: "Ruuvi Innovations",
|
||||
0x0157: "Polar Electro",
|
||||
0x00BD: "Fitbit",
|
||||
0x0171: "Amazon",
|
||||
0x004F: "MediaTek",
|
||||
0x001D: "Qualcomm",
|
||||
}
|
||||
|
||||
# Apple manufacturer data: after the 2-byte company ID, byte 0 of mfr_data is the type.
|
||||
APPLE_TYPES: dict[str, str] = {
|
||||
"02": "iBeacon",
|
||||
"05": "AirDrop",
|
||||
"07": "HomeKit",
|
||||
"09": "AirPods",
|
||||
"0a": "AirPods Pro",
|
||||
"0b": "AirPods 2nd gen",
|
||||
"0c": "AirPods case / Watch",
|
||||
"0f": "AirPods 3rd gen",
|
||||
"12": "FindMy / AirTag",
|
||||
"13": "AirPods 4th gen",
|
||||
"15": "Proximity Pair",
|
||||
"1e": "AirPods Pro 2nd gen",
|
||||
}
|
||||
|
||||
|
||||
def ble_device_type(mfr_id: int | None, mfr_data: str | None) -> str:
|
||||
if mfr_id is None:
|
||||
return "Unknown"
|
||||
if mfr_id == 0x004C and mfr_data and len(mfr_data) >= 2:
|
||||
return APPLE_TYPES.get(mfr_data[:2].lower(), "Apple device")
|
||||
vendor = BLE_COMPANIES.get(mfr_id)
|
||||
return f"{vendor} device" if vendor else f"Unknown (0x{mfr_id:04X})"
|
||||
|
||||
|
||||
_nodes_cache: tuple[float, list] | None = None
|
||||
_networks_cache: tuple[float, list] | None = None
|
||||
_clients_cache: tuple[float, list] | None = None
|
||||
@@ -138,6 +181,7 @@ _cross_node_cache: tuple[float, dict] | None = None
|
||||
_presence_cache: tuple[float, dict] | None = None
|
||||
_alerts_cache: tuple[float, dict] | None = None
|
||||
_sessions_cache: tuple[float, list] | None = None
|
||||
_ble_cache: tuple[float, dict] | None = None
|
||||
_NODES_CACHE_TTL = 10 # seconds
|
||||
_NETWORKS_CACHE_TTL = 30 # seconds
|
||||
_CLIENTS_CACHE_TTL = 30 # seconds
|
||||
@@ -145,6 +189,7 @@ _CROSS_NODE_CACHE_TTL = 60 # seconds
|
||||
_PRESENCE_CACHE_TTL = 60 # seconds
|
||||
_ALERTS_CACHE_TTL = 60 # seconds
|
||||
_SESSIONS_CACHE_TTL = 60 # seconds
|
||||
_BLE_CACHE_TTL = 30 # seconds
|
||||
|
||||
def build_nodes() -> list[dict]:
|
||||
global _nodes_cache
|
||||
@@ -1190,6 +1235,12 @@ async def api_node_detail(node_id: str):
|
||||
return detail
|
||||
|
||||
|
||||
@app.get("/api/ble")
|
||||
async def api_ble():
|
||||
data = await asyncio.to_thread(build_ble)
|
||||
return data
|
||||
|
||||
|
||||
@app.get("/api/stream")
|
||||
async def api_stream():
|
||||
"""SSE stream — polls DB every 2 s, pushes new events to the browser."""
|
||||
@@ -1225,6 +1276,65 @@ async def api_stream():
|
||||
)
|
||||
|
||||
|
||||
def build_ble() -> dict:
|
||||
global _ble_cache
|
||||
now = datetime.datetime.utcnow().timestamp()
|
||||
if _ble_cache and (now - _ble_cache[0]) < _BLE_CACHE_TTL:
|
||||
return _ble_cache[1]
|
||||
|
||||
cutoff = _hot_cutoff()
|
||||
|
||||
devices = query("""
|
||||
SELECT
|
||||
mac, addr_type,
|
||||
MAX(name) AS name,
|
||||
mfr_id,
|
||||
MAX(mfr_data) AS mfr_data,
|
||||
MAX(rssi) AS best_rssi,
|
||||
COUNT(*) AS times_seen,
|
||||
MIN(received_at) AS first_seen,
|
||||
MAX(received_at) AS last_seen,
|
||||
COUNT(DISTINCT node_id) AS node_count
|
||||
FROM ble_events
|
||||
WHERE received_at >= ?
|
||||
GROUP BY mac
|
||||
ORDER BY times_seen DESC
|
||||
LIMIT 300
|
||||
""", (cutoff,))
|
||||
|
||||
for d in devices:
|
||||
d["type"] = ble_device_type(d["mfr_id"], d["mfr_data"])
|
||||
d["vendor"] = BLE_COMPANIES.get(d["mfr_id"], "Unknown") if d["mfr_id"] is not None else "Unknown"
|
||||
|
||||
feed = query("""
|
||||
SELECT received_at, node_id, mac, addr_type, name, rssi, mfr_id, mfr_data
|
||||
FROM ble_events
|
||||
WHERE received_at >= ?
|
||||
ORDER BY id DESC
|
||||
LIMIT 100
|
||||
""", (cutoff,))
|
||||
|
||||
for f in feed:
|
||||
f["type"] = ble_device_type(f["mfr_id"], f["mfr_data"])
|
||||
|
||||
# Manufacturer breakdown
|
||||
breakdown = query("""
|
||||
SELECT mfr_id, COUNT(DISTINCT mac) AS unique_devices
|
||||
FROM ble_events
|
||||
WHERE received_at >= ?
|
||||
GROUP BY mfr_id
|
||||
ORDER BY unique_devices DESC
|
||||
LIMIT 20
|
||||
""", (cutoff,))
|
||||
|
||||
for b in breakdown:
|
||||
b["vendor"] = BLE_COMPANIES.get(b["mfr_id"], "Unknown") if b["mfr_id"] is not None else "No mfr data"
|
||||
|
||||
result = {"devices": devices, "feed": feed, "breakdown": breakdown}
|
||||
_ble_cache = (now, result)
|
||||
return result
|
||||
|
||||
|
||||
# ─── Entry point ─────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -607,6 +607,33 @@ td.disassoc-type { color: var(--orange); }
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ── BLE ── */
|
||||
.ble-type-apple { color: #a8c8ff; }
|
||||
.ble-type-samsung { color: #7eb3ff; }
|
||||
.ble-type-other { color: var(--text-muted); }
|
||||
.ble-addr-public { color: var(--green); font-size: 10px; }
|
||||
.ble-addr-random { color: var(--text-muted); font-size: 10px; }
|
||||
|
||||
.ble-breakdown-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.ble-breakdown-chip {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.ble-breakdown-chip strong {
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* ── Scrollbar ── */
|
||||
::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg); }
|
||||
|
||||
@@ -1190,6 +1190,9 @@ function setView(view) {
|
||||
document.getElementById('tab-sessions').classList.toggle('active', view === 'sessions');
|
||||
document.getElementById('tab-analysis').classList.toggle('active', view === 'analysis');
|
||||
document.getElementById('tab-search').classList.toggle('active', view === 'search');
|
||||
document.getElementById('view-ble').classList.toggle('active', view === 'ble');
|
||||
document.getElementById('tab-ble').classList.toggle('active', view === 'ble');
|
||||
if (view !== 'ble' && _blePoll) { clearInterval(_blePoll); _blePoll = null; }
|
||||
}
|
||||
|
||||
function showFeed() {
|
||||
@@ -1280,6 +1283,7 @@ function startSSE() {
|
||||
else if (currentView === 'presence') await fetchPresence();
|
||||
else if (currentView === 'alerts') await fetchAlerts();
|
||||
else if (currentView === 'analysis') await showAnalysis();
|
||||
else if (currentView === 'ble') await fetchBle();
|
||||
|
||||
if (!_nodesFetching) {
|
||||
_nodesFetching = true;
|
||||
@@ -1306,6 +1310,96 @@ function startSSE() {
|
||||
};
|
||||
}
|
||||
|
||||
// ── BLE ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
let bleData = { devices: [], feed: [], breakdown: [] };
|
||||
let _blePoll = null;
|
||||
|
||||
function showBle() {
|
||||
setView('ble');
|
||||
fetchBle();
|
||||
if (_blePoll) clearInterval(_blePoll);
|
||||
_blePoll = setInterval(fetchBle, 5000);
|
||||
}
|
||||
|
||||
async function fetchBle() {
|
||||
try {
|
||||
bleData = await fetch('/api/ble').then(r => r.json());
|
||||
renderBle();
|
||||
} catch (e) { /* network hiccup */ }
|
||||
}
|
||||
|
||||
function bleTypeClass(type) {
|
||||
if (!type) return 'ble-type-other';
|
||||
if (type.startsWith('Apple') || type === 'iBeacon' || type.includes('AirPods') ||
|
||||
type.includes('FindMy') || type.includes('AirTag') || type.includes('HomeKit') ||
|
||||
type.includes('AirDrop') || type.includes('Proximity'))
|
||||
return 'ble-type-apple';
|
||||
if (type.startsWith('Samsung')) return 'ble-type-samsung';
|
||||
return 'ble-type-other';
|
||||
}
|
||||
|
||||
function renderBle() {
|
||||
// Breakdown chips
|
||||
const bd = bleData.breakdown || [];
|
||||
const bdEl = document.getElementById('ble-breakdown');
|
||||
document.getElementById('ble-breakdown-count').textContent = bd.length ? `${bd.length} manufacturers` : '';
|
||||
bdEl.innerHTML = bd.map(b => {
|
||||
const label = b.vendor && b.vendor !== 'Unknown' && b.vendor !== 'No mfr data'
|
||||
? b.vendor
|
||||
: b.mfr_id != null ? `0x${b.mfr_id.toString(16).padStart(4,'0').toUpperCase()}` : 'No mfr data';
|
||||
return `<div class="ble-breakdown-chip"><strong>${b.unique_devices}</strong>${label}</div>`;
|
||||
}).join('');
|
||||
|
||||
// Devices table
|
||||
const devs = bleData.devices || [];
|
||||
const devsEl = document.getElementById('ble-devices-body');
|
||||
const emptyEl = document.getElementById('ble-devices-empty');
|
||||
const tableEl = document.getElementById('ble-devices-table');
|
||||
document.getElementById('ble-devices-count').textContent = devs.length ? `${devs.length} unique` : '';
|
||||
if (!devs.length) {
|
||||
emptyEl.style.display = '';
|
||||
tableEl.style.display = 'none';
|
||||
} else {
|
||||
emptyEl.style.display = 'none';
|
||||
tableEl.style.display = '';
|
||||
devsEl.innerHTML = devs.map(d => {
|
||||
const typeClass = bleTypeClass(d.type);
|
||||
const addrClass = d.addr_type === 'public' ? 'ble-addr-public' : 'ble-addr-random';
|
||||
const name = d.name || '<span class="muted">—</span>';
|
||||
const vendor = d.vendor && d.vendor !== 'Unknown' ? d.vendor : '<span class="muted">—</span>';
|
||||
return `<tr>
|
||||
<td class="${typeClass}">${fmt(d.type)}</td>
|
||||
<td class="mono">${fmt(d.mac)}</td>
|
||||
<td>${name}</td>
|
||||
<td>${vendor}</td>
|
||||
<td class="${addrClass}">${d.addr_type || '—'}</td>
|
||||
<td class="${rssiClass(d.best_rssi)}">${fmt(d.best_rssi)} dBm</td>
|
||||
<td>${fmt(d.times_seen)}</td>
|
||||
<td>${fmt(d.node_count)}</td>
|
||||
<td class="muted">${shortTime(d.last_seen)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Feed
|
||||
const feed = bleData.feed || [];
|
||||
document.getElementById('ble-feed-count').textContent = feed.length ? `last ${feed.length}` : '';
|
||||
document.getElementById('ble-feed-body').innerHTML = feed.map(f => {
|
||||
const typeClass = bleTypeClass(f.type);
|
||||
const addrClass = f.addr_type === 'public' ? 'ble-addr-public' : 'ble-addr-random';
|
||||
return `<tr>
|
||||
<td class="muted" style="font-size:11px">${shortTime(f.received_at)}</td>
|
||||
<td class="muted">${fmt(f.node_id)}</td>
|
||||
<td class="${typeClass}">${fmt(f.type)}</td>
|
||||
<td class="mono">${fmt(f.mac)}</td>
|
||||
<td>${f.name || '<span class="muted">—</span>'}</td>
|
||||
<td class="${rssiClass(f.rssi)}">${fmt(f.rssi)} dBm</td>
|
||||
<td class="${addrClass}">${f.addr_type || '—'}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Boot ──────────────────────────────────────────────────────────────────────
|
||||
initNetworkSort();
|
||||
init().then(startSSE);
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<button class="tab-btn" id="tab-sessions" onclick="showSessions()">Sessions</button>
|
||||
<button class="tab-btn" id="tab-analysis" onclick="showAnalysis()">Analysis</button>
|
||||
<button class="tab-btn" id="tab-search" onclick="showSearch()">Search</button>
|
||||
<button class="tab-btn" id="tab-ble" onclick="showBle()">BLE</button>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
@@ -42,6 +43,7 @@
|
||||
<div class="stat-row"><span>nodes</span><span id="sum-nodes">—</span></div>
|
||||
<div class="stat-row"><span>online</span><span id="sum-online">—</span></div>
|
||||
<div class="stat-row"><span>events (session)</span><span id="sum-events">—</span></div>
|
||||
<div class="stat-row muted"><span>data window</span><span>30 days</span></div>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-label">Nodes</div>
|
||||
@@ -327,7 +329,7 @@
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>BSSID</th><th>SSID</th><th>Frames</th>
|
||||
<th>Unique Srcs</th><th>Nodes</th><th>Last Seen</th>
|
||||
<th>Dom. Reason</th><th>Unique Srcs</th><th>Nodes</th><th>Last Seen</th>
|
||||
</tr></thead>
|
||||
<tbody id="targets-tbody"></tbody>
|
||||
</table>
|
||||
@@ -344,7 +346,7 @@
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Dst MAC</th><th>Vendor</th><th>Frames</th>
|
||||
<th>Networks Used</th><th>Nodes</th><th>Last Seen</th>
|
||||
<th>Dom. Reason</th><th>Networks Used</th><th>Nodes</th><th>Last Seen</th>
|
||||
</tr></thead>
|
||||
<tbody id="attackers-tbody"></tbody>
|
||||
</table>
|
||||
@@ -426,6 +428,7 @@
|
||||
<th class="sortable" data-col="targets" data-arrow="">Targets</th>
|
||||
<th class="sortable" data-col="node_count" data-arrow="">Nodes</th>
|
||||
<th class="sortable" data-col="peak_rssi" data-arrow="">Peak RSSI</th>
|
||||
<th>Dominant Reason</th>
|
||||
<th class="sortable" data-col="severity" data-arrow="">Severity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -462,6 +465,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BLE view -->
|
||||
<div id="view-ble" class="view">
|
||||
|
||||
<!-- Manufacturer breakdown -->
|
||||
<div class="section-header">
|
||||
<span class="section-title">Manufacturers</span>
|
||||
<span class="section-count" id="ble-breakdown-count"></span>
|
||||
</div>
|
||||
<div id="ble-breakdown" class="ble-breakdown-row"></div>
|
||||
|
||||
<!-- Devices table -->
|
||||
<div class="section-header" style="margin-top:18px">
|
||||
<span class="section-title">Devices</span>
|
||||
<span class="section-count" id="ble-devices-count"></span>
|
||||
<span class="presence-sub">unique MACs — last 7 days</span>
|
||||
</div>
|
||||
<div class="empty" id="ble-devices-empty" style="display:none">No BLE data yet. Flash a node with BLE firmware first.</div>
|
||||
<table class="data-table" id="ble-devices-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th><th>MAC</th><th>Name</th><th>Vendor</th>
|
||||
<th>Addr</th><th>Best RSSI</th><th>Seen</th><th>Nodes</th><th>Last seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ble-devices-body"></tbody>
|
||||
</table>
|
||||
|
||||
<!-- Live feed -->
|
||||
<div class="section-header" style="margin-top:24px">
|
||||
<span class="section-title">Recent Advertisements</span>
|
||||
<span class="section-count" id="ble-feed-count"></span>
|
||||
</div>
|
||||
<table class="data-table" id="ble-feed-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th><th>Node</th><th>Type</th><th>MAC</th>
|
||||
<th>Name</th><th>RSSI</th><th>Addr</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ble-feed-body"></tbody>
|
||||
</table>
|
||||
|
||||
</div><!-- /#view-ble -->
|
||||
|
||||
</div><!-- /#main -->
|
||||
</div><!-- /#app -->
|
||||
|
||||
|
||||
@@ -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