seems finished

This commit is contained in:
ti_mo
2026-05-24 21:10:00 +02:00
parent 0df0c5da43
commit 56c7a51411
11 changed files with 348 additions and 130 deletions
+43
View File
@@ -0,0 +1,43 @@
function result = analyzeTrace(trace_file, channel, window, overlap, fft_precision, sample_rate)
trace = load(trace_file, "iq");
% 1
duration = length(trace.iq) / sample_rate;
% 2
[S, F, T] = spectrogram(trace.iq, window, overlap, fft_precision, sample_rate, "centered");
% 3
mag = abs(S).^2;
P_dB = linearTodB(mag);
% Sum all frequency bins per time slot to obtain total channel power.
channel_power_linear = sum(mag, 1);
channel_power_dB = linearTodB(channel_power_linear);
noise_floor_linear = prctile(channel_power_linear, 10);
noise_floor = linearTodB(noise_floor_linear);
% 4
occupancy = sum(channel_power_dB > noise_floor + 10) / numel(channel_power_dB) * 100;
result.trace_file = trace_file;
result.iq = trace.iq;
result.channel = channel;
result.duration = duration;
result.S = S;
result.F = F;
result.T = T;
result.mag = mag;
result.P_dB = P_dB;
result.channel_power_linear = channel_power_linear;
result.channel_power_dB = channel_power_dB;
result.noise_floor_linear = noise_floor_linear;
result.noise_floor = noise_floor;
result.occupancy = occupancy;
end
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+63 -1
View File
@@ -1,4 +1,66 @@
calculations_only = false;
%{
Disclaimer:
To run the scripts, the provided traces have to be placed in ./traces,
e.g. ./traces/2412mhz.mat.
The corresponding subtasks are labeled as % [int]. These labels indicate
which part of the code or which comment block belongs to the respective
task.
task1.m
Contains the main calculations needed for Task 1. Some functionality was
moved into analyzeTrace.m, plotSpectrogram.m, and linearTodB.m, so it can
also be reused in task4.m. Task 1.5 is not included in the script, because
it involves interactive marking of transmissions and is therefore only
documented in the lab report.
task2.m
The textual answers are repeated in the report. Some parts of the MATLAB
example code were copied to ./from_matlab_example and used to parse the
frames and decode the beacons.
task3.m
Contains the solution for Task 3. The explanation is further summarized and
abstracted in the report.
task4.m
Uses and adapts the MATLAB example code for OFDM beacon generation. A trace
file is stored in ./traces/wlan_fahren_wir_noch_beacons.mat, containing the
complex baseband vector of the generated beacon waveform. This is done so
the generated signal can be processed by the helper functions from Task 1
and plotted in the same way as the provided SDR traces.
Struggles:
I found this lab significantly more difficult and time-consuming than the
previous two labs. A major part of the effort was not only the
implementation itself, but also understanding the required background on
WiFi signals, spectrograms, noise floor estimation, occupancy calculation,
and beacon frame decoding.
Task 1 in particular was difficult because the instructions were very
compact and left several implementation details open. Concepts such as
percentile-based noise floor estimation, channel-wide power aggregation,
and occupancy thresholds were not immediately clear to me. This caused a
large amount of debugging, rechecking, and validation.
Another difficulty was that the WiFi background required for the lab was
quite extensive. Since parts of this topic were covered only during the lab
period, it was hard to apply the concepts confidently from the beginning.
Compared to the previous labs, this task felt much larger in scope and had
a significantly higher potential for mistakes, while still being part of a
pass/fail admission requirement. This made the lab feel disproportionate in
effort and risk.
In retrospect, the overall effort was very high. I estimate my total time
investment at over 30 hours, including background research, implementation,
debugging, interpretation, and report writing.
Despite these difficulties, the lab contributed substantially to my
understanding of WiFi signal analysis and beacon frame decoding.
%}
task1;
task2;
task3;
task4;
+3
View File
@@ -0,0 +1,3 @@
function val = linearTodB(toConvert)
val = 10 * log10(toConvert);
end
+13
View File
@@ -0,0 +1,13 @@
function plotSpectrogram(result, figure_id, p_dB_min, p_dB_max)
figure(figure_id);
imagesc(result.T, result.F, result.P_dB);
axis ij;
xlabel("Time [s]");
ylabel("Freq. [Hz]");
title("Spectrogram Trace " + figure_id + " / Channel " + result.channel);
cb = colorbar;
ylabel(cb, "Power [dB]");
clim([p_dB_min p_dB_max]);
% Optional:
% xlim([0 0.05]);
end
+37 -119
View File
@@ -1,7 +1,4 @@
trace1 = load("traces/2412mhz.mat", "iq");
trace2 = load("traces/2432mhz.mat", "iq");
trace3 = load("traces/2452mhz.mat", "iq");
trace4 = load("traces/2472mhz.mat", "iq");
calculations_only = false;
window = 512;
overlap = 64;
@@ -9,81 +6,32 @@ fft_precision = 2048;
sample_rate = 20e6; % 20 MS/s -> 20e6 S/s
% 1.
duration1 = length(trace1.iq) / sample_rate;
duration2 = length(trace2.iq) / sample_rate;
duration3 = length(trace3.iq) / sample_rate;
duration4 = length(trace4.iq) / sample_rate;
channels = [1 5 9 13];
% 2.
% get all four plot values
[S1,F1,T1] = spectrogram(trace1.iq, window, overlap, fft_precision, sample_rate, "centered");
[S2,F2,T2] = spectrogram(trace2.iq, window, overlap, fft_precision, sample_rate, "centered");
[S3,F3,T3] = spectrogram(trace3.iq, window, overlap, fft_precision, sample_rate, "centered");
[S4,F4,T4] = spectrogram(trace4.iq, window, overlap, fft_precision, sample_rate, "centered");
res1 = analyzeTrace("traces/2412mhz.mat", channels(1), window, overlap, fft_precision, sample_rate);
res2 = analyzeTrace("traces/2432mhz.mat", channels(2), window, overlap, fft_precision, sample_rate);
res3 = analyzeTrace("traces/2452mhz.mat", channels(3), window, overlap, fft_precision, sample_rate);
res4 = analyzeTrace("traces/2472mhz.mat", channels(4), window, overlap, fft_precision, sample_rate);
mag1 = abs(S1).^2;
mag2 = abs(S2).^2;
mag3 = abs(S3).^2;
mag4 = abs(S4).^2;
results = [res1 res2 res3 res4];
% begin calibrated color scale
P1_dB = 10 * log10(mag1);
P2_dB = 10 * log10(mag2);
P3_dB = 10 * log10(mag3);
P4_dB = 10 * log10(mag4);
all_P_dB = [P1_dB(:); P2_dB(:); P3_dB(:); P4_dB(:)];
% Calibrated color scale over all traces
all_P_dB = [
res1.P_dB(:)
res2.P_dB(:)
res3.P_dB(:)
res4.P_dB(:)
];
p_dB_min = min(all_P_dB);
p_dB_max = max(all_P_dB);
% end power calibrated color scale
% 2
if ~calculations_only
% plot all figures like example shows
figure(1);
imagesc(T1, F1, P1_dB);
axis ij;
xlabel("Time [s]");
ylabel("Freq. [Hz]");
title("Spectrogram Trace 1");
cb = colorbar;
ylabel(cb, "Power [dB]");
clim([p_dB_min p_dB_max]);
% xlim([0 0.05]);
figure(2);
imagesc(T2, F2, P2_dB);
axis ij;
xlabel("Time [s]");
ylabel("Freq. [Hz]");
title("Spectrogram Trace 2");
cb = colorbar;
ylabel(cb, "Power [dB]");
clim([p_dB_min p_dB_max]);
% xlim([0 0.05]);
figure(3);
imagesc(T3, F3, P3_dB);
axis ij;
xlabel("Time [s]");
ylabel("Freq. [Hz]");
title("Spectrogram Trace 3");
cb = colorbar;
ylabel(cb, "Power [dB]");
clim([p_dB_min p_dB_max]);
% xlim([0 0.05]);
figure(4);
imagesc(T4, F4, P4_dB);
axis ij; % flip so its like the example fig
xlabel("Time [s]");
ylabel("Freq. [Hz]");
title("Spectrogram Trace 4");
cb = colorbar;
ylabel(cb, "Power [dB]");
clim([p_dB_min p_dB_max]);
% xlim([0 0.05]); % reduce view so its like the example fig
plotSpectrogram(res1, 1, p_dB_min, p_dB_max);
plotSpectrogram(res2, 2, p_dB_min, p_dB_max);
plotSpectrogram(res3, 3, p_dB_min, p_dB_max);
plotSpectrogram(res4, 4, p_dB_min, p_dB_max);
end
%{
@@ -115,41 +63,15 @@ occupancy4 = sum(P4_dB(:) > noise_floor4 + 10) / numel(P4_dB) * 100;
% 3.
% Sum all frequency bins per time slot to obtain total channel power.
channel_power1_linear = sum(mag1, 1);
channel_power2_linear = sum(mag2, 1);
channel_power3_linear = sum(mag3, 1);
channel_power4_linear = sum(mag4, 1);
channel_power1_dB = linearTodB(channel_power1_linear);
channel_power2_dB = linearTodB(channel_power2_linear);
channel_power3_dB = linearTodB(channel_power3_linear);
channel_power4_dB = linearTodB(channel_power4_linear);
noise_floor1_linear = prctile(channel_power1_linear, 10);
noise_floor2_linear = prctile(channel_power2_linear, 10);
noise_floor3_linear = prctile(channel_power3_linear, 10);
noise_floor4_linear = prctile(channel_power4_linear, 10);
noise_floor1 = linearTodB(noise_floor1_linear);
noise_floor2 = linearTodB(noise_floor2_linear);
noise_floor3 = linearTodB(noise_floor3_linear);
noise_floor4 = linearTodB(noise_floor4_linear);
noise_floor_avg = linearTodB(mean([
noise_floor1_linear
noise_floor2_linear
noise_floor3_linear
noise_floor4_linear
res1.noise_floor_linear
res2.noise_floor_linear
res3.noise_floor_linear
res4.noise_floor_linear
]));
% 4.
occupancy1 = sum(channel_power1_dB > noise_floor1 + 10) / numel(channel_power1_dB) * 100;
occupancy2 = sum(channel_power2_dB > noise_floor2 + 10) / numel(channel_power2_dB) * 100;
occupancy3 = sum(channel_power3_dB > noise_floor3 + 10) / numel(channel_power3_dB) * 100;
occupancy4 = sum(channel_power4_dB > noise_floor4 + 10) / numel(channel_power4_dB) * 100;
occupancies = [occupancy1 occupancy2 occupancy3 occupancy4];
channels = [1 5 9 13];
occupancies = [res1.occupancy res2.occupancy res3.occupancy res4.occupancy];
[occupancy_max, max_idx] = max(occupancies);
busiest_channel = channels(max_idx);
@@ -158,30 +80,26 @@ if ~calculations_only
disp("Results Task 1:" + newline)
disp("-- Trace duration ---")
disp("Channel 1 duration: " + duration1 + "s")
disp("Channel 5 duration: " + duration2 + "s")
disp("Channel 9 duration: " + duration3 + "s")
disp("Channel 13 duration: " + duration4 + "s" + newline)
disp("Channel 1 duration: " + res1.duration + "s")
disp("Channel 5 duration: " + res2.duration + "s")
disp("Channel 9 duration: " + res3.duration + "s")
disp("Channel 13 duration: " + res4.duration + "s" + newline)
disp("--- Noise floor ---")
disp("Distinct:")
disp("Channel 1 noise floor: " + noise_floor1 + "dB")
disp("Channel 5 noise floor: " + noise_floor2 + "dB")
disp("Channel 9 noise floor: " + noise_floor3 + "dB")
disp("Channel 13 noise floor: " + noise_floor4 + "dB" + newline)
disp("Channel 1 noise floor: " + res1.noise_floor + "dB")
disp("Channel 5 noise floor: " + res2.noise_floor + "dB")
disp("Channel 9 noise floor: " + res3.noise_floor + "dB")
disp("Channel 13 noise floor: " + res4.noise_floor + "dB" + newline)
disp("Combined:")
disp("Channel all noise floor: " + noise_floor_avg + "dB" + newline)
disp("-- Occupancy ---")
disp("Channel 1 occupancy: " + occupancy1 + "%")
disp("Channel 5 occupancy: " + occupancy2 + "%")
disp("Channel 9 occupancy: " + occupancy3 + "%")
disp("Channel 13 occupancy: " + occupancy4 + "%" + newline)
disp("Channel 1 occupancy: " + res1.occupancy + "%")
disp("Channel 5 occupancy: " + res2.occupancy + "%")
disp("Channel 9 occupancy: " + res3.occupancy + "%")
disp("Channel 13 occupancy: " + res4.occupancy + "%" + newline)
disp("Busiest channel: " + busiest_channel)
end
function val = linearTodB(toConvert)
val = 10 * log10(toConvert);
disp("Busiest channel: " + busiest_channel + newline)
end
+17 -9
View File
@@ -1,23 +1,27 @@
addpath("from_matlab_example");
rxFrame1 = trace1.iq(:);
rxFrame2 = trace2.iq(:);
rxFrame3 = trace3.iq(:);
rxFrame4 = trace4.iq(:);
rxFrame1 = res1.iq(:);
rxFrame2 = res2.iq(:);
rxFrame3 = res3.iq(:);
rxFrame4 = res4.iq(:);
beaconFrames1 = extractBeaconFrames(rxFrame1);
beaconFrames2 = extractBeaconFrames(rxFrame2);
beaconFrames3 = extractBeaconFrames(rxFrame3);
beaconFrames4 = extractBeaconFrames(rxFrame4);
disp("Results Task 2:" + newline)
% 1.
disp("Beacon Frame 1")
beaconFrames1 = extractBeaconFrames(rxFrame1);
printBeaconTable(beaconFrames1);
disp("Beacon Frame 2")
beaconFrames2 = extractBeaconFrames(rxFrame2);
printBeaconTable(beaconFrames2);
disp("Beacon Frame 3")
beaconFrames3 = extractBeaconFrames(rxFrame3);
printBeaconTable(beaconFrames3);
disp("Beacon Frame 4")
beaconFrames4 = extractBeaconFrames(rxFrame4);
printBeaconTable(beaconFrames4);
% 2.
@@ -71,7 +75,7 @@ function beaconFrames = extractBeaconFrames(rxFrame)
while searchOffset < length(rxFrame)
oldOffset = searchOffset;
[bitsData, decParams, searchOffset, res] = recoverOFDMBits(rxFrame, searchOffset);
[bitsData, ~, searchOffset, res] = recoverOFDMBits(rxFrame, searchOffset);
if searchOffset <= oldOffset
searchOffset = oldOffset + 1;
@@ -83,6 +87,10 @@ function beaconFrames = extractBeaconFrames(rxFrame)
[cfgMAC, ~, decodeStatus] = wlanMPDUDecode(bitsData, SuppressWarnings=true);
if ~decodeStatus
disp(cfgMAC.FrameType + " at " + res.PacketOffset / 20e6 * 1000 + "ms")
end
if ~decodeStatus && matches(cfgMAC.FrameType, "Beacon")
if isempty(cfgMAC.ManagementConfig.SSID)
ssid = "Hidden";
+22
View File
@@ -0,0 +1,22 @@
%{
disp("Results Task 3:" + newline)
Looking at trace 1 it likely contains virtualized APs.
Analyzing the BSSIDs of trace 1 we can for example see:
SSID BSSID
TUB-IoT 5C:E1:76:66:02:E2
eduroam 5C:E1:76:66:02:E3
TUB-Guest 5C:E1:76:66:02:E4
_Free_Wifi_Berlin 5C:E1:76:66:02:E7
All four networks share a very similar BSSID,
the first five bytes are identical,
only the last byte of the MAC changes in minimal incremental steps,
so these are likely virtualized APs.
In a ideal conditioned scenario the rss and snr of the networks would be
very similar, because all beacons would be transmitted from the same
physical location. This helps to indicate the use of virtual APs.
However, this is just an additional indicator,
it is not enough, because APs can also be really close to each other.
%}
+65
View File
@@ -0,0 +1,65 @@
% disp("Results Task 4:" + newline)
saveToFile = true;
ssid = "wlan fahren wir noch";
beaconInterval = 100;
band = 2.4;
channel = 1;
frameBodyConfig = wlanMACManagementConfig(BeaconInterval=beaconInterval, SSID=ssid);
dsElementID = 3;
dsInformation = dec2hex(channel, 2);
frameBodyConfig = frameBodyConfig.addIE(dsElementID, dsInformation);
beaconFrameConfig = wlanMACFrameConfig(FrameType="Beacon", ManagementConfig=frameBodyConfig, FromDS=false);
[mpduBits, mpduLength] = wlanMACFrame(beaconFrameConfig, OutputFormat="bits");
fc = wlanChannelFrequency(channel, band);
cfgNonHT = wlanNonHTConfig(PSDULength=mpduLength);
osf = 1;
tbtt = 1e-3;
txWaveform = wlanWaveformGenerator(mpduBits, cfgNonHT, OversamplingFactor=osf, NumPackets=5, IdleTime=tbtt);
Rs = wlanSampleRate(cfgNonHT,OversamplingFactor=osf);
if saveToFile
iq = txWaveform;
% store to pass filepath to analyzeTrace
save("traces/wlan_fahren_wir_noch_beacons.mat", "iq");
end
% parameters from task1.m
generatedTrace = analyzeTrace("traces/wlan_fahren_wir_noch_beacons.mat", channel, window, overlap, fft_precision, Rs);
generated_p_dB_min = prctile(generatedTrace.P_dB(:), 1);
generated_p_dB_max = prctile(generatedTrace.P_dB(:), 99.9);
plotSpectrogram(generatedTrace, 5, generated_p_dB_min, generated_p_dB_max);
%{
The first thing I noticed is that most of the spectrogram is shown in the
lowest color range. This is expected, because the generated waveform is a
synthetic signal and does not include receiver noise, interference, other
WiFi transmissions, or non-WiFi signals.
The only relevant signal components are the generated beacon frames. Between
the beacons, the waveform contains idle time, so the power is close to zero
and therefore appears at the lowest dB level in the spectrogram.
Over time, the spectrum only changes when a beacon frame is transmitted.
During these short intervals, the OFDM signal occupies the 20 MHz channel.
Between the beacon frames, there is no meaningful signal energy. Since the
generation is done under ideal conditions, the repeated beacons look very
similar and there are no random channel effects, noise floor variations, or
additional transmissions as in the recorded SDR traces.
For visualization, the idle time was reduced to 1 ms and NumPackets was set
to 5, so that multiple beacon frames are visible close to each other in the
spectrogram. This does not represent the normal beacon interval of 100 TU
(about 102.4 ms), but makes the generated frames easier to compare visually.
%}
+84
View File
@@ -0,0 +1,84 @@
# WiFi Beacon Analysis Report
**Timo Niemann**
> The code also contains the complete implementation and detailed explanations to the tasks.
## Goal
The goal of this lab was to analyze recorded WiFi signals in the 2.4 GHz band. For this, four SDR traces on channels 1, 5, 9, and 13 were evaluated. The first part focused on the visible activity in the spectrum. After that, beacon frames were decoded, the address fields were examined, and possible signs of virtualized access points were checked. Finally, a custom beacon frame was generated and viewed again as a spectrogram.
## SDR Trace Analysis
The traces contain complex IQ samples with a sample rate of 20 MS/s. For the spectrograms, the parameters from the task description were used:
| Parameter | Value |
|---|---:|
| FFT length | `2048` |
| Window length | `512` |
| Overlap | `64` |
| Sample Rate | `20 MS/s` |
| Trace duration | `0.25 s` |
The color scale was calibrated equally for all four spectrograms so that the channels can be compared fairly. The noise floor was estimated using the 10th percentile of the summed channel power. The occupancy was then calculated as the share of time windows whose power was more than 10 dB above this noise floor. Since the traces are not calibrated to an absolute receive power in dBm, the reported noise floor values are relative dB values derived from the summed spectrogram power. Therefore, they are useful for comparing the traces internally, but they should not be interpreted as absolute RF power levels.
| Channel | Center Frequency | Noise Floor | Occupancy |
|:---:|:---:|:---:|:---:|
| 1 | 2412 MHz | -11.75 dB | 2.4 % |
| 5 | 2432 MHz | -13.25 dB | 11.23 % |
| 9 | 2452 MHz | -13.1 dB | 3.36 % |
| 13 | 2472 MHz | -11.1 dB | 2.23 % |
The busiest channel was channel 5.
## Decoded Beacon Frames
With the adapted MATLAB example, beacon frames were detected in the traces and decoded with `wlanMPDUDecode`. Trace 1 was especially noticeable because several networks with very similar BSSIDs were found, for example:
| SSID | BSSID | Vendor | Time in Record |
|---|---|---|---:|
| TUB-IoT | `5C:E1:76:66:02:E2` | Cisco | 40.1149ms |
| eduroam | `5C:E1:76:66:02:E3` | Cisco | 40.3019ms |
| TUB-Guest | `5C:E1:76:66:02:E4` | Cisco | 40.4586ms |
| _Free_Wifi_Berlin | `5C:E1:76:66:02:E7` | Cisco | 40.6286ms |
The first five bytes of the BSSIDs are identical, and only the last byte is different. This strongly suggests the use of multiple virtual BSSIDs on the same physical access point. Similar SNR values would further support this assumption, but they are not enough as proof on their own, because several real APs can also be placed very close to each other. The packets are close in time to each other, this suites an internal in code iteration over the to-be-announced-beacons for one AP also highly suggesting these beacons came from one physical device.
![Tasks example spectrogram](ex_spec.png)
Rectangle 1 and 2 are likely WiFi data frames, in the analysis of 2513mhz.mat I've came across an equal looking pattern at 18.4303ms in frame which was decoded as data frame, also they occupy a similar frequency range and show a similar time-frequency pattern, so I assume these as WiFi packets. Rectangle 3 is likely not a WiFi frame because it is firstly not in the frequency range of the other 2 likely data frames and also is much shorter. Short Packages in WiFi would be beacons or ACKs but a beacon is transmitted over the whole frequency band and an ACK is not sent out of the blue, meaning it follows forgoing data, which this marked capture does not seem to have. I classify rectangle 4 also as a non-WiFi frame, it has a relative similar frametime as the likely data frames beside, but the frequency band is much smaler than the repeating data frames.
The address usage in beacon frames is as follows:
- Address 1: used to announce the destination station, in a beacon frame its FF:FF:FF:FF:FF:FF (meaning broadcast to all stations)
- Address 2: used to remark the source device address of the frame, the ap mac who sent the beacon is inserted
- Address 3: used for the bssid of the device sending the beacon, means usually address 2 equals address 3, when no virtual ap is used
- Address 4: this field does not exists, after the seq ctrl the frame body follows containing beacon data instead of an address, like timestamp, beacon interval, capability info, ssid, potentially the channel number, see the extracts from 2.1
For normal data frames, the meaning of the addresses depends on the direction:
- Downlink: AP -> Station
- Address 1: MAC of the destination station
- Address 2: MAC of the AP which transmits the frame
- Address 3: MAC of the initial sender which has started the frame
- Address 4: not used
- Uplink: Station -> AP
- Address 1: MAC of the AP that receives and potentially forwards the frame
- Address 2: MAC of the sending station
- Address 3: MAC of the final destination station
- Address 4: not used
The fourth address is only used in special cases, for example:
- 4 Address case: wireless bridge:
- Address 1: MAC of the AP that should receive / transmit through the frame, for example AP, bridge node, repeater or mesh node
- Address 2: MAC of the transmitter (into the other network)
- Address 3: MAC of the final destination
- Address 4: MAC of the initial sender
## Generated Beacon
In the last part, a custom beacon frame with the SSID `wlan fahren wir noch` was generated. The beacon was configured for channel 1 in the 2.4 GHz band and uses a beacon interval of `100 TU`. For the spectrogram view, five beacon frames were generated. For visualization, the idle time was reduced to `1 ms`, so that multiple beacon frames are visible within the short spectrogram window.
In the spectrogram, only short and repeated signal regions are visible. Between them, there is almost no power because the signal was generated synthetically and does not contain real receiver noise, interference, or other WLAN frames. The beacon bursts look very similar because they were generated under ideal conditions.
## Conclusion
The analysis shows that the four recorded channels were used with different intensity. Channel 5 was the most active one in this recording, while the other channels only contained short activity phases. By decoding the beacon frames, several SSIDs and their BSSIDs could be identified. The very similar BSSIDs in Trace 1 strongly suggest the presence of a virtualized access point configuration. The generated beacon also confirms how beacon frames appear in a spectrogram as short, repeated transmissions.
Binary file not shown.