|
| 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