389 lines
12 KiB
C
389 lines
12 KiB
C
/*
|
|
* mcs_telemetry.c
|
|
*
|
|
* Copyright (c) 2026 Umber Networks & Robert McMahon
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright notice,
|
|
* this list of conditions and the following disclaimer.
|
|
*
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* 3. Neither the name of the copyright holder nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "mcs_telemetry.h"
|
|
#include "wifi_monitor.h"
|
|
#include "esp_log.h"
|
|
#include "esp_timer.h"
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
static const char *TAG = "MCS_Telemetry";
|
|
|
|
static bool s_telemetry_running = false;
|
|
static mcs_telemetry_cb_t s_user_callback = NULL;
|
|
static mcs_telemetry_stats_t s_stats = {0};
|
|
|
|
// 802.11ax MCS rates for 20MHz, 800ns GI, 1 Spatial Stream
|
|
// MCS 0-11: Standard HE rates
|
|
static const uint32_t PHY_RATES_20MHZ_1SS_800NS[] = {
|
|
8650, // MCS 0: BPSK 1/2
|
|
17200, // MCS 1: QPSK 1/2
|
|
25800, // MCS 2: QPSK 3/4
|
|
34400, // MCS 3: 16-QAM 1/2
|
|
51600, // MCS 4: 16-QAM 3/4
|
|
68800, // MCS 5: 64-QAM 2/3
|
|
77400, // MCS 6: 64-QAM 3/4
|
|
86000, // MCS 7: 64-QAM 5/6
|
|
103200, // MCS 8: 256-QAM 3/4
|
|
114700, // MCS 9: 256-QAM 5/6
|
|
129000, // MCS 10: 1024-QAM 3/4
|
|
143400 // MCS 11: 1024-QAM 5/6
|
|
};
|
|
|
|
// 802.11ax MCS rates for 20MHz, 400ns GI, 1 Spatial Stream
|
|
static const uint32_t PHY_RATES_20MHZ_1SS_400NS[] = {
|
|
9600, // MCS 0
|
|
19200, // MCS 1
|
|
28800, // MCS 2
|
|
38400, // MCS 3
|
|
57600, // MCS 4
|
|
76800, // MCS 5
|
|
86400, // MCS 6
|
|
96000, // MCS 7
|
|
115200, // MCS 8
|
|
128100, // MCS 9
|
|
144100, // MCS 10
|
|
160200 // MCS 11
|
|
};
|
|
|
|
// 802.11ax MCS rates for 40MHz, 800ns GI, 1 Spatial Stream
|
|
static const uint32_t PHY_RATES_40MHZ_1SS_800NS[] = {
|
|
17200, // MCS 0
|
|
34400, // MCS 1
|
|
51600, // MCS 2
|
|
68800, // MCS 3
|
|
103200, // MCS 4
|
|
137600, // MCS 5
|
|
154900, // MCS 6
|
|
172100, // MCS 7
|
|
206500, // MCS 8
|
|
229400, // MCS 9
|
|
258100, // MCS 10
|
|
286800 // MCS 11
|
|
};
|
|
|
|
// 802.11ax MCS rates for 40MHz, 400ns GI, 1 Spatial Stream
|
|
static const uint32_t PHY_RATES_40MHZ_1SS_400NS[] = {
|
|
19200, // MCS 0
|
|
38400, // MCS 1
|
|
57600, // MCS 2
|
|
76800, // MCS 3
|
|
115200, // MCS 4
|
|
153600, // MCS 5
|
|
172800, // MCS 6
|
|
192000, // MCS 7
|
|
230400, // MCS 8
|
|
256200, // MCS 9
|
|
288200, // MCS 10
|
|
320300 // MCS 11
|
|
};
|
|
|
|
/**
|
|
* @brief Get device index by MAC address, or create new entry
|
|
*/
|
|
static int mcs_get_device_index(const uint8_t *mac) {
|
|
int empty_slot = -1;
|
|
|
|
for (int i = 0; i < MCS_TELEMETRY_MAX_DEVICES; i++) {
|
|
if (memcmp(s_stats.devices[i].mac, mac, 6) == 0) {
|
|
return i; // Found existing device
|
|
}
|
|
if (empty_slot < 0 && s_stats.devices[i].sample_count == 0) {
|
|
empty_slot = i; // Found empty slot
|
|
}
|
|
}
|
|
|
|
// Use empty slot or create new entry
|
|
if (empty_slot >= 0) {
|
|
memcpy(s_stats.devices[empty_slot].mac, mac, 6);
|
|
memset(&s_stats.devices[empty_slot], 0, sizeof(mcs_device_telemetry_t));
|
|
memcpy(s_stats.devices[empty_slot].mac, mac, 6);
|
|
s_stats.total_devices++;
|
|
return empty_slot;
|
|
}
|
|
|
|
// No space - return oldest device (simple round-robin replacement)
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Update device telemetry with new sample
|
|
*/
|
|
static void mcs_update_device_telemetry(int dev_idx, const mcs_sample_t *sample) {
|
|
if (dev_idx < 0 || dev_idx >= MCS_TELEMETRY_MAX_DEVICES) {
|
|
return;
|
|
}
|
|
|
|
mcs_device_telemetry_t *dev = &s_stats.devices[dev_idx];
|
|
|
|
// Update sample buffer (sliding window)
|
|
dev->samples[dev->sample_idx] = *sample;
|
|
dev->sample_idx = (dev->sample_idx + 1) % 16;
|
|
|
|
// Update counters
|
|
dev->sample_count++;
|
|
dev->total_frames++;
|
|
dev->total_bytes += sample->frame_len;
|
|
dev->last_update_ms = sample->timestamp_ms;
|
|
|
|
if (sample->is_retry) {
|
|
dev->retry_frames++;
|
|
}
|
|
|
|
// Update MCS distribution
|
|
if (sample->mcs <= MCS_TELEMETRY_MAX_MCS) {
|
|
dev->mcs_count[sample->mcs]++;
|
|
|
|
// Update dominant MCS (most frequent in recent samples)
|
|
uint32_t max_count = 0;
|
|
for (int i = 0; i <= MCS_TELEMETRY_MAX_MCS; i++) {
|
|
if (dev->mcs_count[i] > max_count) {
|
|
max_count = dev->mcs_count[i];
|
|
dev->current_mcs = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update SS distribution
|
|
if (sample->ss >= 1 && sample->ss <= MCS_TELEMETRY_MAX_SS) {
|
|
dev->ss_count[sample->ss]++;
|
|
|
|
// Update dominant SS
|
|
uint32_t max_count = 0;
|
|
for (int i = 1; i <= MCS_TELEMETRY_MAX_SS; i++) {
|
|
if (dev->ss_count[i] > max_count) {
|
|
max_count = dev->ss_count[i];
|
|
dev->current_ss = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update RSSI statistics
|
|
if (dev->sample_count == 1) {
|
|
dev->avg_rssi = sample->rssi;
|
|
dev->min_rssi = sample->rssi;
|
|
dev->max_rssi = sample->rssi;
|
|
} else {
|
|
// Running average
|
|
dev->avg_rssi = ((int16_t)dev->avg_rssi * (dev->sample_count - 1) + sample->rssi) / dev->sample_count;
|
|
if (sample->rssi < dev->min_rssi) dev->min_rssi = sample->rssi;
|
|
if (sample->rssi > dev->max_rssi) dev->max_rssi = sample->rssi;
|
|
}
|
|
|
|
// Update PHY rate statistics
|
|
uint32_t total_rate = dev->avg_phy_rate_kbps * (dev->sample_count - 1) + sample->phy_rate_kbps;
|
|
dev->avg_phy_rate_kbps = total_rate / dev->sample_count;
|
|
if (sample->phy_rate_kbps > dev->max_phy_rate_kbps) {
|
|
dev->max_phy_rate_kbps = sample->phy_rate_kbps;
|
|
}
|
|
}
|
|
|
|
esp_err_t mcs_telemetry_init(mcs_telemetry_cb_t callback) {
|
|
ESP_LOGI(TAG, "Initializing MCS telemetry");
|
|
|
|
s_user_callback = callback;
|
|
s_telemetry_running = false;
|
|
memset(&s_stats, 0, sizeof(mcs_telemetry_stats_t));
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t mcs_telemetry_start(void) {
|
|
ESP_LOGI(TAG, "Starting MCS telemetry capture");
|
|
|
|
s_telemetry_running = true;
|
|
s_stats.window_start_ms = esp_timer_get_time() / 1000;
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t mcs_telemetry_stop(void) {
|
|
ESP_LOGI(TAG, "Stopping MCS telemetry capture");
|
|
|
|
s_telemetry_running = false;
|
|
s_stats.window_end_ms = esp_timer_get_time() / 1000;
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t mcs_telemetry_process_frame(const wifi_frame_info_t *frame_info, const wifi_pkt_rx_ctrl_t *rx_ctrl) {
|
|
(void)rx_ctrl; /* Optional: not used, frame_info contains RX metadata */
|
|
if (!s_telemetry_running || !frame_info) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
// Get device index by MAC address (Addr2 = transmitter)
|
|
int dev_idx = mcs_get_device_index(frame_info->addr2);
|
|
if (dev_idx < 0) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
// Create sample from frame info
|
|
mcs_sample_t sample = {0};
|
|
sample.timestamp_ms = esp_timer_get_time() / 1000;
|
|
sample.mcs = frame_info->mcs;
|
|
sample.ss = 1; // TODO: Extract from HT/VHT/HE headers
|
|
sample.rssi = frame_info->rssi;
|
|
sample.channel = frame_info->channel;
|
|
sample.bandwidth = (frame_info->bandwidth == 0) ? MCS_BW_20MHZ :
|
|
(frame_info->bandwidth == 1) ? MCS_BW_40MHZ : MCS_BW_20MHZ;
|
|
sample.frame_len = frame_info->frame_len;
|
|
sample.is_retry = frame_info->retry;
|
|
sample.sig_mode = frame_info->sig_mode;
|
|
|
|
// Calculate PHY rate if we have MCS info
|
|
if (sample.mcs <= MCS_TELEMETRY_MAX_MCS && sample.ss >= 1 && sample.ss <= MCS_TELEMETRY_MAX_SS) {
|
|
sample.phy_rate_kbps = mcs_calculate_phy_rate_ax(sample.mcs, sample.ss, sample.bandwidth, frame_info->sgi);
|
|
} else {
|
|
// Fallback to frame_info's calculated rate or estimate from rate index
|
|
sample.phy_rate_kbps = frame_info->phy_rate_kbps;
|
|
}
|
|
|
|
// Update device telemetry
|
|
mcs_update_device_telemetry(dev_idx, &sample);
|
|
s_stats.total_frames_captured++;
|
|
|
|
// TODO: Parse HT/VHT/HE headers to extract actual SS count
|
|
// This requires parsing the PLCP/HT Control/VHT Control/HE Control fields
|
|
// which follow the MAC header in HT/VHT/HE frames
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t mcs_telemetry_get_stats(mcs_telemetry_stats_t *stats) {
|
|
if (!stats) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
memcpy(stats, &s_stats, sizeof(mcs_telemetry_stats_t));
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
void mcs_telemetry_reset(void) {
|
|
ESP_LOGI(TAG, "Resetting telemetry statistics");
|
|
|
|
memset(&s_stats, 0, sizeof(mcs_telemetry_stats_t));
|
|
}
|
|
|
|
esp_err_t mcs_telemetry_to_json(char *json_buffer, size_t buffer_len, const char *device_id) {
|
|
if (!json_buffer || buffer_len == 0) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
#define MCS_JSON_TAIL_RESERVE 4 /* room for "]\0" and safety */
|
|
if (buffer_len <= MCS_JSON_TAIL_RESERVE) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
uint32_t now_ms = esp_timer_get_time() / 1000;
|
|
int written = snprintf(json_buffer, buffer_len,
|
|
"{\"device_id\":\"%s\",\"timestamp\":%lu,\"total_frames\":%lu,\"devices\":[",
|
|
device_id ? device_id : "unknown", now_ms, s_stats.total_frames_captured);
|
|
|
|
if (written < 0 || (size_t)written >= buffer_len) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
size_t offset = (size_t)written;
|
|
bool first = true;
|
|
|
|
for (int i = 0; i < MCS_TELEMETRY_MAX_DEVICES; i++) {
|
|
mcs_device_telemetry_t *dev = &s_stats.devices[i];
|
|
if (dev->sample_count == 0) continue;
|
|
|
|
size_t space = buffer_len - offset - MCS_JSON_TAIL_RESERVE;
|
|
if (space < 2) break;
|
|
|
|
if (!first) {
|
|
written = snprintf(json_buffer + offset, space, ",");
|
|
if (written < 0) break;
|
|
offset += (size_t)((written < (int)space) ? written : (space - 1));
|
|
space = buffer_len - offset - MCS_JSON_TAIL_RESERVE;
|
|
if (space < 2) break;
|
|
}
|
|
first = false;
|
|
|
|
written = snprintf(json_buffer + offset, space,
|
|
"{\"mac\":\"%02x:%02x:%02x:%02x:%02x:%02x\","
|
|
"\"mcs\":%u,\"ss\":%u,\"rssi\":%d,"
|
|
"\"channel\":%u,\"bandwidth\":%u,"
|
|
"\"frames\":%lu,\"retries\":%lu,"
|
|
"\"phy_rate_kbps\":%lu}",
|
|
dev->mac[0], dev->mac[1], dev->mac[2], dev->mac[3], dev->mac[4], dev->mac[5],
|
|
dev->current_mcs, dev->current_ss, dev->avg_rssi,
|
|
dev->samples[0].channel, dev->samples[0].bandwidth,
|
|
dev->total_frames, dev->retry_frames,
|
|
dev->avg_phy_rate_kbps);
|
|
|
|
if (written < 0) break;
|
|
offset += (size_t)((written < (int)space) ? written : (space - 1));
|
|
}
|
|
|
|
{
|
|
size_t tail_space = buffer_len - offset;
|
|
if (tail_space < 3) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
written = snprintf(json_buffer + offset, tail_space, "]}");
|
|
if (written < 0 || (size_t)written >= tail_space) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
}
|
|
|
|
return ESP_OK;
|
|
#undef MCS_JSON_TAIL_RESERVE
|
|
}
|
|
|
|
uint32_t mcs_calculate_phy_rate_ax(uint8_t mcs, uint8_t ss, mcs_bandwidth_t bandwidth, bool sgi) {
|
|
if (mcs > MCS_TELEMETRY_MAX_MCS || ss < 1 || ss > MCS_TELEMETRY_MAX_SS) {
|
|
return 0;
|
|
}
|
|
|
|
const uint32_t *rate_table = NULL;
|
|
|
|
if (bandwidth == MCS_BW_20MHZ) {
|
|
rate_table = sgi ? PHY_RATES_20MHZ_1SS_400NS : PHY_RATES_20MHZ_1SS_800NS;
|
|
} else if (bandwidth == MCS_BW_40MHZ) {
|
|
rate_table = sgi ? PHY_RATES_40MHZ_1SS_400NS : PHY_RATES_40MHZ_1SS_800NS;
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
// PHY rate = base rate (1SS) * spatial streams
|
|
uint32_t base_rate = rate_table[mcs];
|
|
return base_rate * ss;
|
|
}
|
|
|