diff --git a/client_linux.go b/client_linux.go index 9be6a41..251b8c9 100644 --- a/client_linux.go +++ b/client_linux.go @@ -8,6 +8,7 @@ import ( "crypto/sha1" "encoding/binary" "errors" + "fmt" "net" "os" "sync" @@ -750,8 +751,10 @@ func (info *StationInfo) parseAttributes(attrs []netlink.Attribute) error { switch a.Type { case unix.NL80211_STA_INFO_RX_BITRATE: info.ReceiveBitrate = rate.Bitrate + info.ReceiveRateInfo = *rate case unix.NL80211_STA_INFO_TX_BITRATE: info.TransmitBitrate = rate.Bitrate + info.TransmitRateInfo = *rate } } @@ -769,40 +772,159 @@ func (info *StationInfo) parseAttributes(attrs []netlink.Attribute) error { return nil } -// rateInfo provides statistics about the receive or transmit rate of -// an interface. -type rateInfo struct { - // Bitrate in bits per second. - Bitrate int +func bitrateStr(bitrate int) string { + if bitrate > 0 { + return fmt.Sprintf("%d.%d MBit/s ", bitrate/10, bitrate%10) + } + return "(unknown)" } // parseRateInfo parses a rateInfo from netlink attributes. -func parseRateInfo(b []byte) (*rateInfo, error) { +func parseRateInfo(b []byte) (*RateInfo, error) { attrs, err := netlink.UnmarshalAttributes(b) if err != nil { return nil, err } - var info rateInfo + var rateinfo RateInfo + // initialize with unknown values + htModulationInfo := HTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: -1, NSS: -1}} + vhtModulationInfo := VHTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: -1, NSS: -1}} + heModulationInfo := HEModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: -1, NSS: -1}} + ehtModulationInfo := EHTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: -1, NSS: -1}} + + // build the string seperately and assign at the end + var iwDescription string + iwDescription = "" + // shortGi := false + + // re-use Channel Width type from Interface, even though we classify via NL80211_RATE_INFO_* + // strings are done manually do keep comaptibility with iw + var channelWidth ChannelWidth + for _, a := range attrs { + // see iw's station.c for reference implementation + // serach for parse_bitrate(struct nlattr *bitrate_attr, char *buf, int buflen) + // at the moment of implementation iw v6.17 was used: + // https://git.kernel.org/pub/scm/linux/kernel/git/jberg/iw.git/tree/station.c?h=v6.17#n199 switch a.Type { case unix.NL80211_RATE_INFO_BITRATE32: - info.Bitrate = int(nlenc.Uint32(a.Data)) + rateinfo.Bitrate = int(nlenc.Uint32(a.Data)) + iwDescription += bitrateStr(rateinfo.Bitrate) + case unix.NL80211_RATE_INFO_BITRATE: + // Only use 16-bit counters if the 32-bit counters are not present. + // If the 32-bit counters appear later in the slice, they will overwrite + // these values. + if rateinfo.Bitrate == 0 { + rateinfo.Bitrate = int(nlenc.Uint16(a.Data)) + iwDescription += bitrateStr(rateinfo.Bitrate) + } + case unix.NL80211_RATE_INFO_MCS: + htModulationInfo.HTMCS = int(nlenc.Uint8(a.Data)) + htModulationInfo.MCS = htModulationInfo.HTMCS % 8 + htModulationInfo.NSS = (htModulationInfo.HTMCS / 8) + 1 + iwDescription += fmt.Sprintf(" MCS %d", htModulationInfo.HTMCS) + case unix.NL80211_RATE_INFO_VHT_MCS: + vhtModulationInfo.MCS = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" VHT-MCS %d", vhtModulationInfo.MCS) + case unix.NL80211_RATE_INFO_40_MHZ_WIDTH: + channelWidth = ChannelWidth40 + iwDescription += " 40MHz" + case unix.NL80211_RATE_INFO_80_MHZ_WIDTH: + channelWidth = ChannelWidth80 + iwDescription += " 80MHz" + case unix.NL80211_RATE_INFO_80P80_MHZ_WIDTH: + channelWidth = ChannelWidth80P80 + iwDescription += " 80P80MHz" + case unix.NL80211_RATE_INFO_160_MHZ_WIDTH: + channelWidth = ChannelWidth160 + iwDescription += " 160MHz" + case unix.NL80211_RATE_INFO_320_MHZ_WIDTH: + channelWidth = ChannelWidth320 + iwDescription += " 320MHz" + case unix.NL80211_RATE_INFO_1_MHZ_WIDTH: + channelWidth = ChannelWidth1 + iwDescription += " 1MHz" + case unix.NL80211_RATE_INFO_2_MHZ_WIDTH: + channelWidth = ChannelWidth2 + iwDescription += " 2MHz" + case unix.NL80211_RATE_INFO_4_MHZ_WIDTH: + channelWidth = ChannelWidth4 + iwDescription += " 4MHz" + case unix.NL80211_RATE_INFO_8_MHZ_WIDTH: + channelWidth = ChannelWidth8 + iwDescription += " 8MHz" + case unix.NL80211_RATE_INFO_16_MHZ_WIDTH: + channelWidth = ChannelWidth16 + iwDescription += " 16MHz" + case unix.NL80211_RATE_INFO_SHORT_GI: + htModulationInfo.ShortGI = true + vhtModulationInfo.ShortGI = true + iwDescription += " Short GI" + case unix.NL80211_RATE_INFO_VHT_NSS: + vhtModulationInfo.NSS = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" VHT-NSS %d", vhtModulationInfo.NSS) + case unix.NL80211_RATE_INFO_HE_MCS: + heModulationInfo.MCS = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" HE-MCS %d", heModulationInfo.MCS) + case unix.NL80211_RATE_INFO_HE_NSS: + heModulationInfo.NSS = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" HE-NSS %d", heModulationInfo.NSS) + case unix.NL80211_RATE_INFO_HE_GI: + heModulationInfo.GI = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" HE-GI %d", heModulationInfo.GI) + case unix.NL80211_RATE_INFO_HE_DCM: + heModulationInfo.DCM = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" HE-DCM %d", heModulationInfo.DCM) + case unix.NL80211_RATE_INFO_HE_RU_ALLOC: + heModulationInfo.RUAlloc = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" HE-RU-ALLOC %d", heModulationInfo.RUAlloc) + case unix.NL80211_RATE_INFO_EHT_MCS: + ehtModulationInfo.MCS = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" EHT-MCS %d", ehtModulationInfo.MCS) + case unix.NL80211_RATE_INFO_EHT_NSS: + ehtModulationInfo.NSS = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" EHT-NSS %d", ehtModulationInfo.NSS) + case unix.NL80211_RATE_INFO_EHT_GI: + ehtModulationInfo.GI = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" EHT-GI %d", ehtModulationInfo.GI) + case unix.NL80211_RATE_INFO_EHT_RU_ALLOC: + ehtModulationInfo.RUAlloc = int(nlenc.Uint8(a.Data)) + iwDescription += fmt.Sprintf(" EHT-RU-ALLOC %d", ehtModulationInfo.RUAlloc) } + } - // Only use 16-bit counters if the 32-bit counters are not present. - // If the 32-bit counters appear later in the slice, they will overwrite - // these values. - if info.Bitrate == 0 && a.Type == unix.NL80211_RATE_INFO_BITRATE { - info.Bitrate = int(nlenc.Uint16(a.Data)) - } + // Assign modulation info based on what was found + // highest WiFi standard with valid MCS found determines modulation type + switch { + case ehtModulationInfo.MCS != -1: + rateinfo.ModulationType = RateModulationInfoTypeEHT + ehtModulationInfo.IwDescription = iwDescription + rateinfo.Modulation = ehtModulationInfo + case heModulationInfo.MCS != -1: + rateinfo.ModulationType = RateModulationInfoTypeHE + heModulationInfo.IwDescription = iwDescription + rateinfo.Modulation = heModulationInfo + case vhtModulationInfo.MCS != -1: + rateinfo.ModulationType = RateModulationInfoTypeVHT + vhtModulationInfo.IwDescription = iwDescription + rateinfo.Modulation = vhtModulationInfo + case htModulationInfo.MCS != -1: + rateinfo.ModulationType = RateModulationInfoTypeHT + htModulationInfo.IwDescription = iwDescription + rateinfo.Modulation = htModulationInfo + default: + rateinfo.ModulationType = RateModulationInfoTypeUNKNOWN + rateinfo.Modulation = nil } + rateinfo.ChannelWidth = channelWidth + // Scale bitrate to bits/second as base unit instead of 100kbits/second. // * @NL80211_RATE_INFO_BITRATE: total bitrate (u16, 100kbit/s) - info.Bitrate *= 100 * 1000 + rateinfo.Bitrate *= 100 * 1000 - return &info, nil + return &rateinfo, nil } // parseSurveyInfo parses a single SurveyInfo from a byte slice of netlink diff --git a/client_linux_test.go b/client_linux_test.go index b2c83a9..79cca3f 100644 --- a/client_linux_test.go +++ b/client_linux_test.go @@ -10,6 +10,7 @@ import ( "net" "os" "reflect" + "slices" "syscall" "testing" "time" @@ -288,6 +289,8 @@ func TestLinux_clientStationInfoOK(t *testing.T) { BeaconLoss: 3, ReceiveBitrate: 130000000, TransmitBitrate: 130000000, + ReceiveRateInfo: RateInfo{Bitrate: 130000000, ModulationType: RateModulationInfoTypeVHT, Modulation: VHTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 5, NSS: 2, IwDescription: "130.0 MBit/s 130.0 MBit/s VHT-MCS 5 VHT-NSS 2 Short GI 80P80MHz"}, ShortGI: true}, ChannelWidth: ChannelWidth80P80}, + TransmitRateInfo: RateInfo{Bitrate: 130000000, ModulationType: RateModulationInfoTypeVHT, Modulation: VHTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 3, NSS: 1, IwDescription: "130.0 MBit/s 130.0 MBit/s VHT-MCS 3 VHT-NSS 1 Short GI 40MHz"}, ShortGI: true}, ChannelWidth: ChannelWidth40}, }, { InterfaceIndex: 1, @@ -304,7 +307,85 @@ func TestLinux_clientStationInfoOK(t *testing.T) { TransmitFailed: 4, BeaconLoss: 6, ReceiveBitrate: 260000000, - TransmitBitrate: 260000000, + TransmitBitrate: 240000000, + ReceiveRateInfo: RateInfo{Bitrate: 260000000, ModulationType: RateModulationInfoTypeVHT, Modulation: VHTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 5, NSS: 2, IwDescription: "260.0 MBit/s 260.0 MBit/s VHT-MCS 5 VHT-NSS 2 Short GI 80MHz"}, ShortGI: true}, ChannelWidth: ChannelWidth80}, + TransmitRateInfo: RateInfo{Bitrate: 240000000, ModulationType: RateModulationInfoTypeVHT, Modulation: VHTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 3, NSS: 1, IwDescription: "240.0 MBit/s 240.0 MBit/s VHT-MCS 3 VHT-NSS 1 160MHz"}, ShortGI: false}, ChannelWidth: ChannelWidth160}, + }, + { + InterfaceIndex: 1, + HardwareAddr: net.HardwareAddr{0x40, 0xa5, 0xef, 0xd9, 0x96, 0x6f}, + Connected: 60 * time.Minute, + Inactive: 8 * time.Millisecond, + ReceivedBytes: 2000, + TransmittedBytes: 4000, + ReceivedPackets: 20, + TransmittedPackets: 40, + Signal: -25, + SignalAverage: -27, + TransmitRetries: 10, + TransmitFailed: 4, + BeaconLoss: 6, + ReceiveBitrate: 260000000, + TransmitBitrate: 240000000, + ReceiveRateInfo: RateInfo{Bitrate: 260000000, ModulationType: RateModulationInfoTypeEHT, Modulation: EHTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 5, NSS: 2, IwDescription: "260.0 MBit/s 260.0 MBit/s EHT-MCS 5 EHT-NSS 2 EHT-GI 22 EHT-RU-ALLOC 33 320MHz"}, GI: 22, RUAlloc: 33}, ChannelWidth: ChannelWidth320}, + TransmitRateInfo: RateInfo{Bitrate: 240000000, ModulationType: RateModulationInfoTypeHE, Modulation: HEModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 3, NSS: 1, IwDescription: "240.0 MBit/s 240.0 MBit/s HE-MCS 3 HE-NSS 1 HE-GI 1 HE-DCM 2 HE-RU-ALLOC 3 160MHz"}, GI: 1, DCM: 2, RUAlloc: 3}, ChannelWidth: ChannelWidth160}, + }, + { + InterfaceIndex: 3, + HardwareAddr: net.HardwareAddr{0x40, 0xa5, 0xef, 0xd9, 0x96, 0x6f}, + Connected: 40 * time.Minute, + Inactive: 5 * time.Millisecond, + ReceivedBytes: 5000, + TransmittedBytes: 2000, + ReceivedPackets: 20, + TransmittedPackets: 40, + Signal: -25, + SignalAverage: -27, + TransmitRetries: 10, + TransmitFailed: 4, + BeaconLoss: 6, + ReceiveBitrate: 260000000, + TransmitBitrate: 240000000, + ReceiveRateInfo: RateInfo{Bitrate: 260000000, ModulationType: RateModulationInfoTypeHT, Modulation: HTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 6, NSS: 2, IwDescription: "260.0 MBit/s 260.0 MBit/s MCS 14 Short GI 16MHz"}, HTMCS: 14, ShortGI: true}, ChannelWidth: ChannelWidth16}, + TransmitRateInfo: RateInfo{Bitrate: 240000000, ModulationType: RateModulationInfoTypeHT, Modulation: HTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 7, NSS: 1, IwDescription: "240.0 MBit/s 240.0 MBit/s MCS 7 4MHz"}, HTMCS: 7, ShortGI: false}, ChannelWidth: ChannelWidth4}, + }, + { + InterfaceIndex: 3, + HardwareAddr: net.HardwareAddr{0x40, 0xa5, 0xef, 0xd9, 0x96, 0x6f}, + Connected: 40 * time.Minute, + Inactive: 5 * time.Millisecond, + ReceivedBytes: 5000, + TransmittedBytes: 2000, + ReceivedPackets: 20, + TransmittedPackets: 40, + Signal: -25, + SignalAverage: -27, + TransmitRetries: 10, + TransmitFailed: 4, + BeaconLoss: 6, + ReceiveBitrate: 260000000, + TransmitBitrate: 240000000, + ReceiveRateInfo: RateInfo{Bitrate: 260000000, ModulationType: RateModulationInfoTypeHT, Modulation: HTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 6, NSS: 2, IwDescription: "260.0 MBit/s 260.0 MBit/s MCS 14 Short GI 1MHz"}, HTMCS: 14, ShortGI: true}, ChannelWidth: ChannelWidth1}, + TransmitRateInfo: RateInfo{Bitrate: 240000000, ModulationType: RateModulationInfoTypeHT, Modulation: HTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 7, NSS: 1, IwDescription: "240.0 MBit/s 240.0 MBit/s MCS 7 2MHz"}, HTMCS: 7, ShortGI: false}, ChannelWidth: ChannelWidth2}, + }, + { + InterfaceIndex: 3, + HardwareAddr: net.HardwareAddr{0x40, 0xa5, 0xef, 0xd9, 0x96, 0x6f}, + Connected: 40 * time.Minute, + Inactive: 5 * time.Millisecond, + ReceivedBytes: 5000, + TransmittedBytes: 2000, + ReceivedPackets: 20, + TransmittedPackets: 40, + Signal: -25, + SignalAverage: -27, + TransmitRetries: 10, + TransmitFailed: 4, + BeaconLoss: 6, + ReceiveBitrate: 260000000, + TransmitBitrate: 240000000, + ReceiveRateInfo: RateInfo{Bitrate: 260000000, ModulationType: RateModulationInfoTypeHT, Modulation: HTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 6, NSS: 2, IwDescription: "260.0 MBit/s 260.0 MBit/s MCS 14 Short GI 8MHz"}, HTMCS: 14, ShortGI: true}, ChannelWidth: ChannelWidth8}, + TransmitRateInfo: RateInfo{Bitrate: 240000000, ModulationType: RateModulationInfoTypeHT, Modulation: HTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: 7, NSS: 1, IwDescription: "240.0 MBit/s 240.0 MBit/s MCS 7 8MHz"}, HTMCS: 7, ShortGI: false}, ChannelWidth: ChannelWidth8}, }, } @@ -488,6 +569,75 @@ func (b *BSS) attributes() []netlink.Attribute { } } +func modulationAttributes(rateInfo RateModulationInfo) (attr []netlink.Attribute) { + // attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_BITRATE, Data: nlenc.Uint16Bytes(uint16(bitrateAttr(s.ReceiveBitrate)))}) + // attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_BITRATE32, Data: nlenc.Uint32Bytes(bitrateAttr(s.ReceiveBitrate))}) + switch ri := rateInfo.(type) { + case BaseModulationInfo: + //ri := rateInfo.(BaseModulationInfo) + // TODO + case HTModulationInfo: + // TODO ??> + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(ri.HTMCS))}) + if ri.ShortGI { + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_SHORT_GI}) + } + case VHTModulationInfo: + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_VHT_MCS, Data: nlenc.Uint8Bytes(uint8(ri.MCS))}) + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_VHT_NSS, Data: nlenc.Uint8Bytes(uint8(ri.NSS))}) + if ri.ShortGI { + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_SHORT_GI}) + } + case HEModulationInfo: + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_HE_MCS, Data: nlenc.Uint8Bytes(uint8(ri.MCS))}) + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_HE_NSS, Data: nlenc.Uint8Bytes(uint8(ri.NSS))}) + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_HE_GI, Data: nlenc.Uint8Bytes(uint8(ri.GI))}) + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_HE_DCM, Data: nlenc.Uint8Bytes(uint8(ri.DCM))}) + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_HE_RU_ALLOC, Data: nlenc.Uint8Bytes(uint8(ri.RUAlloc))}) + case EHTModulationInfo: + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_EHT_MCS, Data: nlenc.Uint8Bytes(uint8(ri.MCS))}) + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_EHT_NSS, Data: nlenc.Uint8Bytes(uint8(ri.NSS))}) + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_EHT_GI, Data: nlenc.Uint8Bytes(uint8(ri.GI))}) + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_EHT_RU_ALLOC, Data: nlenc.Uint8Bytes(uint8(ri.RUAlloc))}) + default: + fmt.Printf("Could not type-switch %v \n", rateInfo) + } + return attr +} + +func channelWithAttributes(cw ChannelWidth) (attr []netlink.Attribute) { + switch cw { + // case ChannelWidth20NoHT: + // case ChannelWidth20: + case ChannelWidth40: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_40_MHZ_WIDTH}} + case ChannelWidth80: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_80_MHZ_WIDTH}} + case ChannelWidth80P80: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_80P80_MHZ_WIDTH}} + case ChannelWidth160: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_160_MHZ_WIDTH}} + case ChannelWidth5: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_5_MHZ_WIDTH}} + case ChannelWidth10: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_10_MHZ_WIDTH}} + case ChannelWidth1: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_1_MHZ_WIDTH}} + case ChannelWidth2: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_2_MHZ_WIDTH}} + case ChannelWidth4: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_4_MHZ_WIDTH}} + case ChannelWidth8: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_8_MHZ_WIDTH}} + case ChannelWidth16: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_16_MHZ_WIDTH}} + case ChannelWidth320: + return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_320_MHZ_WIDTH}} + default: + return attr + } +} + func (s *StationInfo) attributes() []netlink.Attribute { return []netlink.Attribute{ // TODO(mdlayher): return more attributes for validation? @@ -517,17 +667,25 @@ func (s *StationInfo) attributes() []netlink.Attribute { {Type: unix.NL80211_STA_INFO_BEACON_LOSS, Data: nlenc.Uint32Bytes(uint32(s.BeaconLoss))}, { Type: unix.NL80211_STA_INFO_RX_BITRATE, - Data: mustMarshalAttributes([]netlink.Attribute{ - {Type: unix.NL80211_RATE_INFO_BITRATE, Data: nlenc.Uint16Bytes(uint16(bitrateAttr(s.ReceiveBitrate)))}, - {Type: unix.NL80211_RATE_INFO_BITRATE32, Data: nlenc.Uint32Bytes(bitrateAttr(s.ReceiveBitrate))}, - }), + Data: mustMarshalAttributes(slices.Concat( + []netlink.Attribute{ + {Type: unix.NL80211_RATE_INFO_BITRATE, Data: nlenc.Uint16Bytes(uint16(bitrateAttr(s.ReceiveBitrate)))}, + {Type: unix.NL80211_RATE_INFO_BITRATE32, Data: nlenc.Uint32Bytes(bitrateAttr(s.ReceiveBitrate))}, + }, + modulationAttributes(s.ReceiveRateInfo.Modulation), + channelWithAttributes(s.ReceiveRateInfo.ChannelWidth), + )), }, { Type: unix.NL80211_STA_INFO_TX_BITRATE, - Data: mustMarshalAttributes([]netlink.Attribute{ - {Type: unix.NL80211_RATE_INFO_BITRATE, Data: nlenc.Uint16Bytes(uint16(bitrateAttr(s.TransmitBitrate)))}, - {Type: unix.NL80211_RATE_INFO_BITRATE32, Data: nlenc.Uint32Bytes(bitrateAttr(s.TransmitBitrate))}, - }), + Data: mustMarshalAttributes(slices.Concat( + []netlink.Attribute{ + {Type: unix.NL80211_RATE_INFO_BITRATE, Data: nlenc.Uint16Bytes(uint16(bitrateAttr(s.TransmitBitrate)))}, + {Type: unix.NL80211_RATE_INFO_BITRATE32, Data: nlenc.Uint32Bytes(bitrateAttr(s.TransmitBitrate))}, + }, + modulationAttributes(s.TransmitRateInfo.Modulation), + channelWithAttributes(s.TransmitRateInfo.ChannelWidth), + )), }, }), }, diff --git a/wifi.go b/wifi.go index 290b3a3..b6bf741 100644 --- a/wifi.go +++ b/wifi.go @@ -208,6 +208,116 @@ type Interface struct { ChannelWidth ChannelWidth } +type RateModulationInfo interface { + // MCS is the modulation and coding scheme index. + GetMCS() int + + // NSS is the number of spatial streams. + GetNSS() int + + // Description returns a human-readable description of the modulation info. + // Uses same format as iw tool, but not necessary in the same order. + Description() string + + // WifiGeneration returns the WiFi generation (e.g., "802.11n (WiFi 4)", "802.11ac (WiFi 5)", "802.11ax (WiFi 6)", "802.11be (WiFi 7)") + WifiGeneration() string +} + +type BaseModulationInfo struct { + MCS int + NSS int + IwDescription string +} + +func (mi BaseModulationInfo) GetMCS() int { + return mi.MCS +} + +func (mi BaseModulationInfo) GetNSS() int { + return mi.NSS +} + +func (mi BaseModulationInfo) Description() string { + return mi.IwDescription +} + +func (mi BaseModulationInfo) WifiGeneration() string { + return "unknown" +} + +// HTModulationInfo represents modulation information for HT rates. +// MCS Indexes originally range from 0 to 31. NSS is coded in the MCS index as follows: +// NSS = (MCS / 8) + 1 +// MCS = MCS % 8 +// original MCS index is available as HTMCS +type HTModulationInfo struct { + BaseModulationInfo + HTMCS int + ShortGI bool +} + +func (mi HTModulationInfo) WifiGeneration() string { + return "802.11n (WiFi 4)" +} + +// VHTModulationInfo represents modulation information for VHT rates. +type VHTModulationInfo struct { + BaseModulationInfo + ShortGI bool +} + +func (mi VHTModulationInfo) WifiGeneration() string { + return "802.11ac (WiFi 5)" +} + +type HEModulationInfo struct { + BaseModulationInfo + GI int + DCM int + RUAlloc int +} + +func (mi HEModulationInfo) WifiGeneration() string { + return "802.11ax (WiFi 6)" +} + +type EHTModulationInfo struct { + BaseModulationInfo + GI int + RUAlloc int +} + +func (mi EHTModulationInfo) WifiGeneration() string { + return "802.11be (WiFi 7)" +} + +// RateModulationInfoType indicates the type of modulation used for a rate. +type RateModulationInfoType int + +const ( + RateModulationInfoTypeHT RateModulationInfoType = iota + RateModulationInfoTypeVHT + RateModulationInfoTypeHE + RateModulationInfoTypeEHT + RateModulationInfoTypeUNKNOWN +) + +// rateInfo provides information about the receive or transmit rate of +// an interface. +type RateInfo struct { + // Bitrate in bits per second. + Bitrate int + + // The type of modulation used. Can also be inferred from Modulation.(type) + ModulationType RateModulationInfoType + + // Modulation information. + Modulation RateModulationInfo + + // Channel width used for this rate. + ChannelWidth ChannelWidth +} + // StationInfo contains statistics about a WiFi interface operating in // station mode. type StationInfo struct { @@ -255,6 +365,12 @@ type StationInfo struct { // The number of times a beacon loss was detected. BeaconLoss int + + // The current receive rate and detailed modulation information. + ReceiveRateInfo RateInfo + + // The current transmit rate and detailed modulation information. + TransmitRateInfo RateInfo } // BSSLoad is an Information Element containing measurements of the load on the BSS. diff --git a/wifi_test.go b/wifi_test.go index 6635fa2..a7045a8 100644 --- a/wifi_test.go +++ b/wifi_test.go @@ -481,3 +481,25 @@ func TestRSNErrorHierarchy(t *testing.T) { } }) } + +func TestRateInfo_GenerationString(t *testing.T) { + tests := []struct { + name string + modulation RateModulationInfo + want string + }{ + {"Basic", BaseModulationInfo{}, "unknown"}, + {"HT", HTModulationInfo{}, "802.11n (WiFi 4)"}, + {"VHT", VHTModulationInfo{}, "802.11ac (WiFi 5)"}, + {"HE", HEModulationInfo{}, "802.11ax (WiFi 6)"}, + {"EHT", EHTModulationInfo{}, "802.11be (WiFi 7)"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.modulation.WifiGeneration(); got != tt.want { + t.Errorf("RateModulationInfo.WifiGeneration() = %v, want %v", got, tt.want) + } + }) + } +}