diff --git a/.github/workflows/esp32.yml b/.github/workflows/esp32.yml index 90d686a..58fc5c7 100644 --- a/.github/workflows/esp32.yml +++ b/.github/workflows/esp32.yml @@ -1,5 +1,5 @@ name: esp32 -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: build: name: Test compile examples for esp32 @@ -7,10 +7,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Checkout Arduino-List - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: repository: davidchatting/Arduino-List ref: master @@ -21,5 +21,6 @@ jobs: with: arduino-board-fqbn: esp32:esp32:esp32 platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + arduino-platform: esp32:esp32 extra-arduino-cli-args: "--warnings default" required-libraries: ArduinoJson,StreamUtils,PubSubClient,AceButton diff --git a/.github/workflows/esp8266.yml b/.github/workflows/esp8266.yml index a541ee5..e2d9ac6 100644 --- a/.github/workflows/esp8266.yml +++ b/.github/workflows/esp8266.yml @@ -1,5 +1,5 @@ name: esp8266 -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: build: name: Test compile examples for esp8266 @@ -7,10 +7,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Checkout Arduino-List - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: repository: davidchatting/Arduino-List ref: master diff --git a/.gitignore b/.gitignore index 7b8eeeb..5619053 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .DS_Store .vscode/ +.pio/ +test/ diff --git a/LICENSE.txt b/LICENSE.txt index 1c84f9f..0fcc140 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 David Chatting - github.com/davidchatting/Approximate +Copyright (c) 2020-2026 David Chatting - github.com/davidchatting/Approximate Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 11eb1cf..39557da 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,33 @@ The Approximate library is a WiFi [Arduino](http://www.arduino.cc/download) Libr Technically this library makes it easy to use WiFi signal strength ([RSSI](https://en.wikipedia.org/wiki/Received_signal_strength_indication)) to estimate the physical distance to a device on your home network, then obtain its [MAC address](https://en.wikipedia.org/wiki/MAC_address) and optionally its [IP address](https://en.wikipedia.org/wiki/IPv4). The network activity of these devices can also be observed. +## What's New in v2.0.0 (2026) + +This release includes significant improvements to device detection and project tooling: + +### Improved Packet Parsing +- **Management frame parsing**: The library now parses IEEE 802.11 management frames including probe requests, probe responses, beacons, authentication, association, and deauthentication frames. Probe requests are particularly valuable because they are sent by all WiFi devices scanning for networks - even those not connected to any network - providing RSSI-based proximity detection for a much wider range of devices. +- **Control frame parsing**: RTS (Request to Send), Block Ack Request, Block Ack, and PS-Poll control frames are now parsed to extract transmitter addresses and RSSI, enabling additional proximity observations. +- **New `PROBE` event type**: A new `Approximate::PROBE` event is generated when a device is detected via a management or control frame. This fires alongside the existing `ARRIVE`/`DEPART` lifecycle for proximate devices. +- **Complete IEEE 802.11 frame type definitions**: Added `wifi_ctrl_subtypes_t` (control frame subtypes), `wifi_data_subtypes_t` (data frame subtypes), and dedicated C structs for management frames (`wifi_80211_mgmt_frame`), RTS frames (`wifi_80211_ctrl_rts_frame`), ACK/CTS frames (`wifi_80211_ctrl_ack_frame`), Block Ack frames (`wifi_80211_ctrl_bar_frame`), and Information Elements (`wifi_80211_ie`). ToDS/FromDS direction constants (`DS_IBSS`, `DS_TO_AP`, `DS_FROM_AP`, `DS_WDS`) are now defined for clarity. + +### PlatformIO Support +- **`library.json`**: Added PlatformIO library manifest for registry compatibility. Add `lib_deps = https://github.com/davidchatting/Approximate.git` to your `platformio.ini`. +- **`platformio.ini`**: Included a PlatformIO project configuration with ESP32 and ESP8266 environments. Test examples with `pio ci`. +- **PlatformIO-compatible examples**: All example sketches now include forward declarations for callback functions, ensuring they compile with both the Arduino IDE and PlatformIO (which does not auto-generate prototypes from `.ino` files). +- **ListLib dependency**: Uses the [davidchatting/Arduino-List](https://github.com/davidchatting/Arduino-List) fork, which fixes a case-sensitivity issue (`arduino.h` vs `Arduino.h`) that prevented the upstream ListLib from compiling on Linux. + +### Maintenance +- **Version bump**: 1.4 to 2.0.0 (semver). +- **Copyright updated**: All source file headers updated to reflect 2020-2026. +- **GitHub Actions CI**: Updated from `actions/checkout@v2` to `actions/checkout@v4`. +- **Build tested**: All 9 examples compile cleanly on both ESP32 and ESP8266 via PlatformIO (`pio ci`). +- **New examples**: Added `ProbeDetect` (demonstrates the new `PROBE` event), `ProximityZones` (classifies devices by RSSI zone), and `DeviceFilter` (OUI-based filtering with `addActiveDeviceFilter`). + ## Installation +### Arduino IDE + The latest stable release of Approximate is available in the Arduino IDE Library Manager - search for "Approximate". Click install. Alternatively, Approximate can be installed manually. First locate and open the `libraries` directory used by the Arduino IDE, then clone this repository (https://github.com/davidchatting/Approximate) into that folder - this will create a new subfolder called `Approximate`. @@ -23,6 +48,39 @@ In addition, the following libraries are also required: * ListLib - https://github.com/luisllamasbinaburo/Arduino-List (install via the Arduino IDE Library Manager - searching for "ListLib") +### PlatformIO + +Add Approximate to your `platformio.ini`: + +```ini +[env:esp32] +platform = espressif32 +board = esp32dev +framework = arduino +lib_deps = + https://github.com/davidchatting/Approximate.git +``` + +Or for ESP8266: + +```ini +[env:esp8266] +platform = espressif8266 +board = esp12e +framework = arduino +lib_deps = + https://github.com/davidchatting/Approximate.git +``` + +The `Arduino-List` dependency will be resolved automatically via `library.json`. + +To test-compile the included examples from the library checkout: + +```bash +pio ci examples/CloseBy/CloseBy.ino --lib="." --board=esp32dev +pio ci examples/CloseBy/CloseBy.ino --lib="." --board=esp12e +``` + ## Limitations Approximate works with 2.4GHz WiFi networks, but not 5GHz networks - neither ESP8266 or ESP32 support this technology. This means that devices that are connected to a 5GHz WiFi network will be invisible to this library. Approximate will not work as expected where [MAC address randomisation](https://support.apple.com/en-gb/guide/security/secb9cb3140c/web) is enabled - the default iOS setting. @@ -90,12 +148,13 @@ void onProximateDevice(Device *device, Approximate::DeviceEvent event) { This example uses a Proximate Device Handler. The `onProximateDevice()` callback function receives both a pointer to a `Device` and a `DeviceEvent` for each new observation - in this example the events `ARRIVE` and `DEPART` cause the device's [MAC address](https://en.wikipedia.org/wiki/MAC_address) to be printed out and the state to be indicated via the LED. MAC addresses are the primary way in which the Approximate library identifies network devices. -There are four event types that a `DeviceHandler` will encounter: +There are five event types that a `DeviceHandler` will encounter: * `Approximate::ARRIVE` once when the device first arrives in proximity (only for Proximate Device Handlers) * `Approximate::DEPART` once when the device departs and is no longer seen in proximity (only for Proximate Device Handlers) * `Approximate::SEND` every time the device sends (uploads) data * `Approximate::RECEIVE` every time the device receives (downloads) data (rarely for Proximate Device Handlers, unless the router is also in proximity) +* `Approximate::PROBE` when a device is detected via a management frame (e.g. probe request) or control frame - this can detect devices that are not associated with any network The Proximate Device Handler is set by `setProximateDeviceHandler()`, which takes a `DeviceHandler` callback function parameter (here `onProximateDevice`) and a value for the `rssiThreshold` parameter that describes range considered to be in proximity (here `APPROXIMATE_PERSONAL_RSSI`). [RSSI](https://en.wikipedia.org/wiki/Received_signal_strength_indication) is a measure of WiFi signal strength used to estimate proximity. It is measured in [dBm](https://en.wikipedia.org/wiki/DBm) and at close proximity (where the reception is good) its value will approach zero, as the signal degrades over distance and through objects and walls, the value will fall. For instance, an RSSI of -50 would represent a relatively strong signal. The library predefines four values of `rssiThreshold` for use, that borrow from the language of [proxemics](https://en.wikipedia.org/wiki/Proxemics): @@ -141,7 +200,7 @@ void loop() { approx.loop(); digitalWrite(LED_PIN, ledState); - + if(ledToggleIntervalMs > 0 && millis() > ledToggleAtMs) { ledState = !ledState; ledToggleAtMs = millis() + ledToggleIntervalMs; @@ -149,7 +208,7 @@ void loop() { } void onActiveDevice(Device *device, Approximate::DeviceEvent event) { - if(event == Approximate::SEND) { + if(event == Approximate::SEND) { ledToggleIntervalMs = map(device->getRSSI(), -100, 0, 1000, 0); } } @@ -262,11 +321,11 @@ void onProximateDevice(Device *device, Approximate::DeviceEvent event) { String json = "{\"" + device->getMacAddressAsString() + "\":\"" + Approximate::toString(event) + "\"}"; Serial.println(json); - + approx.onceWifiStatus(WL_CONNECTED, [](String payload) { mqttClient.connect(WiFi.macAddress().c_str()); mqttClient.publish("closeby", payload.c_str(), false); //false = don't retain message - + #if defined(ESP8266) delay(20); approx.disconnectWiFi(); @@ -356,7 +415,7 @@ void onCloseBySonoff(Device *device, Approximate::DeviceEvent event) { } void onButtonEvent(AceButton* button, uint8_t eventType, uint8_t buttonState) { - if(closeBySonoff) { + if(closeBySonoff) { switch (eventType) { case AceButton::kEventPressed: switchCloseBySonoff(true); @@ -376,15 +435,15 @@ void switchCloseBySonoff(bool switchState) { String url = "http://" + closeBySonoff->getIPAddressAsString() + ":8081/zeroconf/switch"; http.begin(url); http.addHeader("Content-Type", "application/json"); - + String switchValue = switchState?"on":"off"; String httpRequestData = "{\"deviceid\": \"\",\"data\": {\"switch\": \"" + switchValue + "\"}}"; - + int httpResponseCode = http.POST(httpRequestData); Serial.printf("%s\t%s\t%i\n",url.c_str(), httpRequestData.c_str(), httpResponseCode); http.end(); } - + #if defined(ESP8266) delay(20); approx.disconnectWiFi(); @@ -399,6 +458,161 @@ This is a further extension to the CloseBy example and again retains the same st Significantly this example requires that not only a proximate device's MAC address be known, but also its local [IP address - IPv4](https://en.wikipedia.org/wiki/IPv4) be determined. In default operation IP addresses are not available, but can be simply enabled by setting an optional parameter on `Approximate::init()` to `true`. This will initiate an [ARP scan](https://en.wikipedia.org/wiki/Address_Resolution_Protocol) of the local network when `Approximate::begin()` is called. However, this will cause an additional delay of 76 seconds on an ESP8266 and 12 seconds on an ESP32 before the main program will operate. The ESP32 will periodically automatically refresh its ARP table, but the ESP8266 will not - meaning that an ESP8266 will be unable to determine the IP address of new devices appearing on the network. +### Probe Detect - detecting devices via management frames + +The [ProbeDetect example](examples/ProbeDetect) demonstrates the `PROBE` event introduced in v2.0. It detects nearby devices via WiFi management frames (probe requests, beacons) and control frames - these are sent by devices even when they are not connected to any network. This makes it possible to detect a wider range of devices than data frame observation alone. No IP address resolution is needed. + +``` +#include +Approximate approx; + +void setup() { + Serial.begin(9600); + + if (approx.init("MyHomeWiFi", "password", false)) { + approx.setProximateDeviceHandler(onProximateDevice, APPROXIMATE_PERSONAL_RSSI); + approx.begin(); + } +} + +void loop() { + approx.loop(); +} + +void onProximateDevice(Device *device, Approximate::DeviceEvent event) { + switch (event) { + case Approximate::ARRIVE: + Serial.printf("ARRIVE\t%s\tOUI: 0x%06X\tRSSI: %i\n", + device->getMacAddressAsString().c_str(), + device->getOUI(), + device->getRSSI()); + break; + case Approximate::DEPART: + Serial.printf("DEPART\t%s\n", + device->getMacAddressAsString().c_str()); + break; + case Approximate::PROBE: + Serial.printf("PROBE\t%s\tRSSI: %i\n", + device->getMacAddressAsString().c_str(), + device->getRSSI()); + break; + } +} +``` + +The `PROBE` event fires each time a management or control frame is received from a proximate device. Unlike `ARRIVE` (which fires once when a device enters proximity), `PROBE` fires repeatedly as frames are observed, providing ongoing RSSI updates. This is useful for tracking signal strength changes in real time. + +### Proximity Zones - classifying devices by distance + +The [ProximityZones example](examples/ProximityZones) classifies nearby devices into proximity zones based on their RSSI signal strength, using the four predefined threshold constants. It demonstrates `setProximateRSSIThreshold()` and `setProximateLastSeenTimeoutMs()`. + +``` +#include +Approximate approx; + +void setup() { + Serial.begin(9600); + + if (approx.init("MyHomeWiFi", "password", false)) { + Approximate::setProximateRSSIThreshold(APPROXIMATE_PUBLIC_RSSI); + Approximate::setProximateLastSeenTimeoutMs(5000); + + approx.setProximateDeviceHandler(onProximateDevice); + approx.begin(); + } +} + +void loop() { + approx.loop(); +} + +void onProximateDevice(Device *device, Approximate::DeviceEvent event) { + int rssi = device->getRSSI(); + + switch (event) { + case Approximate::ARRIVE: + Serial.printf("ARRIVE\t%s\t[%s]\tRSSI: %i\n", + device->getMacAddressAsString().c_str(), + getProximityZone(rssi), rssi); + break; + case Approximate::DEPART: + Serial.printf("DEPART\t%s\n", + device->getMacAddressAsString().c_str()); + break; + case Approximate::PROBE: + Serial.printf("PROBE\t%s\t[%s]\tRSSI: %i\n", + device->getMacAddressAsString().c_str(), + getProximityZone(rssi), rssi); + break; + } +} + +const char* getProximityZone(int rssi) { + if (rssi > APPROXIMATE_INTIMATE_RSSI) return "INTIMATE"; + if (rssi > APPROXIMATE_PERSONAL_RSSI) return "PERSONAL"; + if (rssi > APPROXIMATE_SOCIAL_RSSI) return "SOCIAL"; + return "PUBLIC"; +} +``` + +By setting `APPROXIMATE_PUBLIC_RSSI` as the threshold, this example detects devices at the maximum range and classifies them into zones: `INTIMATE` (< 0.5m), `PERSONAL` (0.5-1.5m), `SOCIAL` (1.5-3m), or `PUBLIC` (3-5m). The `setProximateRSSIThreshold()` and `setProximateLastSeenTimeoutMs()` methods can also be called at runtime to dynamically adjust detection sensitivity. + +### Device Filter - filtering by manufacturer OUI + +The [DeviceFilter example](examples/DeviceFilter) demonstrates `addActiveDeviceFilter()` with OUI (Organizationally Unique Identifier) codes to observe only devices from specific manufacturers. This is useful for monitoring a fleet of devices or detecting specific hardware on the network. + +``` +#include +Approximate approx; + +const int LED_PIN = 2; + +void setup() { + Serial.begin(9600); + pinMode(LED_PIN, OUTPUT); + + if (approx.init("MyHomeWiFi", "password")) { + // Filter for Espressif devices by OUI + approx.addActiveDeviceFilter(0xA4CF12); + approx.addActiveDeviceFilter(0x3C71BF); + approx.addActiveDeviceFilter(0xD8F15B); + + approx.setActiveDeviceHandler(onActiveDevice); + approx.begin(); + } +} + +void loop() { + approx.loop(); +} + +void onActiveDevice(Device *device, Approximate::DeviceEvent event) { + switch (event) { + case Approximate::SEND: + digitalWrite(LED_PIN, HIGH); + Serial.printf("SEND\t%s\tOUI: 0x%06X\tRSSI: %i\t%i bytes\n", + device->getMacAddressAsString().c_str(), + device->getOUI(), device->getRSSI(), + device->getPayloadSizeBytes()); + break; + case Approximate::RECEIVE: + digitalWrite(LED_PIN, LOW); + Serial.printf("RECV\t%s\tOUI: 0x%06X\tRSSI: %i\t%i bytes\n", + device->getMacAddressAsString().c_str(), + device->getOUI(), device->getRSSI(), + device->getPayloadSizeBytes()); + break; + case Approximate::PROBE: + Serial.printf("PROBE\t%s\tOUI: 0x%06X\tRSSI: %i\n", + device->getMacAddressAsString().c_str(), + device->getOUI(), device->getRSSI()); + break; + } +} +``` + +Unlike `setActiveDeviceFilter()` (which replaces any existing filter), `addActiveDeviceFilter()` builds a list of filters. Filters can be removed individually with `removeActiveDeviceFilter()` or cleared with `removeAllActiveDeviceFilters()`. OUI codes for device manufacturers can be found at http://standards-oui.ieee.org/oui.txt. + ## In Use Projects that use the Approximate library include: @@ -415,7 +629,8 @@ The Approximate library has learnt much from the work of others, including: * [ESP32 CSI Tool](https://github.com/StevenMHernandez/ESP32-CSI-Tool) * [ESP32 gather CSI](https://github.com/jonathanmuller/ESP32-gather-channel-state-information-CSI-) * [ESP32 CSI Phase Chart](https://github.com/diegonunesbr/ESP32-CSI-Phase-Chart) +* IEEE 802.11 frame type reference used for packet parsing improvements ## Author -The Approximate library was created by David Chatting ([@davidchatting](https://twitter.com/davidchatting)) as part of the [A Network of One's Own](http://davidchatting.com/nooo/) project. Collaboration welcome - please contribute by raising issues and making pull requests via GitHub. This code is licensed under the [MIT License](LICENSE.txt). \ No newline at end of file +The Approximate library was created by David Chatting ([@davidchatting](https://twitter.com/davidchatting)) as part of the [A Network of One's Own](http://davidchatting.com/nooo/) project. Collaboration welcome - please contribute by raising issues and making pull requests via GitHub. This code is licensed under the [MIT License](LICENSE.txt). diff --git a/examples/CloseBy/CloseBy.ino b/examples/CloseBy/CloseBy.ino index 3fff1ba..4c754ea 100644 --- a/examples/CloseBy/CloseBy.ino +++ b/examples/CloseBy/CloseBy.ino @@ -19,6 +19,8 @@ Approximate approx; const int LED_PIN = 2; #endif +void onProximateDevice(Device *device, Approximate::DeviceEvent event); + void setup() { Serial.begin(9600); pinMode(LED_PIN, OUTPUT); diff --git a/examples/CloseByMQTT/CloseByMQTT.ino b/examples/CloseByMQTT/CloseByMQTT.ino index d0a4ae5..4cf8ced 100644 --- a/examples/CloseByMQTT/CloseByMQTT.ino +++ b/examples/CloseByMQTT/CloseByMQTT.ino @@ -24,6 +24,8 @@ PubSubClient mqttClient(wifiClient); const int LED_PIN = 2; #endif +void onProximateDevice(Device *device, Approximate::DeviceEvent event); + void setup() { Serial.begin(9600); pinMode(LED_PIN, OUTPUT); diff --git a/examples/CloseBySonoff/CloseBySonoff.ino b/examples/CloseBySonoff/CloseBySonoff.ino index 9942e8e..69f8ab5 100644 --- a/examples/CloseBySonoff/CloseBySonoff.ino +++ b/examples/CloseBySonoff/CloseBySonoff.ino @@ -34,6 +34,11 @@ using namespace ace_button; Device *closeBySonoff = NULL; +void onProximateDevice(Device *device, Approximate::DeviceEvent event); +void onCloseBySonoff(Device *device, Approximate::DeviceEvent event); +void onButtonEvent(AceButton* button, uint8_t eventType, uint8_t buttonState); +void switchCloseBySonoff(bool switchState); + void setup() { Serial.begin(9600); diff --git a/examples/DeviceFilter/DeviceFilter.ino b/examples/DeviceFilter/DeviceFilter.ino new file mode 100644 index 0000000..fc6fbc0 --- /dev/null +++ b/examples/DeviceFilter/DeviceFilter.ino @@ -0,0 +1,77 @@ +/* + Device Filter example for the Approximate Library + - + Filter active devices by OUI (Organizationally Unique Identifier) to detect + specific manufacturer devices on the network. Demonstrates addActiveDeviceFilter + and removeActiveDeviceFilter with OUI-based filtering. + - + Common OUIs (see http://standards-oui.ieee.org/oui.txt): + 0xD8F15B Sonoff (Espressif) + 0xA4CF12 Espressif + 0x3C71BF Espressif + 0xDCA632 Apple + 0x98E743 Apple + - + David Chatting - github.com/davidchatting/Approximate + MIT License - Copyright (c) February 2021, Updated 2026 +*/ + +#include +Approximate approx; + +// Define for your board, not all have built-in LED: +#if defined(ESP8266) + const int LED_PIN = 14; +#elif defined(ESP32) + const int LED_PIN = 2; +#endif + +void onActiveDevice(Device *device, Approximate::DeviceEvent event); + +void setup() { + Serial.begin(9600); + pinMode(LED_PIN, OUTPUT); + + if (approx.init("MyHomeWiFi", "password")) { + // Filter for Espressif devices by OUI + approx.addActiveDeviceFilter(0xA4CF12); + approx.addActiveDeviceFilter(0x3C71BF); + approx.addActiveDeviceFilter(0xD8F15B); + + approx.setActiveDeviceHandler(onActiveDevice); + approx.begin(); + } +} + +void loop() { + approx.loop(); +} + +void onActiveDevice(Device *device, Approximate::DeviceEvent event) { + switch (event) { + case Approximate::SEND: + digitalWrite(LED_PIN, HIGH); + Serial.printf("SEND\t%s\tOUI: 0x%06X\tRSSI: %i\t%i bytes\n", + device->getMacAddressAsString().c_str(), + device->getOUI(), + device->getRSSI(), + device->getPayloadSizeBytes()); + break; + case Approximate::RECEIVE: + digitalWrite(LED_PIN, LOW); + Serial.printf("RECV\t%s\tOUI: 0x%06X\tRSSI: %i\t%i bytes\n", + device->getMacAddressAsString().c_str(), + device->getOUI(), + device->getRSSI(), + device->getPayloadSizeBytes()); + break; + case Approximate::PROBE: + Serial.printf("PROBE\t%s\tOUI: 0x%06X\tRSSI: %i\n", + device->getMacAddressAsString().c_str(), + device->getOUI(), + device->getRSSI()); + break; + default: + break; + } +} diff --git a/examples/FindMy/FindMy.ino b/examples/FindMy/FindMy.ino index 72c4303..fd547e6 100644 --- a/examples/FindMy/FindMy.ino +++ b/examples/FindMy/FindMy.ino @@ -22,6 +22,8 @@ bool ledState = LOW; long ledToggleAtMs = 0; int ledToggleIntervalMs = 0; +void onActiveDevice(Device *device, Approximate::DeviceEvent event); + void setup() { Serial.begin(9600); pinMode(LED_PIN, OUTPUT); diff --git a/examples/MonitorCSI/MonitorCSI.ino b/examples/MonitorCSI/MonitorCSI.ino index f4adafe..42b5545 100644 --- a/examples/MonitorCSI/MonitorCSI.ino +++ b/examples/MonitorCSI/MonitorCSI.ino @@ -8,6 +8,8 @@ #include Approximate approx; +void onChannelStateEvent(Channel *channel); + void setup() { Serial.begin(9600); diff --git a/examples/ProbeDetect/ProbeDetect.ino b/examples/ProbeDetect/ProbeDetect.ino new file mode 100644 index 0000000..d88f4d4 --- /dev/null +++ b/examples/ProbeDetect/ProbeDetect.ino @@ -0,0 +1,49 @@ +/* + Probe Detect example for the Approximate Library + - + Detect nearby devices via WiFi management frames (probe requests, beacons) + without requiring a WiFi connection or IP address resolution. + Demonstrates the PROBE event introduced in v2.0. + - + David Chatting - github.com/davidchatting/Approximate + MIT License - Copyright (c) February 2021, Updated 2026 +*/ + +#include +Approximate approx; + +void onProximateDevice(Device *device, Approximate::DeviceEvent event); + +void setup() { + Serial.begin(9600); + + // init with no IP resolution (false) - PROBE events don't need it + if (approx.init("MyHomeWiFi", "password", false)) { + approx.setProximateDeviceHandler(onProximateDevice, APPROXIMATE_PERSONAL_RSSI); + approx.begin(); + } +} + +void loop() { + approx.loop(); +} + +void onProximateDevice(Device *device, Approximate::DeviceEvent event) { + switch (event) { + case Approximate::ARRIVE: + Serial.printf("ARRIVE\t%s\tOUI: 0x%06X\tRSSI: %i\n", + device->getMacAddressAsString().c_str(), + device->getOUI(), + device->getRSSI()); + break; + case Approximate::DEPART: + Serial.printf("DEPART\t%s\n", + device->getMacAddressAsString().c_str()); + break; + case Approximate::PROBE: + Serial.printf("PROBE\t%s\tRSSI: %i\n", + device->getMacAddressAsString().c_str(), + device->getRSSI()); + break; + } +} diff --git a/examples/ProximityZones/ProximityZones.ino b/examples/ProximityZones/ProximityZones.ino new file mode 100644 index 0000000..beeee91 --- /dev/null +++ b/examples/ProximityZones/ProximityZones.ino @@ -0,0 +1,71 @@ +/* + Proximity Zones example for the Approximate Library + - + Classify nearby devices into proximity zones based on RSSI signal strength: + INTIMATE (< 0.5m) RSSI > -20 + PERSONAL (0.5-1.5m) RSSI > -40 + SOCIAL (1.5-3m) RSSI > -60 + PUBLIC (3-5m) RSSI > -80 + - + Demonstrates setProximateRSSIThreshold, setProximateLastSeenTimeoutMs, + and the RSSI threshold constants. + - + David Chatting - github.com/davidchatting/Approximate + MIT License - Copyright (c) February 2021, Updated 2026 +*/ + +#include +Approximate approx; + +void onProximateDevice(Device *device, Approximate::DeviceEvent event); +const char* getProximityZone(int rssi); + +void setup() { + Serial.begin(9600); + + if (approx.init("MyHomeWiFi", "password", false)) { + // Use PUBLIC threshold to detect at maximum range + Approximate::setProximateRSSIThreshold(APPROXIMATE_PUBLIC_RSSI); + // Devices depart after 5 seconds without a packet + Approximate::setProximateLastSeenTimeoutMs(5000); + + approx.setProximateDeviceHandler(onProximateDevice); + approx.begin(); + } +} + +void loop() { + approx.loop(); +} + +void onProximateDevice(Device *device, Approximate::DeviceEvent event) { + int rssi = device->getRSSI(); + + switch (event) { + case Approximate::ARRIVE: + Serial.printf("ARRIVE\t%s\t[%s]\tRSSI: %i\n", + device->getMacAddressAsString().c_str(), + getProximityZone(rssi), + rssi); + break; + case Approximate::DEPART: + Serial.printf("DEPART\t%s\n", + device->getMacAddressAsString().c_str()); + break; + case Approximate::PROBE: + Serial.printf("PROBE\t%s\t[%s]\tRSSI: %i\n", + device->getMacAddressAsString().c_str(), + getProximityZone(rssi), + rssi); + break; + default: + break; + } +} + +const char* getProximityZone(int rssi) { + if (rssi > APPROXIMATE_INTIMATE_RSSI) return "INTIMATE"; + if (rssi > APPROXIMATE_PERSONAL_RSSI) return "PERSONAL"; + if (rssi > APPROXIMATE_SOCIAL_RSSI) return "SOCIAL"; + return "PUBLIC"; +} diff --git a/examples/WatchDevice/WatchDevice.ino b/examples/WatchDevice/WatchDevice.ino index 1808faa..3ca00c9 100644 --- a/examples/WatchDevice/WatchDevice.ino +++ b/examples/WatchDevice/WatchDevice.ino @@ -21,6 +21,9 @@ Approximate approx; long ledOnUntilMs = 0; +void onProximateDevice(Device *device, Approximate::DeviceEvent event); +void onActiveDevice(Device *device, Approximate::DeviceEvent event); + void setup() { Serial.begin(9600); pinMode(LED_PIN, OUTPUT); diff --git a/keywords.txt b/keywords.txt index 11726a1..0803aa3 100644 --- a/keywords.txt +++ b/keywords.txt @@ -47,6 +47,9 @@ String_to_eth_addr KEYWORD2 eth_addr_to_String KEYWORD2 eth_addr_to_c_str KEYWORD2 wifi_csi_info_to_Channel KEYWORD2 +getCountryCode KEYWORD2 +getCountryEnvironment KEYWORD2 +hasCountryInfo KEYWORD2 Packet_to_Device KEYWORD2 # methods from Device.h @@ -66,6 +69,9 @@ getIPAddressAs_c_str KEYWORD2 setIPAddress KEYWORD2 hasIPAddress KEYWORD2 +getSSIDAsString KEYWORD2 +hasSSID KEYWORD2 + setRSSI KEYWORD2 getRSSI KEYWORD2 @@ -113,6 +119,7 @@ DEPART LITERAL1 SEND LITERAL1 RECEIVE LITERAL1 INACTIVE LITERAL1 +PROBE LITERAL1 # public constants from Device.h APPROXIMATE_UNKNOWN_RSSI LITERAL1 \ No newline at end of file diff --git a/library.json b/library.json new file mode 100644 index 0000000..69444cf --- /dev/null +++ b/library.json @@ -0,0 +1,78 @@ +{ + "name": "Approximate", + "version": "2.0.0", + "description": "A WiFi Arduino library for building proximate interactions between your Internet of Things and the ESP8266 or ESP32. Uses WiFi signal strength (RSSI) to estimate physical distance to network devices, obtaining MAC and IP addresses. Supports management and control frame parsing for improved device detection.", + "keywords": "wifi, proximity, rssi, esp8266, esp32, iot, mac-address, signal-strength, promiscuous, csi", + "repository": { + "type": "git", + "url": "https://github.com/davidchatting/Approximate.git" + }, + "authors": [ + { + "name": "David Chatting", + "email": "approximate@davidchatting.com", + "url": "https://github.com/davidchatting", + "maintainer": true + } + ], + "license": "MIT", + "homepage": "https://github.com/davidchatting/Approximate", + "frameworks": "arduino", + "platforms": [ + "espressif8266", + "espressif32" + ], + "dependencies": [ + { + "name": "Arduino-List", + "version": "https://github.com/davidchatting/Arduino-List.git" + } + ], + "examples": [ + { + "name": "CloseBy", + "base": "examples/CloseBy", + "files": ["CloseBy.ino"] + }, + { + "name": "FindMy", + "base": "examples/FindMy", + "files": ["FindMy.ino"] + }, + { + "name": "WatchDevice", + "base": "examples/WatchDevice", + "files": ["WatchDevice.ino"] + }, + { + "name": "CloseByMQTT", + "base": "examples/CloseByMQTT", + "files": ["CloseByMQTT.ino"] + }, + { + "name": "CloseBySonoff", + "base": "examples/CloseBySonoff", + "files": ["CloseBySonoff.ino"] + }, + { + "name": "MonitorCSI", + "base": "examples/MonitorCSI", + "files": ["MonitorCSI.ino"] + }, + { + "name": "ProbeDetect", + "base": "examples/ProbeDetect", + "files": ["ProbeDetect.ino"] + }, + { + "name": "ProximityZones", + "base": "examples/ProximityZones", + "files": ["ProximityZones.ino"] + }, + { + "name": "DeviceFilter", + "base": "examples/DeviceFilter", + "files": ["DeviceFilter.ino"] + } + ] +} diff --git a/library.properties b/library.properties index d517fb9..f835530 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=Approximate -version=1.4 +version=2.0.0 author=David Chatting maintainer=David Chatting sentence=The Approximate Library is a WiFi Arduino library for building proximate interactions between your Internet of Things and the ESP8266 or ESP32. -paragraph=The Approximate Library is a WiFi Arduino library for building proximate interactions between your Internet of Things and the ESP8266 or ESP32. Technically it makes it easy to use WiFi signal strength (RSSI) to estimate the physical distance to a device on your home network, then obtain its MAC address and optionally its IP address. The network activity of these devices can also be observed. +paragraph=The Approximate Library is a WiFi Arduino library for building proximate interactions between your Internet of Things and the ESP8266 or ESP32. Technically it makes it easy to use WiFi signal strength (RSSI) to estimate the physical distance to a device on your home network, then obtain its MAC address and optionally its IP address. The network activity of these devices can also be observed. Now with management and control frame parsing for improved device detection. category=Communication url=https://github.com/davidchatting/Approximate architectures=esp32,esp8266 diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..ed1dd3d --- /dev/null +++ b/platformio.ini @@ -0,0 +1,28 @@ +; PlatformIO Project Configuration File +; https://docs.platformio.org/page/projectconf.html +; +; This file is for building/testing the Approximate library examples. +; +; To test compile an example, use pio ci: +; pio ci examples/CloseBy/CloseBy.ino --lib="." --board=esp32dev +; pio ci examples/CloseBy/CloseBy.ino --lib="." --board=esp12e +; +; To use Approximate as a library in your own PlatformIO project, add to your +; platformio.ini: +; lib_deps = https://github.com/davidchatting/Approximate.git + +[env] +framework = arduino +monitor_speed = 9600 +test_speed = 115200 +test_build_src = yes +lib_deps = + https://github.com/davidchatting/Arduino-List.git + +[env:esp32] +platform = espressif32 +board = esp32dev + +[env:esp8266] +platform = espressif8266 +board = esp12e diff --git a/src/Approximate.cpp b/src/Approximate.cpp index 5acc4cd..37b7afc 100755 --- a/src/Approximate.cpp +++ b/src/Approximate.cpp @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #include "Approximate.h" @@ -158,23 +159,9 @@ void Approximate::onceWifiStatus(wl_status_t status, voidFnPtrWithFnPtrPayload c void Approximate::begin(voidFnPtr thenFnPtr) { Serial.println("Approximate::begin"); - onceWifiStatus(WL_CONNECTED, [](voidFnPtr thenFnPtr) { - if(thenFnPtr) thenFnPtr(); + beginThenFnPtr = thenFnPtr; + beginPending = true; - if(arpTable) { - arpTable -> scan(); //blocking - arpTable -> begin(); - } - - #if defined(ESP8266) - WiFi.disconnect(); - #endif - - //start the packetSniffer after the scan is complete: - if(packetSniffer) packetSniffer -> begin(); - - running = true; - }, thenFnPtr); connectWiFi(); Serial.println("Approximate::begin DONE"); } @@ -210,6 +197,31 @@ bool Approximate::isRunning() { } void Approximate::onWifiStatusChange(wl_status_t oldStatus, wl_status_t newStatus) { + // Handle begin() initialization independently of onceWifiStatus callbacks, + // so user-registered callbacks cannot override ARP scanning (issue #32). + if(beginPending && newStatus == WL_CONNECTED) { + beginPending = false; + + if(beginThenFnPtr) { + beginThenFnPtr(); + beginThenFnPtr = NULL; + } + + if(arpTable) { + arpTable -> scan(); //blocking + arpTable -> begin(); + } + + #if defined(ESP8266) + WiFi.disconnect(); + #endif + + //start the packetSniffer after the scan is complete: + if(packetSniffer) packetSniffer -> begin(); + + running = true; + } + if(newStatus != WL_IDLE_STATUS && newStatus == triggerWifiStatus) { if(onceWifiStatusFnPtr != NULL ) { onceWifiStatusFnPtr(); @@ -250,8 +262,9 @@ wl_status_t Approximate::connectWiFi(char *ssid, char *password) { #elif defined(ESP32) //WiFi.begin() for the ESP32 (1.0.4) > https://github.com/espressif/arduino-esp32/blob/master/libraries/WiFi/src/WiFiSTA.cpp - doesn't call esp_wifi_init() or esp_wifi_start() - which are needed later for esp_wifi_set_csi() - tcpip_adapter_init(); - esp_event_loop_init(NULL, NULL); + esp_netif_init(); + esp_event_loop_create_default(); + esp_netif_create_default_wifi_sta(); if(!WiFi.enableSTA(true)) { log_e("STA enable failed!"); @@ -290,7 +303,8 @@ wl_status_t Approximate::connectWiFi(char *ssid, char *password) { } esp_wifi_set_config(WIFI_IF_STA, &conf); - if(tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA) == ESP_ERR_TCPIP_ADAPTER_DHCPC_START_FAILED){ + esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if(sta_netif && esp_netif_dhcpc_start(sta_netif) == ESP_ERR_ESP_NETIF_DHCPC_START_FAILED){ log_e("dhcp client start failed!"); return WL_CONNECT_FAILED; } @@ -468,6 +482,7 @@ void Approximate::setLocalBSSID(String macAddress) { void Approximate::setLocalBSSID(eth_addr &macAddress) { ETHADDR16_COPY(&this -> localBSSID, &macAddress); + if(packetSniffer) PacketSniffer::setLocalBSSID(macAddress); } void Approximate::setActiveDeviceHandler(DeviceHandler activeDeviceHandler, bool inclusive) { @@ -498,14 +513,9 @@ void Approximate::setChannelStateHandler(ChannelStateHandler channelStateHandler bool Approximate::parsePacket(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t len, int type, int subtype) { bool result = false; - //TODO: is this still needed? - if( wifi_pkt -> rx_ctrl.sig_mode == 1 && len > 512) { - type = PKT_DATA; - } - switch (type) { - case PKT_MGMT: result = parseMgmtPacket(wifi_pkt); break; - case PKT_CTRL: result = parseCtrlPacket(wifi_pkt); break; + case PKT_MGMT: result = parseMgmtPacket(wifi_pkt, len, subtype); break; + case PKT_CTRL: result = parseCtrlPacket(wifi_pkt, len, subtype); break; case PKT_DATA: result = parseDataPacket(wifi_pkt, len); break; case PKT_MISC: result = parseMiscPacket(wifi_pkt); break; } @@ -513,19 +523,93 @@ bool Approximate::parsePacket(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t len, in return(result); } -bool Approximate::parseCtrlPacket(wifi_promiscuous_pkt_t *wifi_pkt) { - return(false); +bool Approximate::parseCtrlPacket(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t len, int subtype) { + bool result = false; + + Device *device = new Device(); + if(PacketSniffer::parseCtrlFrame(wifi_pkt, len, subtype, device)) { + if(!device->matches(ownMacAddress) && (!onlyIndividualDevices || device->isIndividual())) { + result = true; + + if(proximateDeviceHandler) { + Device *proximateDevice = Approximate::getProximateDevice(device); + int rssi = device->getRSSI(); + + if(rssi != APPROXIMATE_UNKNOWN_RSSI) { + if(rssi > proximateRSSIThreshold) { + if(proximateDevice) { + proximateDevice->update(device); + } + else { + proximateDevice = new Device(device); + proximateDeviceList.Add(proximateDevice); + proximateDeviceHandler(proximateDevice, Approximate::ARRIVE); + } + proximateDeviceHandler(proximateDevice, Approximate::PROBE); + proximateDevice->setTimeOutAtMs(millis() + proximateLastSeenTimeoutMs); + } + else { + if(proximateDevice) proximateDevice->update(device); + } + } + } + + if(activeDeviceHandler && (activeDeviceFilterList.IsEmpty() || applyDeviceFilters(device))) { + activeDeviceHandler(device, Approximate::PROBE); + } + } + } + delete(device); + + return(result); } -bool Approximate::parseMgmtPacket(wifi_promiscuous_pkt_t *wifi_pkt) { - return(false); +bool Approximate::parseMgmtPacket(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t len, int subtype) { + bool result = false; + + Device *device = new Device(); + if(PacketSniffer::parseMgmtFrame(wifi_pkt, len, subtype, device)) { + if(!device->matches(ownMacAddress) && (!onlyIndividualDevices || device->isIndividual())) { + result = true; + + if(proximateDeviceHandler) { + Device *proximateDevice = Approximate::getProximateDevice(device); + int rssi = device->getRSSI(); + + if(rssi != APPROXIMATE_UNKNOWN_RSSI) { + if(rssi > proximateRSSIThreshold) { + if(proximateDevice) { + proximateDevice->update(device); + } + else { + proximateDevice = new Device(device); + proximateDeviceList.Add(proximateDevice); + proximateDeviceHandler(proximateDevice, Approximate::ARRIVE); + } + proximateDeviceHandler(proximateDevice, Approximate::PROBE); + proximateDevice->setTimeOutAtMs(millis() + proximateLastSeenTimeoutMs); + } + else { + if(proximateDevice) proximateDevice->update(device); + } + } + } + + if(activeDeviceHandler && (activeDeviceFilterList.IsEmpty() || applyDeviceFilters(device))) { + activeDeviceHandler(device, Approximate::PROBE); + } + } + } + delete(device); + + return(result); } bool Approximate::parseDataPacket(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t payloadLengthBytes) { bool result = false; Device *device = new Device(); - if(Approximate::wifi_promiscuous_pkt_to_Device(wifi_pkt, payloadLengthBytes, device)) { + if(PacketSniffer::parseDataFrame(wifi_pkt, payloadLengthBytes, device)) { if(!device -> matches(ownMacAddress) && (!onlyIndividualDevices || device -> isIndividual())) { result = true; if(proximateDeviceHandler) { @@ -572,7 +656,7 @@ void Approximate::parseChannelStateInformation(wifi_csi_info_t *info) { #if defined(ESP32) if(channelStateHandler) { Channel *channel = new Channel(); - if(wifi_csi_info_to_Channel(info, channel)) { + if(PacketSniffer::parseCSI(info, channel)) { //TODO: apply filtering channelStateHandler(channel); } @@ -661,186 +745,59 @@ bool Approximate::canResolve(ip4_addr_t &ipaddr) { return(result); } +// MAC utility static methods - delegate to free functions for API compatibility bool Approximate::MacAddr_to_eth_addr(MacAddr *in, eth_addr &out) { - bool success = true; - - for(int n=0; n<6; ++n) out.addr[n] = in->mac[n]; - - return(success); + return ::MacAddr_to_eth_addr(in, out); } bool Approximate::uint8_t_to_eth_addr(uint8_t *in, eth_addr &out) { - bool success = true; - - for(int n=0; n<6; ++n) out.addr[n] = in[n]; - - return(success); + return ::uint8_t_to_eth_addr(in, out); } bool Approximate::oui_to_eth_addr(int oui, eth_addr &out) { - bool success = true; - - out.addr[0] = (oui >> 16) & 0xFF; - out.addr[1] = (oui >> 8) & 0xFF; - out.addr[2] = (oui >> 0) & 0xFF; - out.addr[3] = 0xFF; - out.addr[4] = 0xFF; - out.addr[5] = 0xFF; - - return(success); + return ::oui_to_eth_addr(oui, out); } bool Approximate::String_to_eth_addr(String &in, eth_addr &out) { - bool success = c_str_to_eth_addr(in.c_str(), out); - - return(success); + return ::String_to_eth_addr(in, out); } bool Approximate::c_str_to_eth_addr(const char *in, eth_addr &out) { - bool success = false; - - //clear: - for(int n=0; n<6; ++n) out.addr[n] = 0; - - //basic format test ##:##:##:##:##:## - if(strlen(in) == 17) { - int a, b, c, d, e, f; - sscanf(in, "%x:%x:%x:%x:%x:%x", &a, &b, &c, &d, &e, &f); - - out.addr[0] = a; - out.addr[1] = b; - out.addr[2] = c; - out.addr[3] = d; - out.addr[4] = e; - out.addr[5] = f; - - success = true; - } - - return(success); + return ::c_str_to_eth_addr(in, out); } bool Approximate::c_str_to_MacAddr(const char *in, MacAddr &out) { - bool success = false; - - //clear: - for(int n=0; n<6; ++n) out.mac[n] = 0; - - //basic format test ##:##:##:##:##:## - if(strlen(in) == 17) { - int a, b, c, d, e, f; - sscanf(in, "%x:%x:%x:%x:%x:%x", &a, &b, &c, &d, &e, &f); - - out.mac[0] = a; - out.mac[1] = b; - out.mac[2] = c; - out.mac[3] = d; - out.mac[4] = e; - out.mac[5] = f; - - success = true; - } - - return(success); + return ::c_str_to_MacAddr(in, out); } bool Approximate::eth_addr_to_String(eth_addr &in, String &out) { - bool success = true; - - char macAddressAsCharArray[18]; - eth_addr_to_c_str(in, macAddressAsCharArray); - out = String(macAddressAsCharArray); - - return(success); + return ::eth_addr_to_String(in, out); } bool Approximate::eth_addr_to_c_str(eth_addr &in, char *out) { - bool success = true; - - sprintf(out, "%02X:%02X:%02X:%02X:%02X:%02X\0", in.addr[0], in.addr[1], in.addr[2], in.addr[3], in.addr[4], in.addr[5]); - - return(success); + return ::eth_addr_to_c_str(in, out); } bool Approximate::MacAddr_to_c_str(MacAddr *in, char *out) { - bool success = true; - - sprintf(out, "%02X:%02X:%02X:%02X:%02X:%02X\0", in->mac[0], in->mac[1], in->mac[2], in->mac[3], in->mac[4], in->mac[5]); - - return(success); + return ::MacAddr_to_c_str(in, out); } bool Approximate::MacAddr_to_oui(MacAddr *in, int &out) { - bool success = true; - - out = ((in->mac[0] << 16) & 0xFF0000) | ((in->mac[1] << 8) & 0xFF00) | ((in->mac[2] << 0) & 0xFF); - - return(success); + return ::MacAddr_to_oui(in, out); } bool Approximate::MacAddr_to_MacAddr(MacAddr *in, MacAddr &out) { - bool success = true; - - for(int n=0; n<6; ++n) out.mac[n] = in -> mac[n]; - - return(success); + return ::MacAddr_to_MacAddr(in, out); } -bool Approximate::wifi_promiscuous_pkt_to_Device(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t payloadLengthBytes, Device *device) { - bool success = false; - - Packet *packet = new Packet(); - if(wifi_pkt && device && packet) { - wifi_pkt_rx_ctrl_t *rx_ctrl = &(wifi_pkt -> rx_ctrl); - packet -> rssi = rx_ctrl->rssi; - packet -> channel = rx_ctrl->channel; - packet -> payloadLengthBytes = payloadLengthBytes; - - //802.11 packet - wifi_80211_data_frame* frame = (wifi_80211_data_frame*) wifi_pkt -> payload; - MacAddr_to_eth_addr(&(frame -> sa), packet -> src); - MacAddr_to_eth_addr(&(frame -> da), packet -> dst); - - wifi_80211_fctl *fctl = &(frame -> fctl); - byte ds = fctl -> ds; - if(ds == 1 && eth_addr_cmp(&(packet -> dst), &localBSSID)) { - //packet sent by this device - device -> init(packet -> src, localBSSID, packet -> channel, packet -> rssi, millis(), packet -> payloadLengthBytes * -1); - ArpTable::lookupIPAddress(device); - success = true; - } - else if(ds == 2 && eth_addr_cmp(&(packet -> src), &localBSSID)) { - //packet sent to this device - RSSI only informative for messages from device - device -> init(packet -> dst, localBSSID, packet -> channel, packet -> rssi, millis(), packet -> payloadLengthBytes); - ArpTable::lookupIPAddress(device); - success = true; - } - else { - //not associated with this bssid - not on this network - } - } - delete(packet); - - return(success); +String Approximate::getCountryCode() { + return PacketSniffer::getCountryCode(); } -bool Approximate::wifi_csi_info_to_Channel(wifi_csi_info_t *info, Channel *channel) { - bool success = false; - - #if defined(ESP32) - if(info->len >= 128) { - eth_addr thisBssid; - uint8_t_to_eth_addr(info -> mac, thisBssid); - - //Filter this network: - if(eth_addr_cmp(&thisBssid, &localBSSID)) { - channel -> setBssid(thisBssid); - channel -> setBuffer(info->buf); - - success = true; - } - } - #endif +char Approximate::getCountryEnvironment() { + return PacketSniffer::getCountryEnvironment(); +} - return(success); +bool Approximate::hasCountryInfo() { + return PacketSniffer::hasCountryInfo(); } \ No newline at end of file diff --git a/src/Approximate.h b/src/Approximate.h index c80fd67..de48025 100755 --- a/src/Approximate.h +++ b/src/Approximate.h @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #ifndef Approximate_h @@ -42,7 +43,8 @@ class Approximate { DEPART, SEND, RECEIVE, - INACTIVE + INACTIVE, + PROBE // Device detected via management frame (probe request/beacon) } DeviceEvent; typedef void (*DeviceHandler)(Device *device, DeviceEvent event); @@ -54,6 +56,7 @@ class Approximate { case Approximate::RECEIVE: return("RECEIVE"); case Approximate::ARRIVE: return("ARRIVE"); case Approximate::DEPART: return("DEPART"); + case Approximate::PROBE: return("PROBE"); default: return("INACTIVE"); } } @@ -69,6 +72,7 @@ class Approximate { char *password = new char[64]; wl_status_t currentWifiStatus = WL_IDLE_STATUS; + bool initBlind(int channel, uint8_t *bssid, bool ipAddressResolution, bool csiEnabled, bool onlyIndividualDevices); bool initBlind(bool ipAddressResolution, bool csiEnabled, bool onlyIndividualDevices); void onWifiStatusChange(wl_status_t oldStatus, wl_status_t newStatus); @@ -87,9 +91,12 @@ class Approximate { voidFnPtr onceWifiStatusFnPtrPayload; wl_status_t triggerWifiStatus = WL_IDLE_STATUS; + bool beginPending = false; + voidFnPtr beginThenFnPtr = NULL; + static bool parsePacket(wifi_promiscuous_pkt_t *pkt, uint16_t len, int type, int subtype); - static bool parseMgmtPacket(wifi_promiscuous_pkt_t *pkt); - static bool parseCtrlPacket(wifi_promiscuous_pkt_t *pkt); + static bool parseMgmtPacket(wifi_promiscuous_pkt_t *pkt, uint16_t len, int subtype); + static bool parseCtrlPacket(wifi_promiscuous_pkt_t *pkt, uint16_t len, int subtype); static bool parseDataPacket(wifi_promiscuous_pkt_t *pkt, uint16_t payloadLength); static bool parseMiscPacket(wifi_promiscuous_pkt_t *pkt); @@ -115,9 +122,6 @@ class Approximate { void printWiFiStatus(); - static bool wifi_promiscuous_pkt_to_Device(wifi_promiscuous_pkt_t *pkt, uint16_t payloadLengthBytes, Device *device); - static bool wifi_csi_info_to_Channel(wifi_csi_info_t *info, Channel *channel); - public: Approximate(); bool init(String ssid, String password, bool ipAddressResolution = false, bool csiEnabled = false, bool onlyIndividualDevices = true); @@ -178,6 +182,10 @@ class Approximate { void onceWifiStatus(wl_status_t status, voidFnPtrWithBoolPayload callBackFnPtr, bool payload); void onceWifiStatus(wl_status_t status, voidFnPtrWithFnPtrPayload callBackFnPtr, voidFnPtr payload); + static String getCountryCode(); + static char getCountryEnvironment(); + static bool hasCountryInfo(); + static bool MacAddr_to_eth_addr(MacAddr *in, eth_addr &out); static bool uint8_t_to_eth_addr(uint8_t *in, eth_addr &out); static bool oui_to_eth_addr(int oui, eth_addr &out); diff --git a/src/Approximate/ArpTable.cpp b/src/Approximate/ArpTable.cpp index 55e22c9..86f7fc3 100644 --- a/src/Approximate/ArpTable.cpp +++ b/src/Approximate/ArpTable.cpp @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #include "ArpTable.h" diff --git a/src/Approximate/ArpTable.h b/src/Approximate/ArpTable.h index 834bc2c..a41a14c 100644 --- a/src/Approximate/ArpTable.h +++ b/src/Approximate/ArpTable.h @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #ifndef ArpTable_h diff --git a/src/Approximate/Channel.cpp b/src/Approximate/Channel.cpp index 8a8075d..a647264 100644 --- a/src/Approximate/Channel.cpp +++ b/src/Approximate/Channel.cpp @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) February 2021 + Updated 2026 */ #include "Channel.h" diff --git a/src/Approximate/Channel.h b/src/Approximate/Channel.h index 1085f23..b5ef04b 100644 --- a/src/Approximate/Channel.h +++ b/src/Approximate/Channel.h @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) February 2021 + Updated 2026 */ #ifndef Channel_h diff --git a/src/Approximate/Device.cpp b/src/Approximate/Device.cpp index b5650a0..6400cbb 100644 --- a/src/Approximate/Device.cpp +++ b/src/Approximate/Device.cpp @@ -4,10 +4,11 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #include "Device.h" -#include "Approximate.h" +#include "eth_addr.h" Device::Device() { ipAddress.addr = IPADDR_ANY; @@ -15,6 +16,7 @@ Device::Device() { Device::Device(Device *b) { init(b -> macAddress, b -> bssid, b -> channel, b -> rssi, b -> lastSeenAtMs, b -> dataFlowBytes, b -> ipAddress.addr); + setSSID(b -> ssid); } Device::Device(eth_addr &macAddress, eth_addr &bssid, int channel, int rssi, long lastSeenAtMs, int dataFlowBytes, u32_t ipAddress) { @@ -37,17 +39,22 @@ bool Device::operator ==(eth_addr &macAddress) { void Device::init(eth_addr &macAddress, eth_addr &bssid, int channel, int rssi, long lastSeenAtMs, int dataFlowBytes, u32_t ipAddress) { setMacAddress(macAddress); setBssid(bssid); - + setChannel(channel); setRSSI(rssi); setLastSeenAtMs(lastSeenAtMs); setDataFlowBytes(dataFlowBytes); setIPAddress(ipAddress); + + ssid[0] = '\0'; } void Device::update(Device *d) { - if(d) init(d -> macAddress, d -> bssid, d -> channel, d -> rssi, d -> lastSeenAtMs, d -> dataFlowBytes, d -> ipAddress.addr); + if(d) { + init(d -> macAddress, d -> bssid, d -> channel, d -> rssi, d -> lastSeenAtMs, d -> dataFlowBytes, d -> ipAddress.addr); + setSSID(d -> ssid); + } } void Device::getMacAddress(eth_addr &macAddress) { @@ -57,13 +64,13 @@ void Device::getMacAddress(eth_addr &macAddress) { String Device::getMacAddressAsString() { String macAddressAsString = ""; - Approximate::eth_addr_to_String(macAddress, macAddressAsString); + eth_addr_to_String(macAddress, macAddressAsString); return(macAddressAsString); } char *Device::getMacAddressAs_c_str(char *out) { - Approximate::eth_addr_to_c_str(macAddress, out); + eth_addr_to_c_str(macAddress, out); return(out); } @@ -107,6 +114,24 @@ bool Device::hasIPAddress() { return(ipAddress.addr != IPADDR_ANY); } +void Device::setSSID(const char *ssid) { + if(ssid) { + strncpy(this->ssid, ssid, 32); + this->ssid[32] = '\0'; + } + else { + this->ssid[0] = '\0'; + } +} + +String Device::getSSIDAsString() { + return String(ssid); +} + +bool Device::hasSSID() { + return(ssid[0] != '\0'); +} + void Device::setRSSI(int rssi) { this -> rssi = rssi; } diff --git a/src/Approximate/Device.h b/src/Approximate/Device.h index 1d727a2..7ebbcc6 100644 --- a/src/Approximate/Device.h +++ b/src/Approximate/Device.h @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #ifndef Device_h @@ -22,6 +23,7 @@ class Device : public Network { int rssi = APPROXIMATE_UNKNOWN_RSSI; long lastSeenAtMs = -1; int dataFlowBytes = 0; //uploading is negative, downloading positive + char ssid[33] = {0}; long timeOutAtMs = -1; @@ -50,6 +52,10 @@ class Device : public Network { void setIPAddress(u32_t ipAddress); bool hasIPAddress(); + void setSSID(const char *ssid); + String getSSIDAsString(); + bool hasSSID(); + void setRSSI(int rssi); int getRSSI(bool uploadOnly = true); diff --git a/src/Approximate/Filter.cpp b/src/Approximate/Filter.cpp index e9ac60c..852e56d 100644 --- a/src/Approximate/Filter.cpp +++ b/src/Approximate/Filter.cpp @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #include "Filter.h" diff --git a/src/Approximate/Filter.h b/src/Approximate/Filter.h index 7e216d4..6edb6bf 100644 --- a/src/Approximate/Filter.h +++ b/src/Approximate/Filter.h @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #ifndef Filter_h diff --git a/src/Approximate/Network.cpp b/src/Approximate/Network.cpp index 04dea46..bf1bf2d 100644 --- a/src/Approximate/Network.cpp +++ b/src/Approximate/Network.cpp @@ -4,10 +4,11 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) February 2021 + Updated 2026 */ #include "Network.h" -#include "Approximate.h" +#include "eth_addr.h" Network::Network() { } @@ -28,13 +29,13 @@ void Network::getBssid(eth_addr &bssid) { String Network::getBssidAsString() { String bssidAsString = ""; - Approximate::eth_addr_to_String(bssid, bssidAsString); + eth_addr_to_String(bssid, bssidAsString); return(bssidAsString); } char *Network::getBssidAs_c_str(char *out) { - Approximate::eth_addr_to_c_str(bssid, out); + eth_addr_to_c_str(bssid, out); return(out); } diff --git a/src/Approximate/Network.h b/src/Approximate/Network.h index 380e591..70245d2 100644 --- a/src/Approximate/Network.h +++ b/src/Approximate/Network.h @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) February 2021 + Updated 2026 */ #ifndef Network_h diff --git a/src/Approximate/Packet.h b/src/Approximate/Packet.h index 3e7faad..6e314d8 100644 --- a/src/Approximate/Packet.h +++ b/src/Approximate/Packet.h @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #ifndef Packet_h diff --git a/src/Approximate/PacketSniffer.cpp b/src/Approximate/PacketSniffer.cpp index 1ef6500..b01801b 100755 --- a/src/Approximate/PacketSniffer.cpp +++ b/src/Approximate/PacketSniffer.cpp @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #include "PacketSniffer.h" @@ -12,6 +13,10 @@ PacketSniffer::PacketEventHandler PacketSniffer::packetEventHandler = NULL; PacketSniffer::ChannelEventHandler PacketSniffer::channelEventHandler = NULL; bool PacketSniffer::running = false; +eth_addr PacketSniffer::localBSSID = {{0,0,0,0,0,0}}; +char PacketSniffer::countryCode[3] = {0}; +char PacketSniffer::countryEnvironment = 0; + PacketSniffer::PacketSniffer() { Serial.println("PacketSniffer::PacketSniffer"); } @@ -46,8 +51,9 @@ bool PacketSniffer::begin() { } #endif - tcpip_adapter_init(); - esp_event_loop_init(NULL, NULL); + esp_netif_init(); + esp_event_loop_create_default(); + esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&cfg); @@ -178,11 +184,24 @@ void PacketSniffer::setChannelEventHandler(ChannelEventHandler channelEventHandl this -> channelEventHandler = channelEventHandler; } +uint8_t* PacketSniffer::getFrameStart(wifi_promiscuous_pkt_t *pkt) { + #if defined(ESP8266) + // 802.11n AMPDU subframes have a 4-byte delimiter (MPDU length + CRC + + // signature 0x4E) before the actual MAC header. When the Aggregation bit + // is set in rx_ctrl, skip the delimiter so frame control and MAC addresses + // are read from the correct offset. + if(pkt->rx_ctrl.Aggregation) { + return pkt->payload + 4; + } + #endif + return pkt->payload; +} + void PacketSniffer::rxCallback_8266(uint8_t *buf, uint16_t len) { wifi_promiscuous_pkt_t *packet = (wifi_promiscuous_pkt_t *) buf; - wifi_80211_data_frame *frame = (wifi_80211_data_frame *) (packet -> payload); + wifi_80211_data_frame *frame = (wifi_80211_data_frame *) getFrameStart(packet); wifi_promiscuous_pkt_type_t type = frame->fctl.type; - wifi_mgmt_subtypes_t subtype = frame->fctl.subtype; + int subtype = frame->fctl.subtype; uint16_t sig_len = 0; #if defined(ESP8266) @@ -194,9 +213,9 @@ void PacketSniffer::rxCallback_8266(uint8_t *buf, uint16_t len) { void PacketSniffer::rxCallback_32(void* buf, wifi_promiscuous_pkt_type_t type) { wifi_promiscuous_pkt_t *packet = (wifi_promiscuous_pkt_t *) buf; - wifi_80211_data_frame *frame = (wifi_80211_data_frame *) (packet -> payload); - wifi_mgmt_subtypes_t subtype = frame->fctl.subtype; - + wifi_80211_data_frame *frame = (wifi_80211_data_frame *) getFrameStart(packet); + int subtype = frame->fctl.subtype; + uint16_t sig_len = 0; #if defined(ESP32) sig_len = packet->rx_ctrl.sig_len; @@ -205,7 +224,7 @@ void PacketSniffer::rxCallback_32(void* buf, wifi_promiscuous_pkt_type_t type) { rxCallback(packet, sig_len, type, subtype); } -void PacketSniffer::rxCallback(wifi_promiscuous_pkt_t *packet, uint16_t len, wifi_promiscuous_pkt_type_t type, wifi_mgmt_subtypes_t subtype) { +void PacketSniffer::rxCallback(wifi_promiscuous_pkt_t *packet, uint16_t len, wifi_promiscuous_pkt_type_t type, int subtype) { if (running && packetEventHandler) { packetEventHandler(packet, len, (int) type, subtype); } @@ -215,4 +234,245 @@ void PacketSniffer::csiCallback_32(void *ctx, wifi_csi_info_t *data) { if (running && channelEventHandler) { channelEventHandler(data); } +} + +void PacketSniffer::setLocalBSSID(eth_addr &bssid) { + ETHADDR16_COPY(&localBSSID, &bssid); +} + +String PacketSniffer::getCountryCode() { + return String(countryCode); +} + +char PacketSniffer::getCountryEnvironment() { + return countryEnvironment; +} + +bool PacketSniffer::hasCountryInfo() { + return (countryCode[0] != 0); +} + +bool PacketSniffer::parseMgmtFrame(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t len, int subtype, Device *device) { + bool success = false; + + if(wifi_pkt && device) { + wifi_pkt_rx_ctrl_t *rx_ctrl = &(wifi_pkt->rx_ctrl); + + // Management frame header: Addr1=DA, Addr2=SA (transmitter), Addr3=BSSID + wifi_80211_mgmt_frame *frame = (wifi_80211_mgmt_frame *) getFrameStart(wifi_pkt); + + eth_addr srcAddr; + MacAddr_to_eth_addr(&(frame->addr2), srcAddr); + + // Skip broadcast/multicast source addresses + if(srcAddr.addr[0] & 0x01) return false; + // Skip junk MACs (last 3 bytes all zero) + if(srcAddr.addr[3] == 0x0 && srcAddr.addr[4] == 0x0 && srcAddr.addr[5] == 0x0) return false; + + eth_addr bssidAddr; + MacAddr_to_eth_addr(&(frame->addr3), bssidAddr); + + int rssi = rx_ctrl->rssi; + int channel = rx_ctrl->channel; + + // Calculate the size of the management frame header + const size_t mgmt_hdr_size = sizeof(wifi_80211_mgmt_frame); + + switch(subtype) { + case PROBE_REQ: + // Probe requests are sent by all WiFi devices scanning for networks. + // The source MAC (addr2) is the device transmitting the probe. + // Probe requests often have broadcast BSSID (FF:FF:FF:FF:FF:FF). + device->init(srcAddr, bssidAddr, channel, rssi, millis(), 0); + ArpTable::lookupIPAddress(device); + + // Parse Information Elements looking for SSID (IE id 0) + if(len > mgmt_hdr_size) { + const uint8_t *ie_ptr = frame->payload; + size_t ie_remaining = len - mgmt_hdr_size; + + while(ie_remaining >= 2) { + const wifi_80211_ie *ie = (const wifi_80211_ie *) ie_ptr; + if(ie_remaining < (size_t)(2 + ie->length)) break; + + if(ie->id == IE_SSID && ie->length > 0 && ie->length <= 32) { + char ssid_buf[33]; + memcpy(ssid_buf, ie->data, ie->length); + ssid_buf[ie->length] = '\0'; + device->setSSID(ssid_buf); + } + + ie_ptr += 2 + ie->length; + ie_remaining -= 2 + ie->length; + } + } + + success = true; + break; + + case PROBE_RES: + case BEACON: + // Probe responses and beacons are sent by APs. + // Addr2=SA is the AP's MAC, Addr3=BSSID is the network BSSID. + // Only process if from the local network's BSSID. + if(eth_addr_cmp(&bssidAddr, &localBSSID)) { + device->init(srcAddr, bssidAddr, channel, rssi, millis(), 0); + + // Parse Country IE from beacon/probe response body. + // Frame body starts after mgmt header with 12 bytes of fixed fields: + // 8-byte timestamp + 2-byte beacon interval + 2-byte capability info + const size_t fixedFieldsSize = 12; + if(len > mgmt_hdr_size + fixedFieldsSize) { + const uint8_t *ie_ptr = frame->payload + fixedFieldsSize; + size_t ie_remaining = len - mgmt_hdr_size - fixedFieldsSize; + + while(ie_remaining >= 2) { + const wifi_80211_ie *ie = (const wifi_80211_ie *) ie_ptr; + if(ie_remaining < (size_t)(2 + ie->length)) break; + + if(ie->id == IE_COUNTRY && ie->length >= 3) { + countryCode[0] = (char) ie->data[0]; + countryCode[1] = (char) ie->data[1]; + countryCode[2] = '\0'; + countryEnvironment = (char) ie->data[2]; + } + + ie_ptr += 2 + ie->length; + ie_remaining -= 2 + ie->length; + } + } + + success = true; + } + break; + + case AUTHENTICATION: + case ASSOCIATION_REQ: + case REASSOCIATION_REQ: + // Auth/assoc requests from clients contain the client's MAC in addr2. + device->init(srcAddr, bssidAddr, channel, rssi, millis(), 0); + ArpTable::lookupIPAddress(device); + success = true; + break; + + case DEAUTHENTICATION: + case DISASSOCIATION: + // Deauth/disassoc frames - the source addr2 is the sender. + device->init(srcAddr, bssidAddr, channel, rssi, millis(), 0); + success = true; + break; + + default: + break; + } + } + + return(success); +} + +bool PacketSniffer::parseCtrlFrame(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t len, int subtype, Device *device) { + bool success = false; + + if(wifi_pkt && device) { + wifi_pkt_rx_ctrl_t *rx_ctrl = &(wifi_pkt->rx_ctrl); + int rssi = rx_ctrl->rssi; + int channel = rx_ctrl->channel; + + eth_addr deviceAddr; + eth_addr emptyBssid = {{0,0,0,0,0,0}}; + + switch(subtype) { + case CTRL_RTS: + case CTRL_BLOCK_ACK_REQ: + case CTRL_BLOCK_ACK: + case CTRL_PS_POLL: { + // These frames have both RA (addr1) and TA (addr2). + // The transmitter address (addr2) identifies the sending device. + wifi_80211_ctrl_rts_frame *frame = (wifi_80211_ctrl_rts_frame *) getFrameStart(wifi_pkt); + MacAddr_to_eth_addr(&(frame->addr2), deviceAddr); + + // Skip broadcast/multicast + if(deviceAddr.addr[0] & 0x01) return false; + if(deviceAddr.addr[3] == 0x0 && deviceAddr.addr[4] == 0x0 && deviceAddr.addr[5] == 0x0) return false; + + device->init(deviceAddr, emptyBssid, channel, rssi, millis(), 0); + ArpTable::lookupIPAddress(device); + success = true; + break; + } + + case CTRL_CTS: + case CTRL_ACK: { + // CTS and ACK frames have only RA (addr1) - the receiver address. + // The RSSI is from the device that transmitted this frame, but we only + // know who they're talking TO (the RA). We skip these since we can't + // reliably identify the transmitter. + break; + } + + default: + break; + } + } + + return(success); +} + +bool PacketSniffer::parseDataFrame(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t payloadLengthBytes, Device *device) { + bool success = false; + + Packet *packet = new Packet(); + if(wifi_pkt && device && packet) { + wifi_pkt_rx_ctrl_t *rx_ctrl = &(wifi_pkt -> rx_ctrl); + packet -> rssi = rx_ctrl->rssi; + packet -> channel = rx_ctrl->channel; + packet -> payloadLengthBytes = payloadLengthBytes; + + //802.11 packet + wifi_80211_data_frame* frame = (wifi_80211_data_frame*) getFrameStart(wifi_pkt); + MacAddr_to_eth_addr(&(frame -> sa), packet -> src); + MacAddr_to_eth_addr(&(frame -> da), packet -> dst); + + wifi_80211_fctl *fctl = &(frame -> fctl); + byte ds = fctl -> ds; + if(ds == 1 && eth_addr_cmp(&(packet -> dst), &localBSSID)) { + //packet sent by this device + device -> init(packet -> src, localBSSID, packet -> channel, packet -> rssi, millis(), packet -> payloadLengthBytes * -1); + ArpTable::lookupIPAddress(device); + success = true; + } + else if(ds == 2 && eth_addr_cmp(&(packet -> src), &localBSSID)) { + //packet sent to this device - RSSI only informative for messages from device + device -> init(packet -> dst, localBSSID, packet -> channel, packet -> rssi, millis(), packet -> payloadLengthBytes); + ArpTable::lookupIPAddress(device); + success = true; + } + else { + //not associated with this bssid - not on this network + } + } + delete(packet); + + return(success); +} + +bool PacketSniffer::parseCSI(wifi_csi_info_t *info, Channel *channel) { + bool success = false; + + #if defined(ESP32) + if(info->len >= 128) { + eth_addr thisBssid; + uint8_t_to_eth_addr(info -> mac, thisBssid); + + //Filter this network: + if(eth_addr_cmp(&thisBssid, &localBSSID)) { + channel -> setBssid(thisBssid); + channel -> setBuffer(info->buf); + + success = true; + } + } + #endif + + return(success); } \ No newline at end of file diff --git a/src/Approximate/PacketSniffer.h b/src/Approximate/PacketSniffer.h index 7cc7b9a..2f1156f 100755 --- a/src/Approximate/PacketSniffer.h +++ b/src/Approximate/PacketSniffer.h @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #ifndef PacketSniffer_h @@ -12,6 +13,10 @@ #include #include "eth_addr.h" #include "wifi_pkt.h" +#include "Device.h" +#include "Channel.h" +#include "Packet.h" +#include "ArpTable.h" class PacketSniffer { public: @@ -34,6 +39,20 @@ class PacketSniffer { typedef void (*ChannelEventHandler)(wifi_csi_info_t *data); void setChannelEventHandler(ChannelEventHandler channelEventHandler); + // Low-level frame parsing + static bool parseMgmtFrame(wifi_promiscuous_pkt_t *pkt, uint16_t len, int subtype, Device *device); + static bool parseCtrlFrame(wifi_promiscuous_pkt_t *pkt, uint16_t len, int subtype, Device *device); + static bool parseDataFrame(wifi_promiscuous_pkt_t *pkt, uint16_t payloadLengthBytes, Device *device); + static bool parseCSI(wifi_csi_info_t *info, Channel *channel); + + // Local BSSID management + static void setLocalBSSID(eth_addr &bssid); + + // Country info parsed from beacons + static String getCountryCode(); + static char getCountryEnvironment(); + static bool hasCountryInfo(); + private: PacketSniffer(); PacketSniffer(PacketSniffer const&); @@ -50,12 +69,20 @@ class PacketSniffer { static void rxCallback_8266(uint8_t *buf, uint16_t len); static void rxCallback_32(void* buf, wifi_promiscuous_pkt_type_t type); - static void rxCallback(wifi_promiscuous_pkt_t *packet, uint16_t len, wifi_promiscuous_pkt_type_t type, wifi_mgmt_subtypes_t subtype); + static void rxCallback(wifi_promiscuous_pkt_t *packet, uint16_t len, wifi_promiscuous_pkt_type_t type, int subtype); static void csiCallback_32(void *ctx, wifi_csi_info_t *data); + // Returns pointer to start of 802.11 MAC frame within the packet payload. + // On ESP8266, AMPDU subframes have a 4-byte delimiter before the MAC header. + static uint8_t* getFrameStart(wifi_promiscuous_pkt_t *pkt); + static PacketEventHandler packetEventHandler; static ChannelEventHandler channelEventHandler; + + static eth_addr localBSSID; + static char countryCode[3]; + static char countryEnvironment; }; #endif \ No newline at end of file diff --git a/src/Approximate/eth_addr.cpp b/src/Approximate/eth_addr.cpp new file mode 100644 index 0000000..7c1fd5b --- /dev/null +++ b/src/Approximate/eth_addr.cpp @@ -0,0 +1,135 @@ +/* + eth_addr.cpp + Approximate Library + - + David Chatting - github.com/davidchatting/Approximate + MIT License - Copyright (c) October 2020 + Updated 2026 +*/ + +#include "eth_addr.h" + +bool MacAddr_to_eth_addr(MacAddr *in, eth_addr &out) { + bool success = true; + + for(int n=0; n<6; ++n) out.addr[n] = in->mac[n]; + + return(success); +} + +bool uint8_t_to_eth_addr(uint8_t *in, eth_addr &out) { + bool success = true; + + for(int n=0; n<6; ++n) out.addr[n] = in[n]; + + return(success); +} + +bool oui_to_eth_addr(int oui, eth_addr &out) { + bool success = true; + + out.addr[0] = (oui >> 16) & 0xFF; + out.addr[1] = (oui >> 8) & 0xFF; + out.addr[2] = (oui >> 0) & 0xFF; + out.addr[3] = 0xFF; + out.addr[4] = 0xFF; + out.addr[5] = 0xFF; + + return(success); +} + +bool String_to_eth_addr(String &in, eth_addr &out) { + bool success = c_str_to_eth_addr(in.c_str(), out); + + return(success); +} + +bool c_str_to_eth_addr(const char *in, eth_addr &out) { + bool success = false; + + //clear: + for(int n=0; n<6; ++n) out.addr[n] = 0; + + //basic format test ##:##:##:##:##:## + if(strlen(in) == 17) { + int a, b, c, d, e, f; + sscanf(in, "%x:%x:%x:%x:%x:%x", &a, &b, &c, &d, &e, &f); + + out.addr[0] = a; + out.addr[1] = b; + out.addr[2] = c; + out.addr[3] = d; + out.addr[4] = e; + out.addr[5] = f; + + success = true; + } + + return(success); +} + +bool c_str_to_MacAddr(const char *in, MacAddr &out) { + bool success = false; + + //clear: + for(int n=0; n<6; ++n) out.mac[n] = 0; + + //basic format test ##:##:##:##:##:## + if(strlen(in) == 17) { + int a, b, c, d, e, f; + sscanf(in, "%x:%x:%x:%x:%x:%x", &a, &b, &c, &d, &e, &f); + + out.mac[0] = a; + out.mac[1] = b; + out.mac[2] = c; + out.mac[3] = d; + out.mac[4] = e; + out.mac[5] = f; + + success = true; + } + + return(success); +} + +bool eth_addr_to_String(eth_addr &in, String &out) { + bool success = true; + + char macAddressAsCharArray[18]; + eth_addr_to_c_str(in, macAddressAsCharArray); + out = String(macAddressAsCharArray); + + return(success); +} + +bool eth_addr_to_c_str(eth_addr &in, char *out) { + bool success = true; + + sprintf(out, "%02X:%02X:%02X:%02X:%02X:%02X\0", in.addr[0], in.addr[1], in.addr[2], in.addr[3], in.addr[4], in.addr[5]); + + return(success); +} + +bool MacAddr_to_c_str(MacAddr *in, char *out) { + bool success = true; + + sprintf(out, "%02X:%02X:%02X:%02X:%02X:%02X\0", in->mac[0], in->mac[1], in->mac[2], in->mac[3], in->mac[4], in->mac[5]); + + return(success); +} + +bool MacAddr_to_oui(MacAddr *in, int &out) { + bool success = true; + + out = ((in->mac[0] << 16) & 0xFF0000) | ((in->mac[1] << 8) & 0xFF00) | ((in->mac[2] << 0) & 0xFF); + + return(success); +} + +bool MacAddr_to_MacAddr(MacAddr *in, MacAddr &out) { + bool success = true; + + for(int n=0; n<6; ++n) out.mac[n] = in -> mac[n]; + + return(success); +} diff --git a/src/Approximate/eth_addr.h b/src/Approximate/eth_addr.h index 9a9083d..dbff3fe 100644 --- a/src/Approximate/eth_addr.h +++ b/src/Approximate/eth_addr.h @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #ifndef eth_addr_h @@ -33,4 +34,19 @@ struct __attribute__((packed)) MacAddr { } }; +#include + +// Free functions for MAC address conversion utilities +bool MacAddr_to_eth_addr(MacAddr *in, eth_addr &out); +bool uint8_t_to_eth_addr(uint8_t *in, eth_addr &out); +bool oui_to_eth_addr(int oui, eth_addr &out); +bool c_str_to_eth_addr(const char *in, eth_addr &out); +bool c_str_to_MacAddr(const char *in, MacAddr &out); +bool String_to_eth_addr(String &in, eth_addr &out); +bool eth_addr_to_String(eth_addr &in, String &out); +bool eth_addr_to_c_str(eth_addr &in, char *out); +bool MacAddr_to_c_str(MacAddr *in, char *out); +bool MacAddr_to_oui(MacAddr *in, int &out); +bool MacAddr_to_MacAddr(MacAddr *in, MacAddr &out); + #endif \ No newline at end of file diff --git a/src/Approximate/wifi_pkt.h b/src/Approximate/wifi_pkt.h index 7efac0d..ef2f14a 100644 --- a/src/Approximate/wifi_pkt.h +++ b/src/Approximate/wifi_pkt.h @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #ifndef wifi_pkt_h @@ -55,7 +56,7 @@ unsigned channel: 4; //which channel this packet in. unsigned: 12; } wifi_pkt_rx_ctrl_t; - + typedef struct { wifi_pkt_rx_ctrl_t rx_ctrl; u8 payload[0]; // ieee80211 payload @@ -64,7 +65,7 @@ typedef struct { } wifi_csi_info_t; - + #elif defined(ESP32) #define CONFIG_ESP32_WIFI_CSI_ENABLED 1 #define WIFI_MODE WIFI_APSTA_MODE @@ -74,31 +75,80 @@ #include "esp_wifi.h" #include "esp_event.h" #include "esp_wifi_types.h" - + #endif +// ---- IEEE 802.11 Management Frame Subtypes (Type 0) ---- typedef enum { - ASSOCIATION_REQ, - ASSOCIATION_RES, - REASSOCIATION_REQ, - REASSOCIATION_RES, - PROBE_REQ, - PROBE_RES, - NU0, - NU1, - BEACON, - ATIM, - DISASSOCIATION, - AUTHENTICATION, - DEAUTHENTICATION, - ACTION, - ACTION_NACK, + ASSOCIATION_REQ = 0, + ASSOCIATION_RES = 1, + REASSOCIATION_REQ = 2, + REASSOCIATION_RES = 3, + PROBE_REQ = 4, + PROBE_RES = 5, + TIMING_ADV = 6, // Timing Advertisement + NU1 = 7, // Reserved + BEACON = 8, + ATIM = 9, + DISASSOCIATION = 10, + AUTHENTICATION = 11, + DEAUTHENTICATION = 12, + ACTION = 13, + ACTION_NACK = 14, } wifi_mgmt_subtypes_t; +// ---- IEEE 802.11 Control Frame Subtypes (Type 1) ---- +typedef enum { + CTRL_BEAMFORMING = 4, // Beamforming Report Poll + CTRL_VHT_NDP = 5, // VHT NDP Announcement + CTRL_CTRL_EXT = 6, // Control Frame Extension + CTRL_WRAPPER = 7, // Control Wrapper + CTRL_BLOCK_ACK_REQ = 8, // Block Ack Request (BAR) + CTRL_BLOCK_ACK = 9, // Block Ack (BA) + CTRL_PS_POLL = 10, // PS-Poll + CTRL_RTS = 11, // Request to Send + CTRL_CTS = 12, // Clear to Send + CTRL_ACK = 13, // Acknowledgement + CTRL_CF_END = 14, // CF-End + CTRL_CF_END_ACK = 15, // CF-End + CF-Ack +} wifi_ctrl_subtypes_t; + +// ---- IEEE 802.11 Data Frame Subtypes (Type 2) ---- +typedef enum { + DATA_DATA = 0, + DATA_CF_ACK = 1, + DATA_CF_POLL = 2, + DATA_CF_ACK_POLL = 3, + DATA_NULL = 4, // Null (no data) + DATA_CF_ACK_NODATA = 5, + DATA_CF_POLL_NODATA = 6, + DATA_CF_ACK_POLL_NODATA = 7, + DATA_QOS = 8, // QoS Data + DATA_QOS_CF_ACK = 9, + DATA_QOS_CF_POLL = 10, + DATA_QOS_CF_ACK_POLL = 11, + DATA_QOS_NULL = 12, // QoS Null (no data) + DATA_QOS_RESERVED = 13, + DATA_QOS_CF_POLL_NODATA = 14, + DATA_QOS_CF_ACK_POLL_NODATA = 15, +} wifi_data_subtypes_t; + +// ---- IEEE 802.11 ToDS/FromDS Direction Values ---- +// ds field interpretation for address mapping: +// ds=0: IBSS (ad-hoc) - Addr1=DA, Addr2=SA, Addr3=BSSID +// ds=1: To AP - Addr1=BSSID, Addr2=SA, Addr3=DA +// ds=2: From AP - Addr1=DA, Addr2=BSSID, Addr3=SA +// ds=3: WDS bridge - Addr1=RA, Addr2=TA, Addr3=DA, Addr4=SA +#define DS_IBSS 0 // ToDS=0, FromDS=0 +#define DS_TO_AP 1 // ToDS=1, FromDS=0 +#define DS_FROM_AP 2 // ToDS=0, FromDS=1 +#define DS_WDS 3 // ToDS=1, FromDS=1 + +// ---- Frame Control Field ---- typedef struct { unsigned vers:2; wifi_promiscuous_pkt_type_t type:2; - wifi_mgmt_subtypes_t subtype:4; + unsigned subtype:4; unsigned ds:2; unsigned moreFrag:1; unsigned retry:1; @@ -108,6 +158,47 @@ typedef struct { unsigned order:1; } __attribute__((packed)) wifi_80211_fctl; +// ---- Management Frame Header (IEEE 802.11 Section 9.3.3) ---- +// Used for all management frames: beacon, probe req/resp, auth, assoc, etc. +// Management frames always have: Addr1=DA, Addr2=SA, Addr3=BSSID +typedef struct { + wifi_80211_fctl fctl; + unsigned duration:16; + MacAddr addr1; // DA - Destination Address (often broadcast FF:FF:FF:FF:FF:FF) + MacAddr addr2; // SA - Source Address (transmitter) + MacAddr addr3; // BSSID + int16_t seqctl:16; + unsigned char payload[]; +} __attribute__((packed)) wifi_80211_mgmt_frame; + +// ---- Control Frame Headers (IEEE 802.11 Section 9.3.1) ---- +// RTS frame: has both receiver and transmitter addresses +typedef struct { + wifi_80211_fctl fctl; + unsigned duration:16; + MacAddr addr1; // RA - Receiver Address + MacAddr addr2; // TA - Transmitter Address +} __attribute__((packed)) wifi_80211_ctrl_rts_frame; + +// CTS and ACK frames: have only the receiver address +typedef struct { + wifi_80211_fctl fctl; + unsigned duration:16; + MacAddr addr1; // RA - Receiver Address +} __attribute__((packed)) wifi_80211_ctrl_ack_frame; + +// Block Ack Request / Block Ack: receiver and transmitter addresses +typedef struct { + wifi_80211_fctl fctl; + unsigned duration:16; + MacAddr addr1; // RA - Receiver Address + MacAddr addr2; // TA - Transmitter Address + uint16_t bar_ctrl; + uint16_t bar_seq; +} __attribute__((packed)) wifi_80211_ctrl_bar_frame; + +// ---- Data Frame Header (IEEE 802.11 Section 9.3.2) ---- +// Address field meanings depend on ToDS/FromDS (ds field), see DS_* constants above. typedef struct { wifi_80211_fctl fctl; unsigned duration:16; @@ -118,7 +209,28 @@ typedef struct { unsigned char payload[]; } __attribute__((packed)) wifi_80211_data_frame; -//TODO resolve differences with wifi_80211_data_frame: +// ---- Information Element (IE) for management frame bodies ---- +typedef struct { + uint8_t id; + uint8_t length; + uint8_t data[]; +} __attribute__((packed)) wifi_80211_ie; + +// Common IE IDs +#define IE_SSID 0 +#define IE_SUPPORTED_RATES 1 +#define IE_DS_PARAM_SET 3 +#define IE_TIM 5 +#define IE_COUNTRY 7 +#define IE_RSN 48 +#define IE_VENDOR_SPECIFIC 221 + +// Country IE environment field values +#define IE_COUNTRY_ENVIRONMENT_INDOOR 'I' +#define IE_COUNTRY_ENVIRONMENT_OUTDOOR 'O' +#define IE_COUNTRY_ENVIRONMENT_ANY ' ' + +// ---- Generic 4-address MAC header ---- typedef struct { wifi_80211_fctl frame_ctrl; uint8_t addr1[6]; // receiver address @@ -133,4 +245,4 @@ typedef struct { uint8_t payload[2]; // network data ended with 4 bytes csum (CRC32) } wifi_ieee80211_packet_t; -#endif \ No newline at end of file +#endif