From 398972db97dbdb6d06855bbf7a24884499c31812 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 10:55:18 +0000 Subject: [PATCH 01/17] Update copyright headers to 2020-2026 Co-Authored-By: Claude Opus 4.5 --- LICENSE.txt | 2 +- src/Approximate.cpp | 1 + src/Approximate.h | 1 + src/Approximate/ArpTable.cpp | 1 + src/Approximate/ArpTable.h | 1 + src/Approximate/Channel.cpp | 1 + src/Approximate/Channel.h | 1 + src/Approximate/Device.cpp | 1 + src/Approximate/Device.h | 1 + src/Approximate/Filter.cpp | 1 + src/Approximate/Filter.h | 1 + src/Approximate/Network.cpp | 1 + src/Approximate/Network.h | 1 + src/Approximate/Packet.h | 1 + src/Approximate/PacketSniffer.cpp | 1 + src/Approximate/PacketSniffer.h | 1 + src/Approximate/eth_addr.h | 1 + src/Approximate/wifi_pkt.h | 1 + 18 files changed, 18 insertions(+), 1 deletion(-) 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/src/Approximate.cpp b/src/Approximate.cpp index 5acc4cd..d27e509 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" diff --git a/src/Approximate.h b/src/Approximate.h index c80fd67..4386c2b 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 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..fc189cd 100644 --- a/src/Approximate/Device.cpp +++ b/src/Approximate/Device.cpp @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) October 2020 + Updated 2026 */ #include "Device.h" diff --git a/src/Approximate/Device.h b/src/Approximate/Device.h index 1d727a2..1c43cd1 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 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..dabf4d2 100644 --- a/src/Approximate/Network.cpp +++ b/src/Approximate/Network.cpp @@ -4,6 +4,7 @@ - David Chatting - github.com/davidchatting/Approximate MIT License - Copyright (c) February 2021 + Updated 2026 */ #include "Network.h" 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..efa4131 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" diff --git a/src/Approximate/PacketSniffer.h b/src/Approximate/PacketSniffer.h index 7cc7b9a..40854bd 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 diff --git a/src/Approximate/eth_addr.h b/src/Approximate/eth_addr.h index 9a9083d..c8b4fd6 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 diff --git a/src/Approximate/wifi_pkt.h b/src/Approximate/wifi_pkt.h index 7efac0d..8b72d50 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 From 65bacc9d25249d243792ba05812675443b8f02e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 10:55:27 +0000 Subject: [PATCH 02/17] Add complete IEEE 802.11 frame type definitions and structs Define control frame subtypes (wifi_ctrl_subtypes_t), data frame subtypes (wifi_data_subtypes_t), ToDS/FromDS direction constants, and dedicated C structs for management, control (RTS, ACK, BAR), and data frame headers. Add Information Element (IE) struct and common IE ID constants. Change fctl subtype field to unsigned to support all frame types. Co-Authored-By: Claude Opus 4.5 --- src/Approximate/wifi_pkt.h | 148 +++++++++++++++++++++++++++++++------ 1 file changed, 127 insertions(+), 21 deletions(-) diff --git a/src/Approximate/wifi_pkt.h b/src/Approximate/wifi_pkt.h index 8b72d50..c11ad9a 100644 --- a/src/Approximate/wifi_pkt.h +++ b/src/Approximate/wifi_pkt.h @@ -56,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 @@ -65,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 @@ -75,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; @@ -109,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; @@ -119,7 +209,23 @@ 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 + +// ---- Generic 4-address MAC header ---- typedef struct { wifi_80211_fctl frame_ctrl; uint8_t addr1[6]; // receiver address @@ -134,4 +240,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 From 55d8230e987ca065950eff8f4f42a81050083349 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 10:55:35 +0000 Subject: [PATCH 03/17] Implement management and control frame parsing with PROBE event Parse IEEE 802.11 management frames (probe requests, beacons, authentication, association, deauthentication) and control frames (RTS, Block Ack, PS-Poll) to extract device MAC addresses and RSSI for proximity detection. Add new PROBE DeviceEvent type. Change PacketSniffer subtype parameter from wifi_mgmt_subtypes_t to int to support all frame types. Co-Authored-By: Claude Opus 4.5 --- keywords.txt | 1 + src/Approximate.cpp | 203 ++++++++++++++++++++++++++++-- src/Approximate.h | 11 +- src/Approximate/PacketSniffer.cpp | 8 +- src/Approximate/PacketSniffer.h | 2 +- 5 files changed, 210 insertions(+), 15 deletions(-) diff --git a/keywords.txt b/keywords.txt index 11726a1..bc07af1 100644 --- a/keywords.txt +++ b/keywords.txt @@ -113,6 +113,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/src/Approximate.cpp b/src/Approximate.cpp index d27e509..6a62b09 100755 --- a/src/Approximate.cpp +++ b/src/Approximate.cpp @@ -499,14 +499,13 @@ 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; } @@ -514,12 +513,86 @@ 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(wifi_ctrl_frame_to_Device(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(wifi_mgmt_frame_to_Device(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) { @@ -825,6 +898,122 @@ bool Approximate::wifi_promiscuous_pkt_to_Device(wifi_promiscuous_pkt_t *wifi_pk return(success); } +bool Approximate::wifi_mgmt_frame_to_Device(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 *) wifi_pkt->payload; + + 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; + + 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); + 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); + 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 Approximate::wifi_ctrl_frame_to_Device(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 *) wifi_pkt->payload; + 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 Approximate::wifi_csi_info_to_Channel(wifi_csi_info_t *info, Channel *channel) { bool success = false; diff --git a/src/Approximate.h b/src/Approximate.h index 4386c2b..76324e2 100755 --- a/src/Approximate.h +++ b/src/Approximate.h @@ -43,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); @@ -55,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"); } } @@ -89,11 +91,14 @@ class Approximate { wl_status_t triggerWifiStatus = WL_IDLE_STATUS; 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); + static bool wifi_mgmt_frame_to_Device(wifi_promiscuous_pkt_t *pkt, uint16_t len, int subtype, Device *device); + static bool wifi_ctrl_frame_to_Device(wifi_promiscuous_pkt_t *pkt, uint16_t len, int subtype, Device *device); + static void parseChannelStateInformation(wifi_csi_info_t *info); static DeviceHandler activeDeviceHandler; diff --git a/src/Approximate/PacketSniffer.cpp b/src/Approximate/PacketSniffer.cpp index efa4131..3f8c9ca 100755 --- a/src/Approximate/PacketSniffer.cpp +++ b/src/Approximate/PacketSniffer.cpp @@ -183,7 +183,7 @@ 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_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) @@ -196,8 +196,8 @@ 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; - + int subtype = frame->fctl.subtype; + uint16_t sig_len = 0; #if defined(ESP32) sig_len = packet->rx_ctrl.sig_len; @@ -206,7 +206,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); } diff --git a/src/Approximate/PacketSniffer.h b/src/Approximate/PacketSniffer.h index 40854bd..d2165c1 100755 --- a/src/Approximate/PacketSniffer.h +++ b/src/Approximate/PacketSniffer.h @@ -51,7 +51,7 @@ 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); From d8081467b5c400e8a631fe90ea75460d90d2dc39 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 10:55:42 +0000 Subject: [PATCH 04/17] Add forward declarations to examples for PlatformIO compatibility PlatformIO does not auto-generate function prototypes from .ino files like the Arduino IDE does, so add explicit forward declarations for all callback functions in existing examples. Co-Authored-By: Claude Opus 4.5 --- examples/CloseBy/CloseBy.ino | 2 ++ examples/CloseByMQTT/CloseByMQTT.ino | 2 ++ examples/CloseBySonoff/CloseBySonoff.ino | 5 +++++ examples/FindMy/FindMy.ino | 2 ++ examples/MonitorCSI/MonitorCSI.ino | 2 ++ examples/WatchDevice/WatchDevice.ino | 3 +++ 6 files changed, 16 insertions(+) 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/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/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); From da2a998be653a783bff2129c0ed037b6c728dca2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 10:55:48 +0000 Subject: [PATCH 05/17] Add ProbeDetect, ProximityZones, and DeviceFilter examples ProbeDetect demonstrates the new PROBE event for detecting devices via management frames. ProximityZones classifies devices into RSSI- based zones. DeviceFilter shows OUI-based filtering with addActiveDeviceFilter. Co-Authored-By: Claude Opus 4.5 --- examples/DeviceFilter/DeviceFilter.ino | 77 ++++++++++++++++++++++ examples/ProbeDetect/ProbeDetect.ino | 49 ++++++++++++++ examples/ProximityZones/ProximityZones.ino | 71 ++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 examples/DeviceFilter/DeviceFilter.ino create mode 100644 examples/ProbeDetect/ProbeDetect.ino create mode 100644 examples/ProximityZones/ProximityZones.ino 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/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"; +} From 85a219fdcdcb9480fa7e81d060dac80c86377fdc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 10:55:54 +0000 Subject: [PATCH 06/17] Add PlatformIO library manifest and project configuration Add library.json for PlatformIO registry compatibility with Arduino-List dependency. Add platformio.ini with ESP32 and ESP8266 environments for building examples with pio ci. Co-Authored-By: Claude Opus 4.5 --- library.json | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ platformio.ini | 26 +++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 library.json create mode 100644 platformio.ini diff --git a/library.json b/library.json new file mode 100644 index 0000000..3bc9651 --- /dev/null +++ b/library.json @@ -0,0 +1,63 @@ +{ + "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"] + } + ] +} diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..6db0e54 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,26 @@ +; 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 +lib_deps = + https://github.com/davidchatting/Arduino-List.git + +[env:esp32] +platform = espressif32 +board = esp32dev + +[env:esp8266] +platform = espressif8266 +board = esp12e From 028e8205195b9500ff12793e81201a3142e31a32 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 10:56:00 +0000 Subject: [PATCH 07/17] Bump version to 2.0.0 and update CI to actions/checkout@v4 Update library.properties version from 1.4 to 2.0.0 with updated description noting management and control frame parsing. Upgrade GitHub Actions workflows from actions/checkout@v2 to v4. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/esp32.yml | 4 ++-- .github/workflows/esp8266.yml | 4 ++-- library.properties | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/esp32.yml b/.github/workflows/esp32.yml index 90d686a..492c15c 100644 --- a/.github/workflows/esp32.yml +++ b/.github/workflows/esp32.yml @@ -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/.github/workflows/esp8266.yml b/.github/workflows/esp8266.yml index a541ee5..35cf83e 100644 --- a/.github/workflows/esp8266.yml +++ b/.github/workflows/esp8266.yml @@ -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/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 From a262826a19137eaad75dd894f05548dd2479635d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 10:56:05 +0000 Subject: [PATCH 08/17] Update README with v2.0.0 changelog and PlatformIO instructions Document new management/control frame parsing, PROBE event, new examples, PlatformIO installation, and updated DeviceEvent types. Co-Authored-By: Claude Opus 4.5 --- README.md | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 225 insertions(+), 10 deletions(-) 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). From cf641b1ab992212888bfd4a3b115b10d857937cc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 10:57:10 +0000 Subject: [PATCH 09/17] Add .pio/ to .gitignore Co-Authored-By: Claude Opus 4.5 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7b8eeeb..67fa563 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store .vscode/ +.pio/ From fbfcfeef6498b9ee8b9375cf7d15a3ee211e1926 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 11:21:56 +0000 Subject: [PATCH 10/17] Add new examples to library.json PlatformIO manifest Register ProbeDetect, ProximityZones, and DeviceFilter in the examples array so PlatformIO surfaces them correctly. Co-Authored-By: Claude Opus 4.5 --- library.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/library.json b/library.json index 3bc9651..69444cf 100644 --- a/library.json +++ b/library.json @@ -58,6 +58,21 @@ "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"] } ] } From 354568e3fac7ebef852fc9b645429c6c5a4e9f52 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 20:36:37 +0000 Subject: [PATCH 11/17] Add SSID parsing from probe request frames Parse Information Elements in probe request management frames to extract the SSID a device is scanning for. Adds ssid field to Device with setSSID, getSSIDAsString, and hasSSID methods. Co-Authored-By: Claude Opus 4.5 --- .gitignore | 1 + keywords.txt | 3 +++ src/Approximate.cpp | 25 +++++++++++++++++++++++++ src/Approximate/Device.cpp | 28 ++++++++++++++++++++++++++-- src/Approximate/Device.h | 5 +++++ 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 67fa563..5619053 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store .vscode/ .pio/ +test/ diff --git a/keywords.txt b/keywords.txt index bc07af1..af110bb 100644 --- a/keywords.txt +++ b/keywords.txt @@ -66,6 +66,9 @@ getIPAddressAs_c_str KEYWORD2 setIPAddress KEYWORD2 hasIPAddress KEYWORD2 +getSSIDAsString KEYWORD2 +hasSSID KEYWORD2 + setRSSI KEYWORD2 getRSSI KEYWORD2 diff --git a/src/Approximate.cpp b/src/Approximate.cpp index 6a62b09..a51c133 100755 --- a/src/Approximate.cpp +++ b/src/Approximate.cpp @@ -921,6 +921,9 @@ bool Approximate::wifi_mgmt_frame_to_Device(wifi_promiscuous_pkt_t *wifi_pkt, ui 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. @@ -928,6 +931,28 @@ bool Approximate::wifi_mgmt_frame_to_Device(wifi_promiscuous_pkt_t *wifi_pkt, ui // 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; diff --git a/src/Approximate/Device.cpp b/src/Approximate/Device.cpp index fc189cd..1cfd336 100644 --- a/src/Approximate/Device.cpp +++ b/src/Approximate/Device.cpp @@ -16,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) { @@ -38,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) { @@ -108,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 1c43cd1..7ebbcc6 100644 --- a/src/Approximate/Device.h +++ b/src/Approximate/Device.h @@ -23,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; @@ -51,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); From f4c3037188c3824099cb47958690fd429db8dd8a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 20:56:43 +0000 Subject: [PATCH 12/17] Add Country IE parsing from local AP beacons/probe responses Parse IEEE 802.11 Country Information Element (IE id 7) from beacon and probe response frames originating from the local BSSID, storing the country code and environment character as static members with public getters (getCountryCode, getCountryEnvironment, hasCountryInfo). Co-Authored-By: Claude Opus 4.5 --- keywords.txt | 3 +++ src/Approximate.cpp | 39 ++++++++++++++++++++++++++++++++++++++ src/Approximate.h | 6 ++++++ src/Approximate/wifi_pkt.h | 5 +++++ 4 files changed, 53 insertions(+) diff --git a/keywords.txt b/keywords.txt index af110bb..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 diff --git a/src/Approximate.cpp b/src/Approximate.cpp index a51c133..9c586da 100755 --- a/src/Approximate.cpp +++ b/src/Approximate.cpp @@ -23,6 +23,8 @@ eth_addr Approximate::ownMacAddress = {{0,0,0,0,0,0}}; int Approximate::proximateRSSIThreshold = APPROXIMATE_PERSONAL_RSSI; eth_addr Approximate::localBSSID = {{0,0,0,0,0,0}}; +char Approximate::countryCode[3] = {0}; +char Approximate::countryEnvironment = 0; List Approximate::activeDeviceFilterList; List Approximate::proximateDeviceList; @@ -860,6 +862,18 @@ bool Approximate::MacAddr_to_MacAddr(MacAddr *in, MacAddr &out) { return(success); } +String Approximate::getCountryCode() { + return String(countryCode); +} + +char Approximate::getCountryEnvironment() { + return countryEnvironment; +} + +bool Approximate::hasCountryInfo() { + return (countryCode[0] != 0); +} + bool Approximate::wifi_promiscuous_pkt_to_Device(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t payloadLengthBytes, Device *device) { bool success = false; @@ -963,6 +977,31 @@ bool Approximate::wifi_mgmt_frame_to_Device(wifi_promiscuous_pkt_t *wifi_pkt, ui // 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; diff --git a/src/Approximate.h b/src/Approximate.h index 76324e2..0dae9c2 100755 --- a/src/Approximate.h +++ b/src/Approximate.h @@ -110,6 +110,8 @@ class Approximate { static eth_addr ownMacAddress; static eth_addr localBSSID; + static char countryCode[3]; + static char countryEnvironment; static List activeDeviceFilterList; static bool applyDeviceFilters(Device *device); @@ -184,6 +186,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/wifi_pkt.h b/src/Approximate/wifi_pkt.h index c11ad9a..ef2f14a 100644 --- a/src/Approximate/wifi_pkt.h +++ b/src/Approximate/wifi_pkt.h @@ -225,6 +225,11 @@ typedef struct { #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; From 93d46a6d649cf0c4db3815bb4af295b286b0f8bf Mon Sep 17 00:00:00 2001 From: davidchatting-bot Date: Mon, 9 Feb 2026 09:10:31 +0000 Subject: [PATCH 13/17] Move low-level packet parsing out of Approximate class into PacketSniffer Move wifi_mgmt_frame_to_Device, wifi_ctrl_frame_to_Device, wifi_promiscuous_pkt_to_Device, and wifi_csi_info_to_Channel from Approximate into PacketSniffer as parseMgmtFrame, parseCtrlFrame, parseDataFrame, and parseCSI. This separates reusable low-level 802.11 frame parsing from the proximate interaction logic. Extract MAC address conversion utilities (MacAddr_to_eth_addr, eth_addr_to_String, etc.) into free functions in eth_addr.h/cpp. Approximate static methods are kept as thin wrappers for API compatibility. Move countryCode/countryEnvironment storage and localBSSID to PacketSniffer. Device.cpp and Network.cpp now use free functions directly instead of depending on Approximate.h. Co-Authored-By: Claude Opus 4.6 --- src/Approximate.cpp | 346 ++---------------------------- src/Approximate.h | 8 - src/Approximate/Device.cpp | 6 +- src/Approximate/Network.cpp | 6 +- src/Approximate/PacketSniffer.cpp | 245 +++++++++++++++++++++ src/Approximate/PacketSniffer.h | 22 ++ src/Approximate/eth_addr.cpp | 135 ++++++++++++ src/Approximate/eth_addr.h | 15 ++ 8 files changed, 443 insertions(+), 340 deletions(-) create mode 100644 src/Approximate/eth_addr.cpp diff --git a/src/Approximate.cpp b/src/Approximate.cpp index 9c586da..057a605 100755 --- a/src/Approximate.cpp +++ b/src/Approximate.cpp @@ -23,8 +23,6 @@ eth_addr Approximate::ownMacAddress = {{0,0,0,0,0,0}}; int Approximate::proximateRSSIThreshold = APPROXIMATE_PERSONAL_RSSI; eth_addr Approximate::localBSSID = {{0,0,0,0,0,0}}; -char Approximate::countryCode[3] = {0}; -char Approximate::countryEnvironment = 0; List Approximate::activeDeviceFilterList; List Approximate::proximateDeviceList; @@ -471,6 +469,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) { @@ -519,7 +518,7 @@ bool Approximate::parseCtrlPacket(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t len bool result = false; Device *device = new Device(); - if(wifi_ctrl_frame_to_Device(wifi_pkt, len, subtype, device)) { + if(PacketSniffer::parseCtrlFrame(wifi_pkt, len, subtype, device)) { if(!device->matches(ownMacAddress) && (!onlyIndividualDevices || device->isIndividual())) { result = true; @@ -560,7 +559,7 @@ bool Approximate::parseMgmtPacket(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t len bool result = false; Device *device = new Device(); - if(wifi_mgmt_frame_to_Device(wifi_pkt, len, subtype, device)) { + if(PacketSniffer::parseMgmtFrame(wifi_pkt, len, subtype, device)) { if(!device->matches(ownMacAddress) && (!onlyIndividualDevices || device->isIndividual())) { result = true; @@ -601,7 +600,7 @@ bool Approximate::parseDataPacket(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t pay 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) { @@ -648,7 +647,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); } @@ -737,364 +736,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); } String Approximate::getCountryCode() { - return String(countryCode); + return PacketSniffer::getCountryCode(); } char Approximate::getCountryEnvironment() { - return countryEnvironment; + return PacketSniffer::getCountryEnvironment(); } bool Approximate::hasCountryInfo() { - return (countryCode[0] != 0); -} - -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); -} - -bool Approximate::wifi_mgmt_frame_to_Device(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 *) wifi_pkt->payload; - - 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 Approximate::wifi_ctrl_frame_to_Device(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 *) wifi_pkt->payload; - 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 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 - - return(success); + return PacketSniffer::hasCountryInfo(); } \ No newline at end of file diff --git a/src/Approximate.h b/src/Approximate.h index 0dae9c2..b6d7e92 100755 --- a/src/Approximate.h +++ b/src/Approximate.h @@ -96,9 +96,6 @@ class Approximate { static bool parseDataPacket(wifi_promiscuous_pkt_t *pkt, uint16_t payloadLength); static bool parseMiscPacket(wifi_promiscuous_pkt_t *pkt); - static bool wifi_mgmt_frame_to_Device(wifi_promiscuous_pkt_t *pkt, uint16_t len, int subtype, Device *device); - static bool wifi_ctrl_frame_to_Device(wifi_promiscuous_pkt_t *pkt, uint16_t len, int subtype, Device *device); - static void parseChannelStateInformation(wifi_csi_info_t *info); static DeviceHandler activeDeviceHandler; @@ -110,8 +107,6 @@ class Approximate { static eth_addr ownMacAddress; static eth_addr localBSSID; - static char countryCode[3]; - static char countryEnvironment; static List activeDeviceFilterList; static bool applyDeviceFilters(Device *device); @@ -123,9 +118,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); diff --git a/src/Approximate/Device.cpp b/src/Approximate/Device.cpp index 1cfd336..6400cbb 100644 --- a/src/Approximate/Device.cpp +++ b/src/Approximate/Device.cpp @@ -8,7 +8,7 @@ */ #include "Device.h" -#include "Approximate.h" +#include "eth_addr.h" Device::Device() { ipAddress.addr = IPADDR_ANY; @@ -64,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); } diff --git a/src/Approximate/Network.cpp b/src/Approximate/Network.cpp index dabf4d2..bf1bf2d 100644 --- a/src/Approximate/Network.cpp +++ b/src/Approximate/Network.cpp @@ -8,7 +8,7 @@ */ #include "Network.h" -#include "Approximate.h" +#include "eth_addr.h" Network::Network() { } @@ -29,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/PacketSniffer.cpp b/src/Approximate/PacketSniffer.cpp index 3f8c9ca..8a0064b 100755 --- a/src/Approximate/PacketSniffer.cpp +++ b/src/Approximate/PacketSniffer.cpp @@ -13,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"); } @@ -216,4 +220,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 *) wifi_pkt->payload; + + 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 *) wifi_pkt->payload; + 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*) 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); +} + +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 d2165c1..37cc16b 100755 --- a/src/Approximate/PacketSniffer.h +++ b/src/Approximate/PacketSniffer.h @@ -13,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: @@ -35,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&); @@ -57,6 +75,10 @@ class PacketSniffer { 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 c8b4fd6..dbff3fe 100644 --- a/src/Approximate/eth_addr.h +++ b/src/Approximate/eth_addr.h @@ -34,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 From feeccb6e770a773feced9729ba1b7847929ca6d5 Mon Sep 17 00:00:00 2001 From: davidchatting-bot Date: Mon, 9 Feb 2026 09:18:16 +0000 Subject: [PATCH 14/17] Add workflow_dispatch trigger to CI workflows Allow manual triggering of ESP32 and ESP8266 build workflows in addition to push and pull_request events. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/esp32.yml | 2 +- .github/workflows/esp8266.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/esp32.yml b/.github/workflows/esp32.yml index 492c15c..c8eeb2a 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 diff --git a/.github/workflows/esp8266.yml b/.github/workflows/esp8266.yml index 35cf83e..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 From cb3b5f34143f3b6064e5c7c12fd5cfda9a88a8a5 Mon Sep 17 00:00:00 2001 From: davidchatting-bot Date: Mon, 9 Feb 2026 09:25:16 +0000 Subject: [PATCH 15/17] Pin ESP32 Arduino Core to v2.0.17 in CI workflow The latest ESP32 Arduino Core (3.x) removed deprecated tcpip_adapter and esp_event_loop_init APIs. Pin to 2.0.17 which is the last 2.x release compatible with the existing codebase. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/esp32.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/esp32.yml b/.github/workflows/esp32.yml index c8eeb2a..f7db16c 100644 --- a/.github/workflows/esp32.yml +++ b/.github/workflows/esp32.yml @@ -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@2.0.17 extra-arduino-cli-args: "--warnings default" required-libraries: ArduinoJson,StreamUtils,PubSubClient,AceButton From 276e5d25be4e5eb53e4b594be8583f65e56ad640 Mon Sep 17 00:00:00 2001 From: davidchatting-bot Date: Mon, 9 Feb 2026 15:46:06 +0000 Subject: [PATCH 16/17] Fix 802.11n packet parsing (issue #22) Use getFrameStart() to skip 4-byte AMPDU delimiter on ESP8266 when the Aggregation flag is set in rx_ctrl. This ensures frame control and MAC addresses are read from the correct offset for HT packets. Remove the arbitrary sig_mode==1 && len>512 type override hack from Approximate::parsePacket() since the type is now correctly determined from the properly-offset frame control field. Co-Authored-By: Claude Opus 4.6 --- platformio.ini | 2 ++ src/Approximate.cpp | 4 ---- src/Approximate/PacketSniffer.cpp | 23 ++++++++++++++++++----- src/Approximate/PacketSniffer.h | 4 ++++ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/platformio.ini b/platformio.ini index 6db0e54..ed1dd3d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -14,6 +14,8 @@ [env] framework = arduino monitor_speed = 9600 +test_speed = 115200 +test_build_src = yes lib_deps = https://github.com/davidchatting/Arduino-List.git diff --git a/src/Approximate.cpp b/src/Approximate.cpp index 057a605..4b7544e 100755 --- a/src/Approximate.cpp +++ b/src/Approximate.cpp @@ -500,10 +500,6 @@ void Approximate::setChannelStateHandler(ChannelStateHandler channelStateHandler bool Approximate::parsePacket(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t len, int type, int subtype) { bool result = false; - if( wifi_pkt -> rx_ctrl.sig_mode == 1 && len > 512) { - type = PKT_DATA; - } - switch (type) { case PKT_MGMT: result = parseMgmtPacket(wifi_pkt, len, subtype); break; case PKT_CTRL: result = parseCtrlPacket(wifi_pkt, len, subtype); break; diff --git a/src/Approximate/PacketSniffer.cpp b/src/Approximate/PacketSniffer.cpp index 8a0064b..1b6c1af 100755 --- a/src/Approximate/PacketSniffer.cpp +++ b/src/Approximate/PacketSniffer.cpp @@ -183,9 +183,22 @@ 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; int subtype = frame->fctl.subtype; @@ -199,7 +212,7 @@ 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_80211_data_frame *frame = (wifi_80211_data_frame *) getFrameStart(packet); int subtype = frame->fctl.subtype; uint16_t sig_len = 0; @@ -245,7 +258,7 @@ bool PacketSniffer::parseMgmtFrame(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t le 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 *) wifi_pkt->payload; + wifi_80211_mgmt_frame *frame = (wifi_80211_mgmt_frame *) getFrameStart(wifi_pkt); eth_addr srcAddr; MacAddr_to_eth_addr(&(frame->addr2), srcAddr); @@ -374,7 +387,7 @@ bool PacketSniffer::parseCtrlFrame(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t le 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 *) wifi_pkt->payload; + wifi_80211_ctrl_rts_frame *frame = (wifi_80211_ctrl_rts_frame *) getFrameStart(wifi_pkt); MacAddr_to_eth_addr(&(frame->addr2), deviceAddr); // Skip broadcast/multicast @@ -415,7 +428,7 @@ bool PacketSniffer::parseDataFrame(wifi_promiscuous_pkt_t *wifi_pkt, uint16_t pa packet -> payloadLengthBytes = payloadLengthBytes; //802.11 packet - wifi_80211_data_frame* frame = (wifi_80211_data_frame*) wifi_pkt -> payload; + 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); diff --git a/src/Approximate/PacketSniffer.h b/src/Approximate/PacketSniffer.h index 37cc16b..2f1156f 100755 --- a/src/Approximate/PacketSniffer.h +++ b/src/Approximate/PacketSniffer.h @@ -73,6 +73,10 @@ class PacketSniffer { 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; From 8bccd90b165ffaeb1cc2b274728f6b126a8cd89f Mon Sep 17 00:00:00 2001 From: davidchatting-bot Date: Mon, 9 Feb 2026 15:58:07 +0000 Subject: [PATCH 17/17] Fix ESP32 compilation with modern Arduino core (issue #33) and decouple ARP scanning from onceWifiStatus callbacks (issue #32) Issue #33: Replace deprecated tcpip_adapter_init(), esp_event_loop_init(), and tcpip_adapter_dhcpc_start() with esp_netif equivalents in both PacketSniffer.cpp and Approximate.cpp. Remove ESP32 core version pin from CI workflow so it builds against the latest version. Issue #32: Move ARP scanning and packet sniffer initialization out of the onceWifiStatus callback mechanism in begin(). Use a beginPending flag handled directly in onWifiStatusChange() so user-registered onceWifiStatus callbacks cannot silently override the initialization. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/esp32.yml | 2 +- src/Approximate.cpp | 51 +++++++++++++++++++------------ src/Approximate.h | 4 +++ src/Approximate/PacketSniffer.cpp | 5 +-- 4 files changed, 40 insertions(+), 22 deletions(-) diff --git a/.github/workflows/esp32.yml b/.github/workflows/esp32.yml index f7db16c..58fc5c7 100644 --- a/.github/workflows/esp32.yml +++ b/.github/workflows/esp32.yml @@ -21,6 +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@2.0.17 + arduino-platform: esp32:esp32 extra-arduino-cli-args: "--warnings default" required-libraries: ArduinoJson,StreamUtils,PubSubClient,AceButton diff --git a/src/Approximate.cpp b/src/Approximate.cpp index 4b7544e..37b7afc 100755 --- a/src/Approximate.cpp +++ b/src/Approximate.cpp @@ -159,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"); } @@ -211,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(); @@ -251,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!"); @@ -291,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; } diff --git a/src/Approximate.h b/src/Approximate.h index b6d7e92..de48025 100755 --- a/src/Approximate.h +++ b/src/Approximate.h @@ -72,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); @@ -90,6 +91,9 @@ 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, uint16_t len, int subtype); static bool parseCtrlPacket(wifi_promiscuous_pkt_t *pkt, uint16_t len, int subtype); diff --git a/src/Approximate/PacketSniffer.cpp b/src/Approximate/PacketSniffer.cpp index 1b6c1af..b01801b 100755 --- a/src/Approximate/PacketSniffer.cpp +++ b/src/Approximate/PacketSniffer.cpp @@ -51,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);