Add station.cpp, station.h, rssi_measurement.h, radiotap_*.[h|cpp]

Signed-off-by: Tucker Polomik <t.polomik@cablelabs.com>
This commit is contained in:
Tucker Polomik
2022-10-27 13:16:44 -06:00
parent 2f9ff4e6e9
commit c4a9268979
7 changed files with 422 additions and 190 deletions

321
main.cpp
View File

@@ -10,22 +10,28 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <csignal>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <string_view>
#include <vector>
// rt stuff
#include "radiotap-library/platform.h"
#include "radiotap-library/radiotap.h"
#include "radiotap-library/radiotap_iter.h"
// our stuff
#include "radiotap_parse.h"
#include "station.h"
#define MAC2STR(a) (a)[0], (a)[1], (a)[2], (a)[3], (a)[4], (a)[5]
#define MACSTRFMT "%02x:%02x:%02x:%02x:%02x:%02x"
constexpr size_t ETH_ALEN = 6;
// to kill pcap loop on OS signal
static bool stay_alive = true;
// yoinked from linux/ether.h
// addr1 - destination addr
// addr2 - source addr
// addr3 - bssid
// addr4 - optional, just there for padding.
struct ieee80211_hdr {
unsigned short frame_control;
unsigned short duration_id;
@@ -36,6 +42,18 @@ struct ieee80211_hdr {
unsigned short addr4[ETH_ALEN];
} __attribute__((packed));
void printMacToStream(std::ostream &os, unsigned char MACData[])
{
char oldFill = os.fill('0');
os << std::setw(2) << std::hex << static_cast<unsigned int>(MACData[0]);
for (uint i = 1; i < 6; ++i) {
os << ':' << std::setw(2) << std::hex << static_cast<unsigned int>(MACData[i]);
}
os.fill(oldFill);
}
template <typename Callback> static void print_usage_and(Callback cb)
{
static constexpr std::string_view usage_str = "Usage: ./pcap <device> <packet_wait_time (mS)>";
@@ -45,93 +63,54 @@ template <typename Callback> static void print_usage_and(Callback cb)
static const std::chrono::milliseconds STATION_KEEPALIVE_TIMEOUT = std::chrono::milliseconds(5000);
struct radiotap_fields {
int8_t rssi; // dBm
uint16_t channel_number;
uint16_t channel_frequency; // mHz
bool bad_fcs; // bad frame control sequence, indicates that this is bogus crap.
struct packet_capture_params {
// how often do we process a packet?
uint packet_cadence_ms;
// the name of the interface that we're collecting on.
std::string device_name;
};
struct rssi_measurement {
int8_t m_rssi_value;
uint16_t m_measurement_channel;
std::chrono::time_point<std::chrono::high_resolution_clock> m_measurement_timestamp;
};
class station {
radiotap_fields m_rt_fields;
std::array<uint8_t, ETH_ALEN> m_mac;
uint32_t m_rssi_wma;
// the end of this vector will contain the most recent instantaneous measurement.
std::vector<rssi_measurement> m_rssi_measurements;
public:
station(uint8_t mac[ETH_ALEN])
{
for (int i = 0; i < ETH_ALEN; i++)
m_mac[i] = mac[i];
}
station(const std::array<uint8_t, ETH_ALEN> &mac) { m_mac = mac; }
virtual ~station() = default;
std::array<uint8_t, ETH_ALEN> get_mac() const { return m_mac; }
int8_t get_rssi() const { return m_rt_fields.rssi; }
// second nibble of MSB of MAC being 2, E, A or 6 seems to indicate mac randomization (which is canonically only implemented by mobile stations)
bool is_potentially_mobile() const
{
return ((get_mac()[0] & 0x0f) == 0x0A) || ((get_mac()[0] & 0x0f) == 0x0E) ||
((get_mac()[0] & 0x0f) == 0x06) || ((get_mac()[0] & 0x0f) == 0x02);
}
void update_rt_fields(const radiotap_fields &rt_f)
{
m_rt_fields = rt_f;
m_rssi_measurements.push_back(
{m_rt_fields.rssi, m_rt_fields.channel_number, std::chrono::system_clock::now()});
}
int16_t get_frequency() const { return m_rt_fields.channel_frequency; }
uint16_t get_channel() const { return m_rt_fields.channel_number; }
std::chrono::time_point<std::chrono::high_resolution_clock> get_last_rssi_meas_time() const
{
return m_rssi_measurements.back().m_measurement_timestamp;
}
bool is_timed_out_ms(std::chrono::milliseconds timeout_ms) const
{
auto now = std::chrono::system_clock::now();
std::chrono::duration<double, std::milli> elapsed = now - get_last_rssi_meas_time();
return elapsed > timeout_ms;
}
bool operator==(const station &other) const
{
return std::memcmp(this->get_mac().data(), other.get_mac().data(), ETH_ALEN) == 0;
}
// sum (x_1*w_1) + ... + (xn*wn) / sum(w_i)
void calculate_wma()
{
static std::chrono::milliseconds reference_time = std::chrono::milliseconds(5000);
auto now = std::chrono::system_clock::now();
int32_t rssi_sum = 0;
int i = 0;
int denom = 0;
for (const auto &measurement : m_rssi_measurements) {
std::chrono::duration<double, std::milli> time_diff =
now - measurement.m_measurement_timestamp;
auto weight = (reference_time - time_diff).count();
if (weight < 0)
continue;
rssi_sum += (measurement.m_rssi_value * weight);
denom += weight;
}
if (denom != 0)
rssi_sum /= denom;
std::cout << "wma " << rssi_sum << std::endl;
}
};
// list of macs that we want to measure for.
static std::vector<uint8_t *> whitelisted_macs;
static std::vector<station> stations;
static void stop_collecting_metrics(uint8_t mac[ETH_ALEN])
{
bool found = std::find_if(stations.begin(), stations.end(), [&mac](const station &s) {
return std::memcmp(mac, s.get_mac().data(), ETH_ALEN);
}) != stations.end();
// this is not an error case -- we should return true here.
if (!found) {
std::cout << "Station " << std::hex << mac
<< " not found, ignoring request to stop collecting metrics." << std::endl;
return;
}
std::remove_if(whitelisted_macs.begin(), whitelisted_macs.end(),
[&mac](const uint8_t *whitelisted_mac) {
return std::memcmp(whitelisted_mac, mac, ETH_ALEN) == 0;
});
std::remove_if(stations.begin(), stations.end(), [&mac](const station &s) {
return std::memcmp(s.get_mac().data(), mac, ETH_ALEN) == 0;
});
}
static void register_station_of_interest(uint8_t mac[ETH_ALEN])
{
for (const station &s : stations) {
if (std::memcmp(mac, s.get_mac().data(), ETH_ALEN) == 0) {
std::cout << " Station " << std::hex << mac
<< " already known, ignoring register request";
}
}
whitelisted_macs.push_back(mac);
}
/**
* @brief Call 'cb' on every station in the station list
* @brief Call 'cb' on every station in the station list (immutable).
*
* @tparam Callback
* @param cb the callback to call with a station passed in.
@@ -142,11 +121,18 @@ template <typename Callback> void station_for_each(Callback cb)
cb(sta);
}
/**
* @brief Call 'cb' on every station in the station list (mutable).
*
* @tparam Callback
* @param cb the callback to call with a station passed in.
*/
template <typename Callback> void station_for_each_mutable(Callback cb)
{
for (station &s : stations)
cb(s);
}
/**
* @brief Walks every station and checks if they've been seen in at least timeout_ms milliseconds.
*
@@ -170,86 +156,6 @@ static void station_keepalive_check(std::vector<station> &station_list,
stations.end());
}
// only works for sub wifi 6 -- IEEE dudes are wrapping chan # back to 1 for 6GHz : - )
static int frequency_to_channel_number(int freq_)
{
uint16_t channel = 0;
// Channels 1 - 13
if ((freq_ >= 2412) && (freq_ <= 2472)) {
channel = (1 + ((freq_ - 2412) / 5));
}
// Channels 36 - 64
else if ((freq_ >= 5170) && (freq_ <= 5320)) {
channel = (34 + ((freq_ - 5170) / 5));
}
// Channels 100 - 144
else if ((freq_ >= 5500) && (freq_ <= 5720)) {
channel = (100 + ((freq_ - 5500) / 5));
}
// Channels 149 - 161
else if ((freq_ >= 5745) && (freq_ <= 5805)) {
channel = (149 + ((freq_ - 5745) / 5));
}
// Channel 165
else if (freq_ == 5825) {
channel = 165;
}
return (channel);
}
static void parse_radiotap_buf(struct ieee80211_radiotap_iterator &iter, const uint8_t *buf,
size_t buflen, radiotap_fields &rt_fields)
{
// be careful of unaligned access!
int err;
while (!(err = ieee80211_radiotap_iterator_next(&iter))) {
switch (iter.this_arg_index) {
case IEEE80211_RADIOTAP_TSFT:
break;
case IEEE80211_RADIOTAP_FLAGS:
break;
case IEEE80211_RADIOTAP_RATE:
break;
case IEEE80211_RADIOTAP_CHANNEL:
rt_fields.channel_frequency = le16_to_cpu(*(uint16_t *)iter.this_arg);
rt_fields.channel_number = frequency_to_channel_number(rt_fields.channel_frequency);
break;
case IEEE80211_RADIOTAP_FHSS:
break;
case IEEE80211_RADIOTAP_DBM_ANTSIGNAL:
rt_fields.rssi = (int8_t)*iter.this_arg;
break;
case IEEE80211_RADIOTAP_DBM_ANTNOISE:
break;
case IEEE80211_RADIOTAP_LOCK_QUALITY:
case IEEE80211_RADIOTAP_TX_ATTENUATION:
case IEEE80211_RADIOTAP_DB_TX_ATTENUATION:
case IEEE80211_RADIOTAP_DBM_TX_POWER:
case IEEE80211_RADIOTAP_ANTENNA:
case IEEE80211_RADIOTAP_DB_ANTSIGNAL:
break;
case IEEE80211_RADIOTAP_DB_ANTNOISE:
break;
case IEEE80211_RADIOTAP_TX_FLAGS:
break;
case IEEE80211_RADIOTAP_RTS_RETRIES:
case IEEE80211_RADIOTAP_DATA_RETRIES:
break;
break;
case IEEE80211_RADIOTAP_F_BADFCS:
rt_fields.bad_fcs = true;
break;
default:
break;
}
}
if (err != -ENOENT) {
printf("malformed radiotap data\n");
return;
}
}
// typedef void (*pcap_handler)(u_char *, const struct pcap_pkthdr *,
// const u_char *);
static void packet_cb(u_char *args, const struct pcap_pkthdr *pcap_hdr, const u_char *packet)
@@ -257,6 +163,8 @@ static void packet_cb(u_char *args, const struct pcap_pkthdr *pcap_hdr, const u_
int err;
int i;
struct ieee80211_radiotap_iterator iter;
if (!stay_alive)
pcap_breakloop((pcap_t *)args);
err = ieee80211_radiotap_iterator_init(&iter, (ieee80211_radiotap_header *)packet, 2014, NULL);
if (err) {
@@ -265,21 +173,31 @@ static void packet_cb(u_char *args, const struct pcap_pkthdr *pcap_hdr, const u_
}
const size_t eth_hdr_offset = iter._max_length;
struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)(packet + eth_hdr_offset);
if (std::find_if(std::begin(stations), std::end(stations), [&hdr](const station &sta) {
return std::memcmp(sta.get_mac().data(), hdr->addr2, ETH_ALEN) == 0;
}) == stations.end()) {
stations.push_back(hdr->addr2);
}
auto it =
std::find_if(whitelisted_macs.begin(), whitelisted_macs.end(), [&hdr](const uint8_t *mac) {
return std::memcmp(hdr->addr2, mac, ETH_ALEN) == 0;
});
if (it != whitelisted_macs.end()) {
stations.erase(std::remove_if(stations.begin(), stations.end(),
[](const station &s) { return !s.is_potentially_mobile(); }),
stations.end());
// check if it's already accounted for.
auto station_it = std::find_if(stations.begin(), stations.end(), [&hdr](const station &s) {
return std::memcmp(hdr->addr2, s.get_mac().data(), ETH_ALEN) == 0;
});
// if it's not already being tracked, and we care about it, add it to the station list.
if (station_it == stations.end()) {
stations.push_back(hdr->addr2);
}
} else {
// no work to be done, it's not a packet from a station we care about. bail.
return;
}
radiotap_fields rt_fields;
parse_radiotap_buf(iter, (uint8_t *)packet, 2014, rt_fields);
if (rt_fields.bad_fcs) {
std::cout << "This radiotap sample is clapped." << std::endl;
std::cout << "Malformed radiotap header." << std::endl;
return;
}
@@ -291,37 +209,60 @@ static void packet_cb(u_char *args, const struct pcap_pkthdr *pcap_hdr, const u_
return;
sta_it->update_rt_fields(rt_fields);
// DEBUG
station_for_each([](const station &s) {
printf("Station " MACSTRFMT ":", MAC2STR(s.get_mac().data()));
std::cout << " RSSI: " << (int)s.get_rssi() << " ChannelNumber: " << s.get_channel()
<< " ChannelFreq: " << s.get_frequency() << std::endl;
});
// do some housekeeping
station_keepalive_check(stations, STATION_KEEPALIVE_TIMEOUT);
station_for_each_mutable([](station &s) { s.calculate_wma(); });
station_for_each([](const station &s) {
std::cout << "Station ";
printMacToStream(std::cout, s.get_mac().data());
std::cout << std::dec << std::endl
<< " RSSI " << (int)s.get_rssi() << " WMA RSSI " << (int)s.get_wma_rssi()
<< " Channel " << s.get_channel() << " (" << s.get_frequency() << " mHz)"
<< std::endl;
});
return;
}
static int begin_packet_loop(pcap_t *pcap_handle, const packet_capture_params &pcap_params,
pcap_handler callback, int mode)
{
return pcap_loop(pcap_handle, mode, callback, (u_char *)pcap_handle);
}
int main(int argc, char **argv)
{
std::cout << "Welcome to " << argv[0] << std::endl;
if (argc < 3)
print_usage_and([]() { exit(1); });
std::string dev = argv[1];
int packet_cadence_ms = std::stoi(argv[2], 0, 10);
const int signals_of_interest[2] = {
SIGINT,
SIGTERM,
};
packet_capture_params pcap_params{(uint)std::stoi(argv[2], 0, 10), argv[1]};
char err[PCAP_ERRBUF_SIZE];
auto pcap_handle = pcap_open_live(dev.c_str(), BUFSIZ, 1, packet_cadence_ms, err);
auto pcap_handle = pcap_open_live(pcap_params.device_name.c_str(), BUFSIZ, 1,
pcap_params.packet_cadence_ms, err);
if (!pcap_handle) {
std::cout << "Could not get a pcap handle on device '" << dev << "', pcap error: " << err
<< std::endl;
std::cout << "Could not get a pcap handle on device '" << pcap_params.device_name
<< "', pcap error: " << err << std::endl;
return 1;
}
std::cout << "Got a handle to device '" << dev << "'" << std::endl;
pcap_loop(pcap_handle, 0, packet_cb, (u_char *)pcap_handle);
std::cout << "Got a handle to device '" << pcap_params.device_name << "'" << std::endl;
for (int sig : signals_of_interest) {
std::signal(sig, [](int signum) { stay_alive = false; });
}
// DEBUG
uint8_t station_of_interest[ETH_ALEN] = {0x88, 0xf0, 0x31, 0x79, 0xdc, 0x52};
register_station_of_interest(station_of_interest);
int loop_status = begin_packet_loop(pcap_handle, pcap_params, packet_cb, 0);
if (loop_status != PCAP_ERROR_BREAK) {
std::cout << "Unexpected pcap_loop exit code: " << loop_status << std::endl;
}
pcap_close(pcap_handle);
std::cout << "Bye!" << std::endl;
std::cout << "Done sniffing on '" << pcap_params.device_name << "', bye!" << std::endl;
return 0;
}

9
radiotap_fields.h Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
#include <cstdint>
struct radiotap_fields {
int8_t rssi; // dBm
uint16_t channel_number;
uint16_t channel_frequency; // mHz
bool bad_fcs; // bad frame control sequence, indicates that this is bogus data.
};

88
radiotap_parse.cpp Normal file
View File

@@ -0,0 +1,88 @@
#include "radiotap_parse.h"
#include "radiotap_fields.h"
/**
* @brief Convert a sub-WiFi 6 frequency (MHz) to channel number.
*
* Note: only works for sub-WiFi 6, as the IEEE folks are wrapping channel numbers back to 1 for the 6 GHz band.
*
* @param freq_ Frequency, in megahertz (MHz)
* @return int the channel number. Will be zero if the frequency conversion is unknown.
*/
static int frequency_to_channel_number(int freq_)
{
uint16_t channel = 0;
// Channels 1 - 13
if ((freq_ >= 2412) && (freq_ <= 2472)) {
channel = (1 + ((freq_ - 2412) / 5));
}
// Channels 36 - 64
else if ((freq_ >= 5170) && (freq_ <= 5320)) {
channel = (34 + ((freq_ - 5170) / 5));
}
// Channels 100 - 144
else if ((freq_ >= 5500) && (freq_ <= 5720)) {
channel = (100 + ((freq_ - 5500) / 5));
}
// Channels 149 - 161
else if ((freq_ >= 5745) && (freq_ <= 5805)) {
channel = (149 + ((freq_ - 5745) / 5));
}
// Channel 165
else if (freq_ == 5825) {
channel = 165;
}
return (channel);
}
void parse_radiotap_buf(struct ieee80211_radiotap_iterator &iter, const uint8_t *buf, size_t buflen,
radiotap_fields &rt_fields)
{
// be careful of unaligned access!
int err;
while (!(err = ieee80211_radiotap_iterator_next(&iter))) {
switch (iter.this_arg_index) {
case IEEE80211_RADIOTAP_TSFT:
break;
case IEEE80211_RADIOTAP_FLAGS:
break;
case IEEE80211_RADIOTAP_RATE:
break;
case IEEE80211_RADIOTAP_CHANNEL:
rt_fields.channel_frequency = le16_to_cpu(*(uint16_t *)iter.this_arg);
rt_fields.channel_number = frequency_to_channel_number(rt_fields.channel_frequency);
break;
case IEEE80211_RADIOTAP_FHSS:
break;
case IEEE80211_RADIOTAP_DBM_ANTSIGNAL:
rt_fields.rssi = (int8_t)*iter.this_arg;
break;
case IEEE80211_RADIOTAP_DBM_ANTNOISE:
break;
case IEEE80211_RADIOTAP_LOCK_QUALITY:
case IEEE80211_RADIOTAP_TX_ATTENUATION:
case IEEE80211_RADIOTAP_DB_TX_ATTENUATION:
case IEEE80211_RADIOTAP_DBM_TX_POWER:
case IEEE80211_RADIOTAP_ANTENNA:
case IEEE80211_RADIOTAP_DB_ANTSIGNAL:
break;
case IEEE80211_RADIOTAP_DB_ANTNOISE:
break;
case IEEE80211_RADIOTAP_TX_FLAGS:
break;
case IEEE80211_RADIOTAP_RTS_RETRIES:
case IEEE80211_RADIOTAP_DATA_RETRIES:
break;
break;
case IEEE80211_RADIOTAP_F_BADFCS:
rt_fields.bad_fcs = true;
break;
default:
break;
}
}
if (err != -ENOENT) {
return;
}
}

24
radiotap_parse.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include <cstdint>
// rt stuff
#include "radiotap-library/platform.h"
#include "radiotap-library/radiotap.h"
#include "radiotap-library/radiotap_iter.h"
struct radiotap_fields;
/**
* @brief Parse the radiotap header of a packet.
*
*
* If `rt_fields` has `bad_fcs` set, then the output should be ignored.
*
* @param iter The initialized radiotap iterator.
* @param buf The radiotap header.
* @param buflen The length of the radiotap header.
* @param[out] rt_fields Radiotap fields of interest.
*/
void parse_radiotap_buf(struct ieee80211_radiotap_iterator &iter, const uint8_t *buf, size_t buflen,
radiotap_fields &rt_fields);

22
rssi_measurement.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include <chrono>
/**
* @brief Struct to hold metadata about an RSSI measurement.
*/
struct rssi_measurement {
/**
* @brief The RSSI value (usually, log-scale negative dBm)
*
*/
int8_t m_rssi_value;
/**
* @brief The channel number on which the RSSI measurement was made.
*
*/
uint16_t m_measurement_channel;
/**
* @brief The timestamp of this measurement.
*
*/
std::chrono::time_point<std::chrono::high_resolution_clock> m_measurement_timestamp;
};

64
station.cpp Normal file
View File

@@ -0,0 +1,64 @@
#include "station.h"
#include <cstring>
station::station(uint8_t mac[ETH_ALEN])
{
for (int i = 0; i < ETH_ALEN; i++)
m_mac[i] = mac[i];
}
station::station(const std::array<uint8_t, ETH_ALEN> &mac) { m_mac = mac; }
std::array<uint8_t, ETH_ALEN> station::get_mac() const { return m_mac; }
int8_t station::get_rssi() const { return m_rt_fields.rssi; }
// second nibble of MSB of MAC being 2, E, A or 6 seems to indicate mac randomization (which is canonically only implemented by mobile stations)
bool station::is_potentially_mobile() const
{
return ((get_mac()[0] & 0x0f) == 0x0A) || ((get_mac()[0] & 0x0f) == 0x0E) ||
((get_mac()[0] & 0x0f) == 0x06) || ((get_mac()[0] & 0x0f) == 0x02);
}
void station::update_rt_fields(const radiotap_fields &rt_f)
{
m_rt_fields = rt_f;
m_rssi_measurements.push_back(
{m_rt_fields.rssi, m_rt_fields.channel_number, std::chrono::system_clock::now()});
}
int16_t station::get_frequency() const { return m_rt_fields.channel_frequency; }
uint16_t station::get_channel() const { return m_rt_fields.channel_number; }
int8_t station::get_wma_rssi() const { return m_rssi_wma; }
std::chrono::time_point<std::chrono::high_resolution_clock> station::get_last_rssi_meas_time() const
{
return m_rssi_measurements.back().m_measurement_timestamp;
}
bool station::is_timed_out_ms(std::chrono::milliseconds timeout_ms) const
{
auto now = std::chrono::system_clock::now();
std::chrono::duration<double, std::milli> elapsed = now - get_last_rssi_meas_time();
return elapsed > timeout_ms;
}
bool station::operator==(const station &other) const
{
return std::memcmp(this->get_mac().data(), other.get_mac().data(), ETH_ALEN) == 0;
}
// sum (x_1*w_1) + ... + (xn*wn) / sum(w_i)
void station::calculate_wma()
{
static std::chrono::milliseconds reference_time = std::chrono::milliseconds(5000);
auto now = std::chrono::system_clock::now();
int32_t rssi_sum = 0;
int i = 0;
int denom = 0;
for (const auto &measurement : m_rssi_measurements) {
std::chrono::duration<double, std::milli> time_diff =
now - measurement.m_measurement_timestamp;
auto weight = (reference_time - time_diff).count();
if (weight < 0)
continue;
rssi_sum += (measurement.m_rssi_value * weight);
denom += weight;
}
if (denom != 0)
rssi_sum /= denom;
m_rssi_wma = rssi_sum;
}

84
station.h Normal file
View File

@@ -0,0 +1,84 @@
#pragma once
#include "radiotap_fields.h"
#include "rssi_measurement.h"
#include <array>
#include <chrono>
#include <vector>
/**
* @brief The length of a MAC address, in bytes.
*
*/
constexpr size_t ETH_ALEN = 6;
class station {
radiotap_fields m_rt_fields;
std::array<uint8_t, ETH_ALEN> m_mac;
int8_t m_rssi_wma;
// the end of this vector will contain the most recent instantaneous measurement.
std::vector<rssi_measurement> m_rssi_measurements;
public:
station(uint8_t mac[ETH_ALEN]);
station(const std::array<uint8_t, ETH_ALEN> &mac);
virtual ~station() = default;
/**
* @brief Get this station's MAC address.
*
* @return std::array<uint8_t, ETH_ALEN> - the MAC.
*/
std::array<uint8_t, ETH_ALEN> get_mac() const;
/**
* @brief Get the most recent RSSI measurement that has been made for this station.
*
* @return int8_t the RSSI
*/
int8_t get_rssi() const;
/**
* @brief Returns whether or not this station is _likely_ a mobile station (i.e. a cell phone, a laptop, tablet, etc)
*
* The second nibble of the MSB of a station's MAC address being one of [0x2, 0xE, 0xA, 0x6] seems to indicate MAC randomization on the station.
* Canonically, MAC randomization is only implemented for mobile stations, mostly phones.
*
* @return true if this station is likely a mobile station.
* @return false otherwise.
*/
bool is_potentially_mobile() const;
/**
* @brief Update this station's parsed radiotap fields
*
* @param rt_f The radiotap fields to read from.
*/
void update_rt_fields(const radiotap_fields &rt_f);
int16_t get_frequency() const;
uint16_t get_channel() const;
int8_t get_wma_rssi() const;
/**
* @brief Get the measurement time of the most recent RSSI reading. Used to check if a station is 'alive' still.
*
* @return std::chrono::time_point<std::chrono::high_resolution_clock> the timestamp.
*/
std::chrono::time_point<std::chrono::high_resolution_clock> get_last_rssi_meas_time() const;
/**
* @brief Determine if a station is alive still.
*
* @param timeout_ms The timeout value (milliseconds) threshold. If there has not been a new RSSI measurement made for this station in `timeout_ms`, then it is considered timedout.
* @return true if the station is timed out.
* @return false otherwise.
*/
bool is_timed_out_ms(std::chrono::milliseconds timeout_ms) const;
bool operator==(const station &other) const;
/**
* @brief Computes the weighted average of RSSI values over this station's RSSI measurement list.
*
* More recent measurements are weighed higher.
*
* (x_1*w_1) + ... + (x_n*w_n) / sum(w_i) -- where x_i is an RSSI measurement value in dBm, and w_i is it's weight.
*/
void calculate_wma();
};