Skip to content

Commit c310def

Browse files
committed
Add Darwin Wi-Fi metrics module using CoreWLAN
Implements a pure C API for querying Wi-Fi metrics on macOS using CoreWLAN framework. This provides RSSI, noise, link speed, and computed max link speed without relying on external commands like wdutil or airport. New files: - lltd_wifi_corewlan.h: Pure C89 header with lltd_wifi_metrics_t struct - lltd_wifi_corewlan.m: Objective-C shim using CoreWLAN - lltd_wifi_rates.c/h: Rate calculation tables for HT/VHT/HE PHY modes - tools/lltd-wifi-metrics.c: CLI tool for testing/verification Features: - Queries RSSI, noise, PHY mode, channel width via CoreWLAN - Computes theoretical max link speed based on PHY/width/NSS - Infers NSS/MCS/GI from observed link rate using rate tables - Supports 802.11a/b/g/n/ac/ax with proper rate calculations - Handles missing selectors gracefully for SDK compatibility The module is Darwin-only and guarded with __APPLE__. No Objective-C headers leak into the cross-platform C codebase.
1 parent e35ad88 commit c310def

6 files changed

Lines changed: 1009 additions & 0 deletions

File tree

lltdDaemon.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
C1A0D7D618E01E0900863EA1 /* lltd_wifi_corewlan.m in Sources */ = {isa = PBXBuildFile; fileRef = C1A0D7D518E01E0900863EA1 /* lltd_wifi_corewlan.m */; };
11+
C1A0D7D618E01E0900863EA2 /* lltd_wifi_rates.c in Sources */ = {isa = PBXBuildFile; fileRef = C1A0D7D518E01E0900863EA2 /* lltd_wifi_rates.c */; };
12+
C1A0D7D618E01E0900863EA5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1A0D7D518E01E0900863EA5 /* Foundation.framework */; };
1013
E500D7D618E01E0900863EAC /* darwin-ops.c in Sources */ = {isa = PBXBuildFile; fileRef = E500D7D518E01E0900863EAC /* darwin-ops.c */; };
1114
E500D7DD18E02F3900863EAC /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E500D7DC18E02F3900863EAC /* ImageIO.framework */; };
1215
E500D7E318E0427200863EAC /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E500D7E218E0427200863EAC /* CoreServices.framework */; };
@@ -53,6 +56,11 @@
5356
/* End PBXCopyFilesBuildPhase section */
5457

5558
/* Begin PBXFileReference section */
59+
C1A0D7D518E01E0900863EA1 /* lltd_wifi_corewlan.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "lltd_wifi_corewlan.m"; path = "lltdDaemon/darwin/lltd_wifi_corewlan.m"; sourceTree = SOURCE_ROOT; };
60+
C1A0D7D518E01E0900863EA2 /* lltd_wifi_rates.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "lltd_wifi_rates.c"; path = "lltdDaemon/darwin/lltd_wifi_rates.c"; sourceTree = SOURCE_ROOT; };
61+
C1A0D7D518E01E0900863EA3 /* lltd_wifi_corewlan.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "lltd_wifi_corewlan.h"; path = "lltdDaemon/darwin/lltd_wifi_corewlan.h"; sourceTree = SOURCE_ROOT; };
62+
C1A0D7D518E01E0900863EA4 /* lltd_wifi_rates.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "lltd_wifi_rates.h"; path = "lltdDaemon/darwin/lltd_wifi_rates.h"; sourceTree = SOURCE_ROOT; };
63+
C1A0D7D518E01E0900863EA5 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
5664
E500D7D518E01E0900863EAC /* darwin-ops.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "darwin-ops.c"; path = "lltdDaemon/darwin/darwin-ops.c"; sourceTree = SOURCE_ROOT; };
5765
E500D7D718E01E1800863EAC /* darwin-ops.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "darwin-ops.h"; path = "lltdDaemon/darwin/darwin-ops.h"; sourceTree = SOURCE_ROOT; };
5866
E500D7DC18E02F3900863EAC /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; };
@@ -85,6 +93,7 @@
8593
isa = PBXFrameworksBuildPhase;
8694
buildActionMask = 2147483647;
8795
files = (
96+
C1A0D7D618E01E0900863EA5 /* Foundation.framework in Frameworks */,
8897
E59ADBBE1C80982300895932 /* SystemConfiguration.framework in Frameworks */,
8998
E500D7E318E0427200863EAC /* CoreServices.framework in Frameworks */,
9099
E500D7DD18E02F3900863EAC /* ImageIO.framework in Frameworks */,
@@ -130,6 +139,7 @@
130139
E52240FA18CF0CA200DD9E2F /* Frameworks */ = {
131140
isa = PBXGroup;
132141
children = (
142+
C1A0D7D518E01E0900863EA5 /* Foundation.framework */,
133143
E59ADBBD1C80982300895932 /* SystemConfiguration.framework */,
134144
E500D7E218E0427200863EAC /* CoreServices.framework */,
135145
E500D7DC18E02F3900863EAC /* ImageIO.framework */,
@@ -150,6 +160,10 @@
150160
E5774FA718D61F5E00988358 /* darwin-main.h */,
151161
E500D7D518E01E0900863EAC /* darwin-ops.c */,
152162
E500D7D718E01E1800863EAC /* darwin-ops.h */,
163+
C1A0D7D518E01E0900863EA1 /* lltd_wifi_corewlan.m */,
164+
C1A0D7D518E01E0900863EA3 /* lltd_wifi_corewlan.h */,
165+
C1A0D7D518E01E0900863EA2 /* lltd_wifi_rates.c */,
166+
C1A0D7D518E01E0900863EA4 /* lltd_wifi_rates.h */,
153167
E53E69BE1908293B0033F651 /* lltdBlock.c */,
154168
E53E69C0190829540033F651 /* lltdBlock.h */,
155169
E55B08981C6A3C930020A136 /* lltdDaemon.h */,
@@ -232,6 +246,8 @@
232246
files = (
233247
E5A41A0E19082C0B00D5D2FD /* darwin-main.c in Sources */,
234248
E500D7D618E01E0900863EAC /* darwin-ops.c in Sources */,
249+
C1A0D7D618E01E0900863EA1 /* lltd_wifi_corewlan.m in Sources */,
250+
C1A0D7D618E01E0900863EA2 /* lltd_wifi_rates.c in Sources */,
235251
FAD3A21C19EACF54000FB445 /* lltdAutomata.c in Sources */,
236252
E53E69BF1908293B0033F651 /* lltdBlock.c in Sources */,
237253
E5FD8AE0192A130900E4EA22 /* lltdTlvOps.c in Sources */,
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/******************************************************************************
2+
* *
3+
* lltd_wifi_corewlan.h *
4+
* lltdDaemon *
5+
* *
6+
* Pure C API for Darwin Wi-Fi metrics via CoreWLAN. *
7+
* This header is safe to include from C89 code. *
8+
* *
9+
* Created by Răzvan Corneliu C.R. VILT on 07.01.2026. *
10+
* Copyright © 2014-2026 Răzvan Corneliu C.R. VILT. All rights reserved. *
11+
* *
12+
******************************************************************************/
13+
14+
#ifndef LLTD_WIFI_COREWLAN_H
15+
#define LLTD_WIFI_COREWLAN_H
16+
17+
#ifdef __APPLE__
18+
19+
#ifdef __cplusplus
20+
extern "C" {
21+
#endif
22+
23+
/*
24+
* Flags bitmask for lltd_wifi_metrics_t.flags
25+
*/
26+
#define LLTD_WIFI_FLAG_VALID_RSSI (1 << 0)
27+
#define LLTD_WIFI_FLAG_VALID_NOISE (1 << 1)
28+
#define LLTD_WIFI_FLAG_VALID_LINK (1 << 2)
29+
#define LLTD_WIFI_FLAG_VALID_MAX (1 << 3)
30+
#define LLTD_WIFI_FLAG_APPROX_MAX (1 << 4) /* Max is approximate (NSS inferred) */
31+
#define LLTD_WIFI_FLAG_VALID_PHY (1 << 5)
32+
#define LLTD_WIFI_FLAG_VALID_WIDTH (1 << 6)
33+
#define LLTD_WIFI_FLAG_VALID_INFER (1 << 7) /* NSS/MCS/GI were successfully inferred */
34+
35+
/*
36+
* PHY mode constants (matching CWPHYMode values)
37+
*/
38+
#define LLTD_WIFI_PHY_NONE 0
39+
#define LLTD_WIFI_PHY_11A 1
40+
#define LLTD_WIFI_PHY_11B 2
41+
#define LLTD_WIFI_PHY_11G 3
42+
#define LLTD_WIFI_PHY_11N 4
43+
#define LLTD_WIFI_PHY_11AC 5
44+
#define LLTD_WIFI_PHY_11AX 6
45+
46+
/*
47+
* Wi-Fi metrics structure
48+
*
49+
* All rate values are in Mbps (integer).
50+
* RSSI/noise are in dBm.
51+
*/
52+
typedef struct lltd_wifi_metrics_s {
53+
int rssi_dbm; /* e.g., -42 */
54+
int noise_dbm; /* optional if available, else 0 */
55+
int link_mbps; /* current tx rate Mbps (integer) */
56+
int max_link_mbps; /* computed theoretical max Mbps */
57+
int phy_mode; /* numeric CWPHYMode value for logging */
58+
int chan_width_mhz; /* 20/40/80/160 (0 if unknown) */
59+
int nss_inferred; /* inferred spatial streams count (0 if unknown) */
60+
int mcs_inferred; /* inferred MCS (0 if unknown) */
61+
int gi_ns_inferred; /* inferred GI in ns (0 if unknown) */
62+
int flags; /* bitmask of LLTD_WIFI_FLAG_* values */
63+
} lltd_wifi_metrics_t;
64+
65+
/*
66+
* Query Wi-Fi metrics for the given interface.
67+
*
68+
* Parameters:
69+
* ifname - BSD interface name (e.g., "en0")
70+
* out - pointer to metrics structure (will be zero-initialized)
71+
*
72+
* Returns:
73+
* 0 - success (check flags for which fields are valid)
74+
* -1 - invalid arguments
75+
* -2 - interface not found
76+
* -3 - not Wi-Fi / not associated / link speed unavailable
77+
*/
78+
int lltd_darwin_wifi_get_metrics(const char *ifname, lltd_wifi_metrics_t *out);
79+
80+
/*
81+
* Get a human-readable PHY mode name.
82+
*
83+
* Parameters:
84+
* phy_mode - one of LLTD_WIFI_PHY_* constants
85+
*
86+
* Returns:
87+
* Static string (e.g., "802.11ax", "802.11ac", etc.)
88+
*/
89+
const char *lltd_wifi_phy_mode_name(int phy_mode);
90+
91+
#ifdef __cplusplus
92+
}
93+
#endif
94+
95+
#endif /* __APPLE__ */
96+
97+
#endif /* LLTD_WIFI_COREWLAN_H */
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/******************************************************************************
2+
* *
3+
* lltd_wifi_corewlan.m *
4+
* lltdDaemon *
5+
* *
6+
* Objective-C implementation of Wi-Fi metrics using CoreWLAN. *
7+
* Exposes a pure C API for use from C code. *
8+
* *
9+
* Created by Răzvan Corneliu C.R. VILT on 07.01.2026. *
10+
* Copyright © 2014-2026 Răzvan Corneliu C.R. VILT. All rights reserved. *
11+
* *
12+
******************************************************************************/
13+
14+
#import <Foundation/Foundation.h>
15+
#import <CoreWLAN/CoreWLAN.h>
16+
17+
#include "lltd_wifi_corewlan.h"
18+
#include "lltd_wifi_rates.h"
19+
#include <string.h>
20+
21+
/*
22+
* Map CWChannelWidth to MHz
23+
*/
24+
static int channelWidthToMHz(CWChannelWidth width) {
25+
switch (width) {
26+
case kCWChannelWidth20MHz: return 20;
27+
case kCWChannelWidth40MHz: return 40;
28+
case kCWChannelWidth80MHz: return 80;
29+
case kCWChannelWidth160MHz: return 160;
30+
default: return 0;
31+
}
32+
}
33+
34+
/*
35+
* Map CWPHYMode to our PHY mode constants
36+
*/
37+
static int phyModeToConstant(CWPHYMode mode) {
38+
switch (mode) {
39+
case kCWPHYMode11a: return LLTD_WIFI_PHY_11A;
40+
case kCWPHYMode11b: return LLTD_WIFI_PHY_11B;
41+
case kCWPHYMode11g: return LLTD_WIFI_PHY_11G;
42+
case kCWPHYMode11n: return LLTD_WIFI_PHY_11N;
43+
case kCWPHYMode11ac: return LLTD_WIFI_PHY_11AC;
44+
default: break;
45+
}
46+
47+
/* Check for 802.11ax (Wi-Fi 6) - may not be defined in older SDKs */
48+
#if defined(kCWPHYMode11ax)
49+
if (mode == kCWPHYMode11ax) {
50+
return LLTD_WIFI_PHY_11AX;
51+
}
52+
#endif
53+
54+
/* For newer PHY modes on older SDKs, check numeric value */
55+
/* kCWPHYMode11ax = 6 on macOS 11+ */
56+
if ((int)mode == 6) {
57+
return LLTD_WIFI_PHY_11AX;
58+
}
59+
60+
return LLTD_WIFI_PHY_NONE;
61+
}
62+
63+
/*
64+
* Get the transmit rate from CWInterface.
65+
* Tries multiple selectors for compatibility across macOS versions.
66+
*/
67+
static double getTransmitRate(CWInterface *iface) {
68+
double rate = 0.0;
69+
70+
/* Try transmitRate first (modern API) */
71+
if ([iface respondsToSelector:@selector(transmitRate)]) {
72+
rate = [iface transmitRate];
73+
if (rate > 0) return rate;
74+
}
75+
76+
/* Try txRate (if it exists as alternate name) */
77+
SEL txRateSel = NSSelectorFromString(@"txRate");
78+
if ([iface respondsToSelector:txRateSel]) {
79+
NSMethodSignature *sig = [iface methodSignatureForSelector:txRateSel];
80+
if (sig && strcmp([sig methodReturnType], "d") == 0) {
81+
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
82+
[inv setSelector:txRateSel];
83+
[inv invokeWithTarget:iface];
84+
[inv getReturnValue:&rate];
85+
if (rate > 0) return rate;
86+
}
87+
}
88+
89+
return rate;
90+
}
91+
92+
const char *lltd_wifi_phy_mode_name(int phy_mode) {
93+
switch (phy_mode) {
94+
case LLTD_WIFI_PHY_NONE: return "none";
95+
case LLTD_WIFI_PHY_11A: return "802.11a";
96+
case LLTD_WIFI_PHY_11B: return "802.11b";
97+
case LLTD_WIFI_PHY_11G: return "802.11g";
98+
case LLTD_WIFI_PHY_11N: return "802.11n";
99+
case LLTD_WIFI_PHY_11AC: return "802.11ac";
100+
case LLTD_WIFI_PHY_11AX: return "802.11ax";
101+
default: return "unknown";
102+
}
103+
}
104+
105+
int lltd_darwin_wifi_get_metrics(const char *ifname, lltd_wifi_metrics_t *out) {
106+
@autoreleasepool {
107+
/* Validate arguments */
108+
if (!ifname || !out) {
109+
return -1;
110+
}
111+
112+
/* Zero-initialize output */
113+
memset(out, 0, sizeof(lltd_wifi_metrics_t));
114+
115+
/* Get the CWWiFiClient */
116+
CWWiFiClient *client = [CWWiFiClient sharedWiFiClient];
117+
if (!client) {
118+
return -2;
119+
}
120+
121+
/* Get the interface by name */
122+
NSString *ifnameStr = [NSString stringWithUTF8String:ifname];
123+
CWInterface *iface = [client interfaceWithName:ifnameStr];
124+
if (!iface) {
125+
return -2;
126+
}
127+
128+
/* Check if we're associated to a network */
129+
if (![iface ssid]) {
130+
return -3;
131+
}
132+
133+
/* Get RSSI */
134+
if ([iface respondsToSelector:@selector(rssiValue)]) {
135+
out->rssi_dbm = (int)[iface rssiValue];
136+
out->flags |= LLTD_WIFI_FLAG_VALID_RSSI;
137+
}
138+
139+
/* Get Noise */
140+
if ([iface respondsToSelector:@selector(noiseMeasurement)]) {
141+
out->noise_dbm = (int)[iface noiseMeasurement];
142+
out->flags |= LLTD_WIFI_FLAG_VALID_NOISE;
143+
}
144+
145+
/* Get PHY mode */
146+
CWPHYMode phyMode = kCWPHYModeNone;
147+
if ([iface respondsToSelector:@selector(activePHYMode)]) {
148+
phyMode = [iface activePHYMode];
149+
out->phy_mode = phyModeToConstant(phyMode);
150+
if (out->phy_mode != LLTD_WIFI_PHY_NONE) {
151+
out->flags |= LLTD_WIFI_FLAG_VALID_PHY;
152+
}
153+
}
154+
155+
/* Get channel width */
156+
CWChannel *channel = [iface wlanChannel];
157+
if (channel) {
158+
out->chan_width_mhz = channelWidthToMHz([channel channelWidth]);
159+
if (out->chan_width_mhz > 0) {
160+
out->flags |= LLTD_WIFI_FLAG_VALID_WIDTH;
161+
}
162+
}
163+
164+
/* Get current link speed (transmit rate) */
165+
double txRate = getTransmitRate(iface);
166+
if (txRate > 0) {
167+
out->link_mbps = (int)(txRate + 0.5); /* Round to nearest integer */
168+
out->flags |= LLTD_WIFI_FLAG_VALID_LINK;
169+
} else {
170+
/* Cannot proceed without link rate */
171+
return -3;
172+
}
173+
174+
/* Now compute max link speed */
175+
int phy_family = lltd_wifi_phy_family_from_cwmode(out->phy_mode);
176+
int width = out->chan_width_mhz > 0 ? out->chan_width_mhz : 20;
177+
178+
if (phy_family == WIFI_PHY_LEGACY_B) {
179+
/* 802.11b max is 11 Mbps */
180+
out->max_link_mbps = 11;
181+
out->flags |= LLTD_WIFI_FLAG_VALID_MAX;
182+
} else if (phy_family == WIFI_PHY_LEGACY_AG) {
183+
/* 802.11a/g max is 54 Mbps */
184+
out->max_link_mbps = 54;
185+
out->flags |= LLTD_WIFI_FLAG_VALID_MAX;
186+
} else if (phy_family != 0) {
187+
/* HT/VHT/HE: try to infer NSS/MCS/GI from observed rate */
188+
wifi_rate_params_t inferred;
189+
int tolerance = 15; /* Allow 15 Mbps tolerance for rate matching */
190+
191+
if (lltd_wifi_infer_params(phy_family, width, out->link_mbps, tolerance, &inferred) == 0) {
192+
/* Successfully inferred parameters */
193+
out->nss_inferred = inferred.nss;
194+
out->mcs_inferred = inferred.mcs;
195+
out->gi_ns_inferred = inferred.gi_ns;
196+
out->flags |= LLTD_WIFI_FLAG_VALID_INFER;
197+
198+
/* Calculate max rate using inferred NSS */
199+
out->max_link_mbps = lltd_wifi_calc_max_rate(phy_family, width, inferred.nss);
200+
out->flags |= LLTD_WIFI_FLAG_VALID_MAX;
201+
} else {
202+
/* Could not infer, assume 1 SS and mark as approximate */
203+
out->nss_inferred = 1;
204+
out->max_link_mbps = lltd_wifi_calc_max_rate(phy_family, width, 1);
205+
out->flags |= LLTD_WIFI_FLAG_VALID_MAX | LLTD_WIFI_FLAG_APPROX_MAX;
206+
}
207+
} else {
208+
/* Unknown PHY, use link rate as max */
209+
out->max_link_mbps = out->link_mbps;
210+
out->flags |= LLTD_WIFI_FLAG_VALID_MAX | LLTD_WIFI_FLAG_APPROX_MAX;
211+
}
212+
213+
return 0;
214+
}
215+
}

0 commit comments

Comments
 (0)