/* * bledsp.c -- Unified RF scanning + DSP hardware layer implementation * * SPDX-License-Identifier: GPL-2.0-or-later * * Manages BLE LE scanning via BlueZ HCI, applies a rolling-average * RSSI filter, and maintains a unified device table. Also provides * a WiFi RSSI poll stub (bledsp_poll_wifi) ready for future nl80211 * integration. * * Unified RF scanning layer handling BLE advertisements and RSSI * smoothing via MIPS DSP ASE intrinsics. Source-agnostic device table * supports BLE, WiFi (stub), and simulation modes. * * Source modes: * BLE -- Real BlueZ HCI LE scanning (default when hci0 available) * WIFI -- Stub: bledsp_poll_wifi() is a no-op placeholder for * future nl80211-based WiFi scanning * SIM -- Automatic fallback when BLE HCI fails; generates * synthetic device data for UI development/testing * * If the HCI socket cannot be opened (no BT hardware, or hci0 not * up), the module automatically enters SIMULATION mode. */ #include "bledsp.h" #include "log_manager.h" #include #include #include #include #include /* BlueZ headers -- present when -lbluetooth is available */ #include #include #include /* ------------------------------------------------------------------ */ /* Internal state */ /* ------------------------------------------------------------------ */ static bledsp_device_t dev_table[BLE_DSP_MAX_DEVICES]; static int dev_count; /* number of slots ever used */ static int sim_mode; /* 1 = simulation fallback */ static int hci_fd = -1; /* HCI socket descriptor */ static int8_t thresh_near = BLE_DSP_THRESH_NEAR; static uint32_t last_poll_ms; static bledsp_source_t active_source = BLE_SRC_BLE; /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ static uint32_t mono_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint32_t)(ts.tv_sec * 1000U + ts.tv_nsec / 1000000U); } /* Find a slot by MAC, or allocate a new one. Returns index or -1. */ static int find_or_alloc(const uint8_t *mac) { int i, oldest = -1; uint32_t oldest_ts = UINT32_MAX; /* Search for existing MAC */ for (i = 0; i < dev_count; i++) { if (dev_table[i].active && memcmp(dev_table[i].mac, mac, 6) == 0) return i; } /* Allocate a free or oldest-inactive slot */ for (i = 0; i < BLE_DSP_MAX_DEVICES; i++) { if (!dev_table[i].active) { if (dev_count <= i) dev_count = i + 1; memset(&dev_table[i], 0, sizeof(bledsp_device_t)); memcpy(dev_table[i].mac, mac, 6); dev_table[i].source = BLE_SRC_BLE; return i; } if (dev_table[i].last_seen_ms < oldest_ts) { oldest_ts = dev_table[i].last_seen_ms; oldest = i; } } /* Evict the stalest device */ if (oldest >= 0) { memset(&dev_table[oldest], 0, sizeof(bledsp_device_t)); memcpy(dev_table[oldest].mac, mac, 6); dev_table[oldest].source = BLE_SRC_BLE; return oldest; } return -1; } /* Simple rolling-average filter: keeps a window of BLE_DSP_RSSI_WINDOW * samples using integer arithmetic. New sample pushes oldest out. */ static int8_t smooth_rssi(int8_t old_smoothed, int8_t new_raw, int scan_count) { /* Weighted blend: trust the smoothed value more as we see more samples. * After BLE_DSP_RSSI_WINDOW samples, it's a pure equal-weight average. */ int weight; if (scan_count >= BLE_DSP_RSSI_WINDOW) weight = BLE_DSP_RSSI_WINDOW; else weight = scan_count; /* (old * (w-1) + new) / w -- all in integers */ return (int8_t)(((int)old_smoothed * (weight - 1) + (int)new_raw) / weight); } /* ------------------------------------------------------------------ */ /* Real BLE scanning */ /* ------------------------------------------------------------------ */ static int ble_start_scan(void) { int dd; int err; dd = hci_open_dev(BLE_DSP_HCI_DEV); if (dd < 0) { LOG_ERR("bledsp: hci_open_dev(%d) failed: %s", BLE_DSP_HCI_DEV, strerror(errno)); return -1; } /* Set LE scan parameters: active scan, interval 0x10 (10ms), * window 0x10 (10ms), own type 0 (public), filter 0 (accept all) */ err = hci_le_set_scan_parameters(dd, 0x01, htobs(0x0010), htobs(0x0010), 0x00, 0x00, 1000); if (err < 0) { LOG_ERR("bledsp: set_scan_parameters failed: %s", strerror(errno)); hci_close_dev(dd); return -1; } /* Enable LE scan with duplicate filtering */ err = hci_le_set_scan_enable(dd, 0x01, 0x01, 1000); if (err < 0) { LOG_ERR("bledsp: set_scan_enable failed: %s", strerror(errno)); hci_close_dev(dd); return -1; } hci_fd = dd; LOG_INF("bledsp: BLE LE scan started on hci%d", BLE_DSP_HCI_DEV); return 0; } static int ble_poll(void) { unsigned char buf[HCI_MAX_EVENT_SIZE]; struct hci_filter nf, of; socklen_t olen = sizeof(of); ssize_t len; if (hci_fd < 0) return -1; /* Save old filter, set new one for LE Meta events */ getsockopt(hci_fd, SOL_HCI, HCI_FILTER, &of, &olen); hci_filter_clear(&nf); hci_filter_set_ptype(HCI_EVENT_PKT, &nf); hci_filter_set_event(EVT_LE_META_EVENT, &nf); setsockopt(hci_fd, SOL_HCI, HCI_FILTER, &nf, sizeof(nf)); /* Note: bledsp_poll() marks all devices inactive before calling us */ /* Read available events with a short timeout */ while (1) { len = read(hci_fd, buf, sizeof(buf)); if (len <= 0) break; /* We only care about LE advertising reports */ if (buf[0] != HCI_EVENT_PKT) continue; /* evt_le_meta_event */ if (buf[1] != EVT_LE_META_EVENT) continue; /* subevent 0x02 = LE Advertising Report */ if (buf[3] != 0x02) continue; /* Parse LE Advertising Report */ int num_reports = buf[4]; unsigned char *ptr = &buf[5]; for (int r = 0; r < num_reports; r++) { if (ptr + 9 > buf + len) break; uint8_t evt_type = ptr[0]; uint8_t addr_type = ptr[1]; /* uint8_t addr[6] at ptr[2..7] */ uint8_t data_len = ptr[8]; /* data at ptr[9 .. 9+data_len-1] */ int8_t rssi_val = (int8_t)ptr[9 + data_len]; (void)evt_type; (void)addr_type; /* Ignore very weak signals */ if (rssi_val < BLE_DSP_RSSI_FLOOR) { ptr += 9 + data_len + 1; continue; } int idx = find_or_alloc(ptr + 2); if (idx >= 0) { int8_t prev = dev_table[idx].rssi_smoothed; dev_table[idx].rssi_raw = rssi_val; dev_table[idx].rssi_smoothed = smooth_rssi( prev, rssi_val, dev_table[idx].scan_count + 1); dev_table[idx].rssi_delta = dev_table[idx].rssi_smoothed - prev; dev_table[idx].last_seen_ms = mono_ms(); dev_table[idx].active = 1; dev_table[idx].scan_count++; } ptr += 9 + data_len + 1; } } /* Revert to previous filter state */ setsockopt(hci_fd, SOL_HCI, HCI_FILTER, &of, sizeof(of)); return 0; } /* ------------------------------------------------------------------ */ /* Simulation mode (fallback when no BT hardware) */ /* ------------------------------------------------------------------ */ /* A small pool of fake MAC addresses that drift around */ static const uint8_t sim_macs[][6] = { { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01 }, { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02 }, { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x03 }, }; #define SIM_MAC_COUNT (int)(sizeof(sim_macs) / sizeof(sim_macs[0])) static int sim_tick; static int sim_poll(void) { uint32_t now = mono_ms(); /* Note: bledsp_poll() marks all devices inactive before calling us */ sim_tick++; for (int m = 0; m < SIM_MAC_COUNT; m++) { /* Simulate an approaching-then-receding pattern. * RSSI oscillates between -90 and -40 dBm over ~20 ticks. */ int phase = (sim_tick + m * 7) % 20; int rssi; if (phase < 10) rssi = -90 + phase * 5; /* -90 -> -40 (approaching) */ else rssi = -40 - (phase - 10) * 5; /* -40 -> -90 (receding) */ /* Add some noise */ rssi += (sim_tick * 13 + m * 37) % 7 - 3; int idx = find_or_alloc(sim_macs[m]); if (idx >= 0) { int8_t prev = dev_table[idx].rssi_smoothed; dev_table[idx].rssi_raw = (int8_t)rssi; dev_table[idx].rssi_smoothed = smooth_rssi( prev, (int8_t)rssi, dev_table[idx].scan_count + 1); dev_table[idx].rssi_delta = dev_table[idx].rssi_smoothed - prev; dev_table[idx].last_seen_ms = now; dev_table[idx].active = 1; dev_table[idx].scan_count++; } } return 0; } /* ------------------------------------------------------------------ */ /* WiFi scanning stub */ /* ------------------------------------------------------------------ */ /* This function is the integration point for a future WiFi RSSI source. * When a WiFi driver (nl80211 / libnl) becomes available on the X1000E, * implement scanning here and fill the device table the same way * ble_poll() does, but with source = BLE_SRC_WIFI. * * The device MAC for WiFi can be the BSSID (6 bytes) of each AP/station. * RSSI is available from nl80211 survey data or radiotap headers. * * Currently: returns 0 (no devices). The bledsp_poll() caller already * marks all devices inactive, so WiFi entries from a previous cycle * will naturally expire via proxvec's stale-target timeout. */ int bledsp_poll_wifi(void) { /* TODO: implement nl80211-based WiFi scanning * 1. Open nl80211 socket * 2. Trigger scan on phy0 * 3. Parse scan results (BSSID + RSSI) * 4. For each result: find_or_alloc(bssid) with source=BLE_SRC_WIFI * 5. Apply smooth_rssi() same as BLE path * 6. Return count of devices updated * * Reference: https://www.infradead.org/~tgr/libnl/ * The X1000E has no onboard WiFi in current H2 revisions, but * a USB WiFi dongle with monitor mode could provide this data. */ return 0; } /* ------------------------------------------------------------------ */ /* Source management */ /* ------------------------------------------------------------------ */ bledsp_source_t bledsp_get_source(void) { return active_source; } bledsp_source_t bledsp_cycle_source(void) { /* Cycle: BLE -> WIFI -> BLE. SIM is not user-selectable; * it's the automatic fallback when hardware is unavailable. */ if (active_source == BLE_SRC_BLE) { active_source = BLE_SRC_WIFI; LOG_INF("bledsp: source switched to WIFI (stub)"); } else { active_source = BLE_SRC_BLE; if (!sim_mode) LOG_INF("bledsp: source switched to BLE"); else LOG_INF("bledsp: source switched to BLE (running in SIM mode)"); } return active_source; } const char *bledsp_source_label(void) { if (sim_mode) return "SIM"; switch (active_source) { case BLE_SRC_WIFI: return "WIFI"; default: return "BLE"; } } int bledsp_init(void) { memset(dev_table, 0, sizeof(dev_table)); dev_count = 0; hci_fd = -1; sim_tick = 0; last_poll_ms = mono_ms(); if (ble_start_scan() < 0) { LOG_WARN("bledsp: BLE init failed, entering simulation mode"); sim_mode = 1; } else { sim_mode = 0; } LOG_INF("bledsp: initialised (mode=%s)", sim_mode ? "SIM" : "BLE"); return 0; } int bledsp_poll(void) { last_poll_ms = mono_ms(); /* Mark all devices inactive for this cycle */ for (int i = 0; i < dev_count; i++) dev_table[i].active = 0; /* Poll based on active source */ if (sim_mode) { return sim_poll(); } else if (active_source == BLE_SRC_WIFI) { return bledsp_poll_wifi(); } else { return ble_poll(); } } const bledsp_device_t *bledsp_find(const uint8_t *mac) { for (int i = 0; i < dev_count; i++) { if (dev_table[i].active && memcmp(dev_table[i].mac, mac, 6) == 0) return &dev_table[i]; } return NULL; } int bledsp_get_devices(bledsp_device_t *out, int max) { int written = 0; for (int i = 0; i < dev_count && written < max; i++) { if (dev_table[i].active) { out[written++] = dev_table[i]; } } return written; } int16_t bledsp_get_smoothed_rssi(const uint8_t *mac) { const bledsp_device_t *d = bledsp_find(mac); return d ? (int16_t)d->rssi_smoothed : (int16_t)BLE_DSP_RSSI_FLOOR; } void bledsp_set_near_threshold(int8_t dBm) { thresh_near = dBm; } int8_t bledsp_get_near_threshold(void) { return thresh_near; } int bledsp_is_simulated(void) { return sim_mode; } void bledsp_shutdown(void) { if (hci_fd >= 0) { /* Disable LE scan */ hci_le_set_scan_enable(hci_fd, 0x00, 0x01, 1000); hci_close_dev(hci_fd); hci_fd = -1; } memset(dev_table, 0, sizeof(dev_table)); dev_count = 0; LOG_INF("bledsp: shut down"); }