From 1b362c48f9af306180b988375ab622a54de74063 Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Wed, 29 Oct 2025 08:22:22 +0100 Subject: [PATCH 01/11] add Basic Rate Info Parsing add basic MCS Decoding test: add additional station info test case with MCS attributes feat: add support for VHT MCS indexes in StationInfo and related parsing logic feat: enhance StationInfo with detailed rate information and VHT support --- client_linux.go | 49 +++++++++++++++++++++++++++++++++++++++++--- client_linux_test.go | 42 +++++++++++++++++++++++++++++++++++++ wifi.go | 18 ++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/client_linux.go b/client_linux.go index 9be6a41..3d3a95b 100644 --- a/client_linux.go +++ b/client_linux.go @@ -750,8 +750,14 @@ func (info *StationInfo) parseAttributes(attrs []netlink.Attribute) error { switch a.Type { case unix.NL80211_STA_INFO_RX_BITRATE: info.ReceiveBitrate = rate.Bitrate + info.RX_MCS = rate.MCS + info.RX_VHT_MCS = rate.VHT_MCS + info.ReceiveRateInfo = *rate case unix.NL80211_STA_INFO_TX_BITRATE: info.TransmitBitrate = rate.Bitrate + info.TX_MCS = rate.MCS + info.TX_VHT_MCS = rate.VHT_MCS + info.TransmitRateInfo = *rate } } @@ -769,25 +775,62 @@ func (info *StationInfo) parseAttributes(attrs []netlink.Attribute) error { return nil } +// type RateModulationInfo interface { +// GetMCS() int +// GetNSS() int +// String() string +// } + +// type htRateInfo struct { +// MCS int +// } + +// func (r htRateInfo) GetMCS() int { +// return r.MCS +// } + +// func (r htRateInfo) GetNSS() int { +// return (r.MCS / 8) + 1 +// } + +// func (r htRateInfo) String() string { +// return fmt.Sprintf("HT MCS %d", r.MCS) +// } + // rateInfo provides statistics about the receive or transmit rate of // an interface. -type rateInfo struct { +type RateInfo struct { // Bitrate in bits per second. Bitrate int + + // MCS is the modulation and coding scheme index for HT. + MCS int + + // VHT-MCS is the modulation and coding scheme index for VHT. + VHT_MCS int + + // VHT-NSS is the number of spatial streams for VHT. + VHT_NSS int } // 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 info RateInfo for _, a := range attrs { switch a.Type { case unix.NL80211_RATE_INFO_BITRATE32: info.Bitrate = int(nlenc.Uint32(a.Data)) + case unix.NL80211_RATE_INFO_MCS: + info.MCS = int(nlenc.Uint8(a.Data)) + case unix.NL80211_RATE_INFO_VHT_MCS: + info.VHT_MCS = int(nlenc.Uint8(a.Data)) + case unix.NL80211_RATE_INFO_VHT_NSS: + info.VHT_NSS = int(nlenc.Uint8(a.Data)) } // Only use 16-bit counters if the 32-bit counters are not present. diff --git a/client_linux_test.go b/client_linux_test.go index b2c83a9..93e2a87 100644 --- a/client_linux_test.go +++ b/client_linux_test.go @@ -306,6 +306,44 @@ func TestLinux_clientStationInfoOK(t *testing.T) { ReceiveBitrate: 260000000, TransmitBitrate: 260000000, }, + { + 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: 260000000, + RX_MCS: 1, + TX_MCS: 2, + }, + { + 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: 260000000, + RX_VHT_MCS: 7, + TX_VHT_MCS: 8, + }, } ifi := &Interface{ @@ -520,6 +558,8 @@ func (s *StationInfo) attributes() []netlink.Attribute { 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))}, + {Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(s.RX_MCS))}, + {Type: unix.NL80211_RATE_INFO_VHT_MCS, Data: nlenc.Uint8Bytes(uint8(s.RX_VHT_MCS))}, }), }, { @@ -527,6 +567,8 @@ func (s *StationInfo) attributes() []netlink.Attribute { 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))}, + {Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(s.TX_MCS))}, + {Type: unix.NL80211_RATE_INFO_VHT_MCS, Data: nlenc.Uint8Bytes(uint8(s.TX_VHT_MCS))}, }), }, }), diff --git a/wifi.go b/wifi.go index 290b3a3..546e646 100644 --- a/wifi.go +++ b/wifi.go @@ -255,6 +255,24 @@ type StationInfo struct { // The number of times a beacon loss was detected. BeaconLoss int + + // The current receive MCS indexes (for HT rates). + RX_MCS int + + // The current transmit MCS indexes (for HT rates). + TX_MCS int + + // The current receive MCS indexes (for VHT rates). + RX_VHT_MCS int + + // The current transmit MCS indexes (for VHT rates). + TX_VHT_MCS int + + // The current receive rate information. + ReceiveRateInfo RateInfo + + // The current transmit rate information. + TransmitRateInfo RateInfo } // BSSLoad is an Information Element containing measurements of the load on the BSS. From 74d434d8c97fa83e5b8d85c6d62b2d41e0c29a90 Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Wed, 29 Oct 2025 11:42:34 +0100 Subject: [PATCH 02/11] refactor: remove unused rate modulation interfaces and related code --- client_linux.go | 38 -------------------------------------- wifi.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/client_linux.go b/client_linux.go index 3d3a95b..0d57e24 100644 --- a/client_linux.go +++ b/client_linux.go @@ -775,44 +775,6 @@ func (info *StationInfo) parseAttributes(attrs []netlink.Attribute) error { return nil } -// type RateModulationInfo interface { -// GetMCS() int -// GetNSS() int -// String() string -// } - -// type htRateInfo struct { -// MCS int -// } - -// func (r htRateInfo) GetMCS() int { -// return r.MCS -// } - -// func (r htRateInfo) GetNSS() int { -// return (r.MCS / 8) + 1 -// } - -// func (r htRateInfo) String() string { -// return fmt.Sprintf("HT MCS %d", r.MCS) -// } - -// rateInfo provides statistics about the receive or transmit rate of -// an interface. -type RateInfo struct { - // Bitrate in bits per second. - Bitrate int - - // MCS is the modulation and coding scheme index for HT. - MCS int - - // VHT-MCS is the modulation and coding scheme index for VHT. - VHT_MCS int - - // VHT-NSS is the number of spatial streams for VHT. - VHT_NSS int -} - // parseRateInfo parses a rateInfo from netlink attributes. func parseRateInfo(b []byte) (*RateInfo, error) { attrs, err := netlink.UnmarshalAttributes(b) diff --git a/wifi.go b/wifi.go index 546e646..772a14f 100644 --- a/wifi.go +++ b/wifi.go @@ -208,6 +208,44 @@ type Interface struct { ChannelWidth ChannelWidth } +// type RateModulationInfo interface { +// GetMCS() int +// GetNSS() int +// String() string +// } + +// type htRateInfo struct { +// MCS int +// } + +// func (r htRateInfo) GetMCS() int { +// return r.MCS +// } + +// func (r htRateInfo) GetNSS() int { +// return (r.MCS / 8) + 1 +// } + +// func (r htRateInfo) String() string { +// return fmt.Sprintf("HT MCS %d", r.MCS) +// } + +// rateInfo provides statistics about the receive or transmit rate of +// an interface. +type RateInfo struct { + // Bitrate in bits per second. + Bitrate int + + // MCS is the modulation and coding scheme index for HT. + MCS int + + // VHT-MCS is the modulation and coding scheme index for VHT. + VHT_MCS int + + // VHT-NSS is the number of spatial streams for VHT. + VHT_NSS int +} + // StationInfo contains statistics about a WiFi interface operating in // station mode. type StationInfo struct { From c490bd454a82f3632a85a2ac0694904da39fbd0f Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Fri, 21 Nov 2025 14:42:52 +0100 Subject: [PATCH 03/11] refactor Rate Info expose Interface create subtypes that implement the interface --- client_linux.go | 146 +++++++++++++++++++++++++++++++++++++++---- client_linux_test.go | 46 ++------------ wifi.go | 110 +++++++++++++++++++++----------- 3 files changed, 210 insertions(+), 92 deletions(-) diff --git a/client_linux.go b/client_linux.go index 0d57e24..c2b970d 100644 --- a/client_linux.go +++ b/client_linux.go @@ -8,6 +8,7 @@ import ( "crypto/sha1" "encoding/binary" "errors" + "fmt" "net" "os" "sync" @@ -750,13 +751,9 @@ func (info *StationInfo) parseAttributes(attrs []netlink.Attribute) error { switch a.Type { case unix.NL80211_STA_INFO_RX_BITRATE: info.ReceiveBitrate = rate.Bitrate - info.RX_MCS = rate.MCS - info.RX_VHT_MCS = rate.VHT_MCS info.ReceiveRateInfo = *rate case unix.NL80211_STA_INFO_TX_BITRATE: info.TransmitBitrate = rate.Bitrate - info.TX_MCS = rate.MCS - info.TX_VHT_MCS = rate.VHT_MCS info.TransmitRateInfo = *rate } } @@ -775,6 +772,14 @@ func (info *StationInfo) parseAttributes(attrs []netlink.Attribute) error { return nil } +func bitrateStr(bitrate int) string { + if bitrate > 0 { + return fmt.Sprintf("%d.%d Mb/s ", bitrate/10, bitrate%10) + } else { + return "(unknown)" + } +} + // parseRateInfo parses a rateInfo from netlink attributes. func parseRateInfo(b []byte) (*RateInfo, error) { attrs, err := netlink.UnmarshalAttributes(b) @@ -783,26 +788,141 @@ func parseRateInfo(b []byte) (*RateInfo, error) { } var info RateInfo + // initialize with unknown values + // baseModulationInfo := BaseModulationInfo{MCS: -1, NSS: -1} + 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) switch a.Type { case unix.NL80211_RATE_INFO_BITRATE32: info.Bitrate = int(nlenc.Uint32(a.Data)) + iwDescription += bitrateStr(info.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 info.Bitrate == 0 { + info.Bitrate = int(nlenc.Uint16(a.Data)) + iwDescription += bitrateStr(info.Bitrate) + } case unix.NL80211_RATE_INFO_MCS: - info.MCS = int(nlenc.Uint8(a.Data)) + htModulationInfo.HT_MCS = int(nlenc.Uint8(a.Data)) + htModulationInfo.MCS = htModulationInfo.HT_MCS % 8 + htModulationInfo.NSS = (htModulationInfo.HT_MCS / 8) + 1 + iwDescription += fmt.Sprintf(" MCS %d", htModulationInfo.HT_MCS) case unix.NL80211_RATE_INFO_VHT_MCS: - info.VHT_MCS = int(nlenc.Uint8(a.Data)) + 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_16_MHZ_WIDTH: + channelWidth = ChannelWidth16 + iwDescription += " 16MHz" + case unix.NL80211_RATE_INFO_SHORT_GI: + htModulationInfo.ShortGi = true + vhtModulationInfo.ShortGi = true + // shortGi = true // TODO: Do other modulations support short GI? + iwDescription += " Short GI" case unix.NL80211_RATE_INFO_VHT_NSS: - info.VHT_NSS = int(nlenc.Uint8(a.Data)) + 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)) - } + // 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 + // Verify and assign modulation info + //highest WiFi standard with valid MCS found determines modulation type + switch { + case ehtModulationInfo.MCS != -1: + info.ModulationType = RateModulationInfoTypeEHT + ehtModulationInfo.IwDescription = iwDescription + info.Modulation = ehtModulationInfo + case heModulationInfo.MCS != -1: + info.ModulationType = RateModulationInfoTypeHE + heModulationInfo.IwDescription = iwDescription + info.Modulation = heModulationInfo + case vhtModulationInfo.MCS != -1: + info.ModulationType = RateModulationInfoTypeVHT + vhtModulationInfo.IwDescription = iwDescription + info.Modulation = vhtModulationInfo + case htModulationInfo.MCS != -1: + info.ModulationType = RateModulationInfoTypeHT + htModulationInfo.IwDescription = iwDescription + info.Modulation = htModulationInfo + default: + info.ModulationType = RateModulationInfoTypeUNKNOWN + info.Modulation = nil + } + + info.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 diff --git a/client_linux_test.go b/client_linux_test.go index 93e2a87..d023289 100644 --- a/client_linux_test.go +++ b/client_linux_test.go @@ -306,44 +306,6 @@ func TestLinux_clientStationInfoOK(t *testing.T) { ReceiveBitrate: 260000000, TransmitBitrate: 260000000, }, - { - 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: 260000000, - RX_MCS: 1, - TX_MCS: 2, - }, - { - 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: 260000000, - RX_VHT_MCS: 7, - TX_VHT_MCS: 8, - }, } ifi := &Interface{ @@ -558,8 +520,8 @@ func (s *StationInfo) attributes() []netlink.Attribute { 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))}, - {Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(s.RX_MCS))}, - {Type: unix.NL80211_RATE_INFO_VHT_MCS, Data: nlenc.Uint8Bytes(uint8(s.RX_VHT_MCS))}, + // {Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(s.RX_MCS))}, + // {Type: unix.NL80211_RATE_INFO_VHT_MCS, Data: nlenc.Uint8Bytes(uint8(s.RX_VHT_MCS))}, }), }, { @@ -567,8 +529,8 @@ func (s *StationInfo) attributes() []netlink.Attribute { 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))}, - {Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(s.TX_MCS))}, - {Type: unix.NL80211_RATE_INFO_VHT_MCS, Data: nlenc.Uint8Bytes(uint8(s.TX_VHT_MCS))}, + // {Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(s.TX_MCS))}, + // {Type: unix.NL80211_RATE_INFO_VHT_MCS, Data: nlenc.Uint8Bytes(uint8(s.TX_VHT_MCS))}, }), }, }), diff --git a/wifi.go b/wifi.go index 772a14f..44f9286 100644 --- a/wifi.go +++ b/wifi.go @@ -208,27 +208,75 @@ type Interface struct { ChannelWidth ChannelWidth } -// type RateModulationInfo interface { -// GetMCS() int -// GetNSS() int -// String() string -// } +type RateModulationInfo interface { + // MCS is the modulation and coding scheme index. + GetMCS() int -// type htRateInfo struct { -// MCS int -// } + // NSS is the number of spatial streams. + GetNSS() int -// func (r htRateInfo) GetMCS() int { -// return r.MCS -// } + // Description returns a human-readable description of the modulation. + // This will be in the format also used by iw. + Description() string +} + +type BaseModulationInfo struct { + MCS int + NSS int + IwDescription string +} + +func (r BaseModulationInfo) GetMCS() int { + return r.MCS +} + +func (r BaseModulationInfo) GetNSS() int { + return r.NSS +} + +func (r BaseModulationInfo) Description() string { + return r.IwDescription +} + +// 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 HT_MCS +type HTModulationInfo struct { + BaseModulationInfo + HT_MCS int + ShortGi bool +} + +// VHTModulationInfo represents modulation information for VHT rates. +type VHTModulationInfo struct { + BaseModulationInfo + ShortGi bool +} + +type HEModulationInfo struct { + BaseModulationInfo + Gi int + DCM int + RUAlloc int +} -// func (r htRateInfo) GetNSS() int { -// return (r.MCS / 8) + 1 -// } +type EHTModulationInfo struct { + BaseModulationInfo + Gi int + RUAlloc int +} + +type RateModulationInfoType int -// func (r htRateInfo) String() string { -// return fmt.Sprintf("HT MCS %d", r.MCS) -// } +const ( + RateModulationInfoTypeHT RateModulationInfoType = iota + RateModulationInfoTypeVHT + RateModulationInfoTypeHE + RateModulationInfoTypeEHT + RateModulationInfoTypeUNKNOWN +) // rateInfo provides statistics about the receive or transmit rate of // an interface. @@ -236,14 +284,14 @@ type RateInfo struct { // Bitrate in bits per second. Bitrate int - // MCS is the modulation and coding scheme index for HT. - MCS int + // The type of modulation used. Can also be inferred from Modulation.(type) + ModulationType RateModulationInfoType - // VHT-MCS is the modulation and coding scheme index for VHT. - VHT_MCS int + // Modulation information. + Modulation RateModulationInfo - // VHT-NSS is the number of spatial streams for VHT. - VHT_NSS int + // Channel width used for this rate. + ChannelWidth ChannelWidth } // StationInfo contains statistics about a WiFi interface operating in @@ -294,22 +342,10 @@ type StationInfo struct { // The number of times a beacon loss was detected. BeaconLoss int - // The current receive MCS indexes (for HT rates). - RX_MCS int - - // The current transmit MCS indexes (for HT rates). - TX_MCS int - - // The current receive MCS indexes (for VHT rates). - RX_VHT_MCS int - - // The current transmit MCS indexes (for VHT rates). - TX_VHT_MCS int - - // The current receive rate information. + // The current receive rate and modulation information. ReceiveRateInfo RateInfo - // The current transmit rate information. + // The current transmit rate and modulation information. TransmitRateInfo RateInfo } From 038a8dccb97d8fe98db4bbdb656dd5017a6a30c6 Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Fri, 21 Nov 2025 16:20:20 +0100 Subject: [PATCH 04/11] add documentation --- client_linux.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/client_linux.go b/client_linux.go index c2b970d..f643f22 100644 --- a/client_linux.go +++ b/client_linux.go @@ -774,7 +774,7 @@ func (info *StationInfo) parseAttributes(attrs []netlink.Attribute) error { func bitrateStr(bitrate int) string { if bitrate > 0 { - return fmt.Sprintf("%d.%d Mb/s ", bitrate/10, bitrate%10) + return fmt.Sprintf("%d.%d MBit/s ", bitrate/10, bitrate%10) } else { return "(unknown)" } @@ -807,6 +807,8 @@ func parseRateInfo(b []byte) (*RateInfo, error) { 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)) @@ -857,7 +859,6 @@ func parseRateInfo(b []byte) (*RateInfo, error) { case unix.NL80211_RATE_INFO_SHORT_GI: htModulationInfo.ShortGi = true vhtModulationInfo.ShortGi = true - // shortGi = true // TODO: Do other modulations support short GI? iwDescription += " Short GI" case unix.NL80211_RATE_INFO_VHT_NSS: vhtModulationInfo.NSS = int(nlenc.Uint8(a.Data)) @@ -890,15 +891,10 @@ func parseRateInfo(b []byte) (*RateInfo, error) { ehtModulationInfo.RUAlloc = int(nlenc.Uint8(a.Data)) iwDescription += fmt.Sprintf(" EHT-RU-ALLOC %d", ehtModulationInfo.RUAlloc) } - - // 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 - // Verify and assign modulation info - //highest WiFi standard with valid MCS found determines modulation type + // highest WiFi standard with valid MCS found determines modulation type switch { case ehtModulationInfo.MCS != -1: info.ModulationType = RateModulationInfoTypeEHT From 362fba8cd861bfc90ffa48a9235a1e0193de3d39 Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Wed, 26 Nov 2025 12:20:50 +0100 Subject: [PATCH 05/11] refactored variable names for better clarity --- client_linux.go | 51 ++++++++++++++++++++++++------------------------- wifi.go | 19 +++++++++--------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/client_linux.go b/client_linux.go index f643f22..4d8da3f 100644 --- a/client_linux.go +++ b/client_linux.go @@ -787,9 +787,8 @@ func parseRateInfo(b []byte) (*RateInfo, error) { return nil, err } - var info RateInfo + var rateinfo RateInfo // initialize with unknown values - // baseModulationInfo := BaseModulationInfo{MCS: -1, NSS: -1} htModulationInfo := HTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: -1, NSS: -1}} vhtModulationInfo := VHTModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: -1, NSS: -1}} heModulationInfo := HEModulationInfo{BaseModulationInfo: BaseModulationInfo{MCS: -1, NSS: -1}} @@ -811,15 +810,15 @@ func parseRateInfo(b []byte) (*RateInfo, error) { // 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)) - iwDescription += bitrateStr(info.Bitrate) + 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 info.Bitrate == 0 { - info.Bitrate = int(nlenc.Uint16(a.Data)) - iwDescription += bitrateStr(info.Bitrate) + if rateinfo.Bitrate == 0 { + rateinfo.Bitrate = int(nlenc.Uint16(a.Data)) + iwDescription += bitrateStr(rateinfo.Bitrate) } case unix.NL80211_RATE_INFO_MCS: htModulationInfo.HT_MCS = int(nlenc.Uint8(a.Data)) @@ -857,8 +856,8 @@ func parseRateInfo(b []byte) (*RateInfo, error) { channelWidth = ChannelWidth16 iwDescription += " 16MHz" case unix.NL80211_RATE_INFO_SHORT_GI: - htModulationInfo.ShortGi = true - vhtModulationInfo.ShortGi = true + htModulationInfo.ShortGI = true + vhtModulationInfo.ShortGI = true iwDescription += " Short GI" case unix.NL80211_RATE_INFO_VHT_NSS: vhtModulationInfo.NSS = int(nlenc.Uint8(a.Data)) @@ -870,8 +869,8 @@ func parseRateInfo(b []byte) (*RateInfo, error) { 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) + 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) @@ -885,8 +884,8 @@ func parseRateInfo(b []byte) (*RateInfo, error) { 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) + 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) @@ -897,33 +896,33 @@ func parseRateInfo(b []byte) (*RateInfo, error) { // highest WiFi standard with valid MCS found determines modulation type switch { case ehtModulationInfo.MCS != -1: - info.ModulationType = RateModulationInfoTypeEHT + rateinfo.ModulationType = RateModulationInfoTypeEHT ehtModulationInfo.IwDescription = iwDescription - info.Modulation = ehtModulationInfo + rateinfo.Modulation = ehtModulationInfo case heModulationInfo.MCS != -1: - info.ModulationType = RateModulationInfoTypeHE + rateinfo.ModulationType = RateModulationInfoTypeHE heModulationInfo.IwDescription = iwDescription - info.Modulation = heModulationInfo + rateinfo.Modulation = heModulationInfo case vhtModulationInfo.MCS != -1: - info.ModulationType = RateModulationInfoTypeVHT + rateinfo.ModulationType = RateModulationInfoTypeVHT vhtModulationInfo.IwDescription = iwDescription - info.Modulation = vhtModulationInfo + rateinfo.Modulation = vhtModulationInfo case htModulationInfo.MCS != -1: - info.ModulationType = RateModulationInfoTypeHT + rateinfo.ModulationType = RateModulationInfoTypeHT htModulationInfo.IwDescription = iwDescription - info.Modulation = htModulationInfo + rateinfo.Modulation = htModulationInfo default: - info.ModulationType = RateModulationInfoTypeUNKNOWN - info.Modulation = nil + rateinfo.ModulationType = RateModulationInfoTypeUNKNOWN + rateinfo.Modulation = nil } - info.ChannelWidth = channelWidth + 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/wifi.go b/wifi.go index 44f9286..74b62aa 100644 --- a/wifi.go +++ b/wifi.go @@ -215,8 +215,8 @@ type RateModulationInfo interface { // NSS is the number of spatial streams. GetNSS() int - // Description returns a human-readable description of the modulation. - // This will be in the format also used by iw. + // 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 } @@ -246,28 +246,29 @@ func (r BaseModulationInfo) Description() string { type HTModulationInfo struct { BaseModulationInfo HT_MCS int - ShortGi bool + ShortGI bool } // VHTModulationInfo represents modulation information for VHT rates. type VHTModulationInfo struct { BaseModulationInfo - ShortGi bool + ShortGI bool } type HEModulationInfo struct { BaseModulationInfo - Gi int + GI int DCM int RUAlloc int } type EHTModulationInfo struct { BaseModulationInfo - Gi int + GI int RUAlloc int } +// RateModulationInfoType indicates the type of modulation used for a rate. type RateModulationInfoType int const ( @@ -278,7 +279,7 @@ const ( RateModulationInfoTypeUNKNOWN ) -// rateInfo provides statistics about the receive or transmit rate of +// rateInfo provides information about the receive or transmit rate of // an interface. type RateInfo struct { // Bitrate in bits per second. @@ -342,10 +343,10 @@ type StationInfo struct { // The number of times a beacon loss was detected. BeaconLoss int - // The current receive rate and modulation information. + // The current receive rate and detailed modulation information. ReceiveRateInfo RateInfo - // The current transmit rate and modulation information. + // The current transmit rate and detailed modulation information. TransmitRateInfo RateInfo } From d6fae4a70d2999a9f9efac2b3e5c58f36a6b9a93 Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Thu, 27 Nov 2025 10:04:37 +0100 Subject: [PATCH 06/11] feat: add WifiGeneration method to modulation info types --- wifi.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/wifi.go b/wifi.go index 74b62aa..cb38ca7 100644 --- a/wifi.go +++ b/wifi.go @@ -218,6 +218,9 @@ type RateModulationInfo interface { // 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", "802.11ac", "802.11ax", "802.11be") + WifiGeneration() string } type BaseModulationInfo struct { @@ -238,6 +241,10 @@ func (r BaseModulationInfo) Description() string { return r.IwDescription } +func (r 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 @@ -249,12 +256,20 @@ type HTModulationInfo struct { ShortGI bool } +func (r HTModulationInfo) WifiGeneration() string { + return "802.11n (WiFi 4)" +} + // VHTModulationInfo represents modulation information for VHT rates. type VHTModulationInfo struct { BaseModulationInfo ShortGI bool } +func (r VHTModulationInfo) WifiGeneration() string { + return "802.11ac (WiFi 5)" +} + type HEModulationInfo struct { BaseModulationInfo GI int @@ -262,12 +277,20 @@ type HEModulationInfo struct { RUAlloc int } +func (r HEModulationInfo) WifiGeneration() string { + return "802.11ax (WiFi 6)" +} + type EHTModulationInfo struct { BaseModulationInfo GI int RUAlloc int } +func (r EHTModulationInfo) WifiGeneration() string { + return "802.11be (WiFi 7)" +} + // RateModulationInfoType indicates the type of modulation used for a rate. type RateModulationInfoType int From d5d5e856f152f7f81f722dfd7f5db57cf8b6b811 Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Thu, 27 Nov 2025 10:39:28 +0100 Subject: [PATCH 07/11] improve Documantation --- wifi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wifi.go b/wifi.go index cb38ca7..3003a1a 100644 --- a/wifi.go +++ b/wifi.go @@ -219,7 +219,7 @@ type RateModulationInfo interface { // Uses same format as iw tool, but not necessary in the same order. Description() string - // WifiGeneration returns the WiFi generation (e.g., "802.11n", "802.11ac", "802.11ax", "802.11be") + // 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 } From c28fea0308ae497fa4076abbad08de082b63cd72 Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Thu, 27 Nov 2025 10:42:13 +0100 Subject: [PATCH 08/11] fix: Linter Errors --- client_linux.go | 11 +++++------ wifi.go | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/client_linux.go b/client_linux.go index 4d8da3f..10c813c 100644 --- a/client_linux.go +++ b/client_linux.go @@ -775,9 +775,8 @@ func (info *StationInfo) parseAttributes(attrs []netlink.Attribute) error { func bitrateStr(bitrate int) string { if bitrate > 0 { return fmt.Sprintf("%d.%d MBit/s ", bitrate/10, bitrate%10) - } else { - return "(unknown)" } + return "(unknown)" } // parseRateInfo parses a rateInfo from netlink attributes. @@ -821,10 +820,10 @@ func parseRateInfo(b []byte) (*RateInfo, error) { iwDescription += bitrateStr(rateinfo.Bitrate) } case unix.NL80211_RATE_INFO_MCS: - htModulationInfo.HT_MCS = int(nlenc.Uint8(a.Data)) - htModulationInfo.MCS = htModulationInfo.HT_MCS % 8 - htModulationInfo.NSS = (htModulationInfo.HT_MCS / 8) + 1 - iwDescription += fmt.Sprintf(" MCS %d", htModulationInfo.HT_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) diff --git a/wifi.go b/wifi.go index 3003a1a..50aa45c 100644 --- a/wifi.go +++ b/wifi.go @@ -249,10 +249,10 @@ func (r BaseModulationInfo) WifiGeneration() string { // 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 HT_MCS +// original MCS index is available as HTMCS type HTModulationInfo struct { BaseModulationInfo - HT_MCS int + HTMCS int ShortGI bool } From c608a434732374f6195c37ac0316b031c6bb9a37 Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Wed, 21 Jan 2026 14:21:30 +0100 Subject: [PATCH 09/11] Add Tests for RateModulationInfo --- client_linux_test.go | 98 ++++++++++++++++++++++++++++++++++++++------ wifi.go | 61 ++++++++++++++++++++++----- wifi_test.go | 22 ++++++++++ 3 files changed, 157 insertions(+), 24 deletions(-) diff --git a/client_linux_test.go b/client_linux_test.go index d023289..bbf3969 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,9 @@ 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 Short GI 160MHz"}, ShortGI: true}, ChannelWidth: ChannelWidth160}, }, } @@ -488,6 +493,69 @@ 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: + //ri := rateInfo.(HTModulationInfo) + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(ri.HTMCS))}) + // TODO + case VHTModulationInfo: + //ri := rateInfo.(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))}) + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_SHORT_GI}) + // TODO + case HEModulationInfo: + // TODO + case EHTModulationInfo: + // TODO + default: + fmt.Printf("Could not type-switch %v \n", rateInfo) + } + return attr +} + +func channelWithAttributes(cw ChannelWidth) (attr []netlink.Attribute) { + switch cw { + // case ChannelWidth20NoHT: + // return "20 MHz (no HT)" + // case ChannelWidth20: + // return "20 MHz" + 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 + //return fmt.Sprintf("unknown(%d)", t) + } +} + func (s *StationInfo) attributes() []netlink.Attribute { return []netlink.Attribute{ // TODO(mdlayher): return more attributes for validation? @@ -517,21 +585,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))}, - // {Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(s.RX_MCS))}, - // {Type: unix.NL80211_RATE_INFO_VHT_MCS, Data: nlenc.Uint8Bytes(uint8(s.RX_VHT_MCS))}, - }), + 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))}, - // {Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(s.TX_MCS))}, - // {Type: unix.NL80211_RATE_INFO_VHT_MCS, Data: nlenc.Uint8Bytes(uint8(s.TX_VHT_MCS))}, - }), + 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 50aa45c..2f3f239 100644 --- a/wifi.go +++ b/wifi.go @@ -229,19 +229,29 @@ type BaseModulationInfo struct { IwDescription string } -func (r BaseModulationInfo) GetMCS() int { - return r.MCS +func (mi BaseModulationInfo) Equal(mi2 BaseModulationInfo) bool { + if mi.MCS != mi2.MCS { + return false + } + if mi.NSS != mi2.NSS { + return false + } + return true +} + +func (mi BaseModulationInfo) GetMCS() int { + return mi.MCS } -func (r BaseModulationInfo) GetNSS() int { - return r.NSS +func (mi BaseModulationInfo) GetNSS() int { + return mi.NSS } -func (r BaseModulationInfo) Description() string { - return r.IwDescription +func (mi BaseModulationInfo) Description() string { + return mi.IwDescription } -func (r BaseModulationInfo) WifiGeneration() string { +func (mi BaseModulationInfo) WifiGeneration() string { return "unknown" } @@ -256,7 +266,23 @@ type HTModulationInfo struct { ShortGI bool } -func (r HTModulationInfo) WifiGeneration() string { +func (mi HTModulationInfo) Equal(mi2 HTModulationInfo) bool { + if mi.MCS != mi2.MCS { + return false + } + if mi.NSS != mi2.NSS { + return false + } + if mi.HTMCS != mi2.HTMCS { + return false + } + if mi.ShortGI != mi2.ShortGI { + return false + } + return true +} + +func (mi HTModulationInfo) WifiGeneration() string { return "802.11n (WiFi 4)" } @@ -266,7 +292,20 @@ type VHTModulationInfo struct { ShortGI bool } -func (r VHTModulationInfo) WifiGeneration() string { +func (mi VHTModulationInfo) Equal(mi2 VHTModulationInfo) bool { + if mi.MCS != mi2.MCS { + return false + } + if mi.NSS != mi2.NSS { + return false + } + if mi.ShortGI != mi2.ShortGI { + return false + } + return true +} + +func (mi VHTModulationInfo) WifiGeneration() string { return "802.11ac (WiFi 5)" } @@ -277,7 +316,7 @@ type HEModulationInfo struct { RUAlloc int } -func (r HEModulationInfo) WifiGeneration() string { +func (mi HEModulationInfo) WifiGeneration() string { return "802.11ax (WiFi 6)" } @@ -287,7 +326,7 @@ type EHTModulationInfo struct { RUAlloc int } -func (r EHTModulationInfo) WifiGeneration() string { +func (mi EHTModulationInfo) WifiGeneration() string { return "802.11be (WiFi 7)" } 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) + } + }) + } +} From ff869d0564e6619f2e82975a9cf8fa6f77f72d0b Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Wed, 21 Jan 2026 16:04:28 +0100 Subject: [PATCH 10/11] more test Cases --- client_linux_test.go | 63 ++++++++++++++++++++++++++++++++++++++------ wifi.go | 39 --------------------------- 2 files changed, 55 insertions(+), 47 deletions(-) diff --git a/client_linux_test.go b/client_linux_test.go index bbf3969..aa4ef69 100644 --- a/client_linux_test.go +++ b/client_linux_test.go @@ -309,7 +309,45 @@ func TestLinux_clientStationInfoOK(t *testing.T) { ReceiveBitrate: 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 Short GI 160MHz"}, ShortGI: true}, ChannelWidth: ChannelWidth160}, + 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}, }, } @@ -501,19 +539,28 @@ func modulationAttributes(rateInfo RateModulationInfo) (attr []netlink.Attribute //ri := rateInfo.(BaseModulationInfo) // TODO case HTModulationInfo: - //ri := rateInfo.(HTModulationInfo) + // TODO ??> attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_MCS, Data: nlenc.Uint8Bytes(uint8(ri.HTMCS))}) - // TODO + if ri.ShortGI { + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_SHORT_GI}) + } case VHTModulationInfo: - //ri := rateInfo.(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))}) - attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_SHORT_GI}) - // TODO + if ri.ShortGI { + attr = append(attr, netlink.Attribute{Type: unix.NL80211_RATE_INFO_SHORT_GI}) + } case HEModulationInfo: - // TODO + 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: - // TODO + 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) } diff --git a/wifi.go b/wifi.go index 2f3f239..b6bf741 100644 --- a/wifi.go +++ b/wifi.go @@ -229,16 +229,6 @@ type BaseModulationInfo struct { IwDescription string } -func (mi BaseModulationInfo) Equal(mi2 BaseModulationInfo) bool { - if mi.MCS != mi2.MCS { - return false - } - if mi.NSS != mi2.NSS { - return false - } - return true -} - func (mi BaseModulationInfo) GetMCS() int { return mi.MCS } @@ -266,22 +256,6 @@ type HTModulationInfo struct { ShortGI bool } -func (mi HTModulationInfo) Equal(mi2 HTModulationInfo) bool { - if mi.MCS != mi2.MCS { - return false - } - if mi.NSS != mi2.NSS { - return false - } - if mi.HTMCS != mi2.HTMCS { - return false - } - if mi.ShortGI != mi2.ShortGI { - return false - } - return true -} - func (mi HTModulationInfo) WifiGeneration() string { return "802.11n (WiFi 4)" } @@ -292,19 +266,6 @@ type VHTModulationInfo struct { ShortGI bool } -func (mi VHTModulationInfo) Equal(mi2 VHTModulationInfo) bool { - if mi.MCS != mi2.MCS { - return false - } - if mi.NSS != mi2.NSS { - return false - } - if mi.ShortGI != mi2.ShortGI { - return false - } - return true -} - func (mi VHTModulationInfo) WifiGeneration() string { return "802.11ac (WiFi 5)" } From 24e4be8070915fe1b7c2e154158a11e5a6e3f1af Mon Sep 17 00:00:00 2001 From: Lukas Raffelt Date: Wed, 21 Jan 2026 16:19:29 +0100 Subject: [PATCH 11/11] add missing 8MHz channel width to rate parsing --- client_linux.go | 3 +++ client_linux_test.go | 41 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/client_linux.go b/client_linux.go index 10c813c..251b8c9 100644 --- a/client_linux.go +++ b/client_linux.go @@ -851,6 +851,9 @@ func parseRateInfo(b []byte) (*RateInfo, error) { 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" diff --git a/client_linux_test.go b/client_linux_test.go index aa4ef69..79cca3f 100644 --- a/client_linux_test.go +++ b/client_linux_test.go @@ -349,6 +349,44 @@ func TestLinux_clientStationInfoOK(t *testing.T) { 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}, + }, } ifi := &Interface{ @@ -570,9 +608,7 @@ func modulationAttributes(rateInfo RateModulationInfo) (attr []netlink.Attribute func channelWithAttributes(cw ChannelWidth) (attr []netlink.Attribute) { switch cw { // case ChannelWidth20NoHT: - // return "20 MHz (no HT)" // case ChannelWidth20: - // return "20 MHz" case ChannelWidth40: return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_40_MHZ_WIDTH}} case ChannelWidth80: @@ -599,7 +635,6 @@ func channelWithAttributes(cw ChannelWidth) (attr []netlink.Attribute) { return []netlink.Attribute{{Type: unix.NL80211_RATE_INFO_320_MHZ_WIDTH}} default: return attr - //return fmt.Sprintf("unknown(%d)", t) } }