mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-27 03:47:47 +02:00
feat: monitor connected Wi-Fi signal per interface (#2367)
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/henrygd/beszel/agent/battery"
|
||||
"github.com/henrygd/beszel/agent/btrfs"
|
||||
"github.com/henrygd/beszel/agent/utils"
|
||||
"github.com/henrygd/beszel/agent/wifi"
|
||||
"github.com/henrygd/beszel/agent/zfs"
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
@@ -267,6 +268,8 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
||||
}
|
||||
}
|
||||
|
||||
systemStats.WiFi = wifi.Collect()
|
||||
|
||||
// update system info
|
||||
a.systemInfo.ConnectionType = a.connectionManager.ConnectionType
|
||||
a.systemInfo.Cpu = systemStats.Cpu
|
||||
@@ -274,6 +277,7 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
||||
a.systemInfo.MemPct = systemStats.MemPct
|
||||
a.systemInfo.DiskPct = systemStats.DiskPct
|
||||
a.systemInfo.Battery = systemStats.Battery
|
||||
a.systemInfo.WiFi = systemStats.WiFi
|
||||
a.systemInfo.Uptime, _ = getUptime()
|
||||
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
|
||||
a.systemInfo.Threads = a.systemDetails.Threads
|
||||
|
||||
46
agent/wifi/README.md
Normal file
46
agent/wifi/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Connected Wi-Fi signal
|
||||
|
||||
Each fresh stats poll reports a snapshot of connected station interfaces in
|
||||
`stats.wifi` and `info.wifi`. Map keys identify interfaces, not networks. `s` is
|
||||
optional SSID metadata; `r` is nullable native RSSI in dBm. Quality percentages
|
||||
are never converted to dBm. An associated interface without an accessible RSSI
|
||||
still appears with an unavailable signal. No scans or network changes occur.
|
||||
|
||||
The hub panel gates exclusively on current `systems.info.wifi` and system `up`
|
||||
status, independently of the selected historical period. Empty/null snapshots
|
||||
clear it. Historical averages use only available readings per interface; gaps
|
||||
are not zero signal. Interface colors and keys remain stable on reconnect.
|
||||
SSID in an aggregate is the latest observed metadata, not a separate series.
|
||||
|
||||
## Platforms
|
||||
|
||||
- Linux: native nl80211 through `github.com/mdlayher/wifi`, compiled into the
|
||||
static agent, including scratch and all other agent images. No `iw`, shared
|
||||
libraries, extra capabilities, or external helper required. Host network
|
||||
namespace access (Docker `network_mode: host`) is necessary to see host Wi-Fi.
|
||||
Only managed station interfaces with explicit associated BSS status appear.
|
||||
Kernel BSS cache reads do not trigger scans; station statistics supply native
|
||||
RSSI matched to the associated AP. Denied/missing station statistics retain
|
||||
association with unavailable RSSI, never substitute stale scan-cache signal.
|
||||
Missing nl80211/driver support or denied association reads yield no readings.
|
||||
A single two-second socket deadline bounds enumeration and interface queries
|
||||
after opening the client. The library's initial nl80211 family discovery is
|
||||
synchronous and does not expose a deadline.
|
||||
- macOS: system `osascript` uses public CoreWLAN via JXA. Station mode proves
|
||||
association; RSSI and optional SSID are read independently. No private airport
|
||||
binary, elevated command or compiled helper required. Privacy settings may
|
||||
redact SSIDs. The subprocess has a two-second deadline.
|
||||
- Windows: native WLAN API, interface GUID identity, connected interface state,
|
||||
optional current-connection SSID and native RSSI query. No localized `netsh`
|
||||
parsing. Missing WLAN service/API yields no readings; denied SSID/RSSI query
|
||||
leaves a connected interface with missing metadata/signal. Non-UTF-8 raw
|
||||
SSIDs are omitted so they cannot invalidate CBOR text in the agent response.
|
||||
Native synchronous WLAN calls cannot be interrupted by the Go deadline.
|
||||
- FreeBSD and other platforms: unsupported, empty snapshot. No approximation
|
||||
from ifconfig quality and no stale data retained.
|
||||
|
||||
Collectors retry each fresh poll, allowing interfaces and capabilities to appear
|
||||
without an agent restart. Standard agent response caching still applies. Existing
|
||||
hub record JSON storage requires no database schema migration. Older agents
|
||||
without the field keep the panel hidden. Native macOS/Windows runtime checks and
|
||||
real adapter testing are still required; cross compilation is not hardware proof.
|
||||
40
agent/wifi/wifi.go
Normal file
40
agent/wifi/wifi.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Package wifi collects only currently associated station interfaces. Collection
|
||||
// failures are empty snapshots, never cached connected state.
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
)
|
||||
|
||||
type commandRunner func(context.Context, string, ...string) ([]byte, error)
|
||||
|
||||
func run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Env = append(os.Environ(), "LC_ALL=C", "LANG=C")
|
||||
cmd.WaitDelay = 100 * time.Millisecond
|
||||
return cmd.Output()
|
||||
}
|
||||
|
||||
// validSSID omits non-UTF-8 SSIDs: 802.11 permits arbitrary octets, but CBOR
|
||||
// text strings require UTF-8. Metadata must never invalidate the whole response.
|
||||
func validSSID(ssid string) string {
|
||||
if !utf8.ValidString(ssid) {
|
||||
return ""
|
||||
}
|
||||
return ssid
|
||||
}
|
||||
|
||||
// Collect uses a single deadline across interface queries where supported.
|
||||
// Unsupported platforms and denied association access produce no readings;
|
||||
// later polls retry.
|
||||
func Collect() map[string]system.WiFi {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
return collect(ctx)
|
||||
}
|
||||
43
agent/wifi/wifi_darwin.go
Normal file
43
agent/wifi/wifi_darwin.go
Normal file
@@ -0,0 +1,43 @@
|
||||
//go:build darwin
|
||||
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
)
|
||||
|
||||
// JXA exposes the system CoreWLAN framework without cgo, private airport tools,
|
||||
// sudo, or scanning nearby networks. SSID can be redacted by macOS privacy rules.
|
||||
const coreWLANScript = `ObjC.import('CoreWLAN');
|
||||
var result = {};
|
||||
var interfaces = $.CWWiFiClient.sharedWiFiClient.interfaces;
|
||||
if (interfaces) {
|
||||
for (var i = 0; i < interfaces.count; i++) {
|
||||
var iface = interfaces.objectAtIndex(i);
|
||||
if (!iface.powerOn || Number(iface.interfaceMode) !== 1) continue;
|
||||
var name = ObjC.unwrap(iface.interfaceName);
|
||||
if (!name) continue;
|
||||
var reading = {};
|
||||
var ssid = ObjC.unwrap(iface.ssid);
|
||||
if (ssid) reading.s = ssid;
|
||||
var signal = Number(iface.rssiValue);
|
||||
if (signal >= -150 && signal < 0) reading.r = signal;
|
||||
result[name] = reading;
|
||||
}
|
||||
}
|
||||
JSON.stringify(result);`
|
||||
|
||||
func collect(ctx context.Context) map[string]system.WiFi {
|
||||
output, err := run(ctx, "/usr/bin/osascript", "-l", "JavaScript", "-e", coreWLANScript)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var result map[string]system.WiFi
|
||||
if json.Unmarshal(output, &result) != nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
78
agent/wifi/wifi_linux.go
Normal file
78
agent/wifi/wifi_linux.go
Normal file
@@ -0,0 +1,78 @@
|
||||
//go:build linux
|
||||
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
native "github.com/mdlayher/wifi"
|
||||
)
|
||||
|
||||
type linuxClient interface {
|
||||
Interfaces() ([]*native.Interface, error)
|
||||
BSS(*native.Interface) (*native.BSS, error)
|
||||
StationInfo(*native.Interface) ([]*native.StationInfo, error)
|
||||
SetDeadline(time.Time) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
func collect(ctx context.Context) map[string]system.WiFi {
|
||||
client, err := native.New()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer client.Close()
|
||||
return collectLinux(ctx, client)
|
||||
}
|
||||
|
||||
func collectLinux(ctx context.Context, client linuxClient) map[string]system.WiFi {
|
||||
result := make(map[string]system.WiFi)
|
||||
if ctx.Err() != nil {
|
||||
return result
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if client.SetDeadline(deadline) != nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
interfaces, err := client.Interfaces()
|
||||
if err != nil {
|
||||
return result
|
||||
}
|
||||
for _, iface := range interfaces {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if iface == nil || iface.Type != native.InterfaceTypeStation || iface.Name == "" {
|
||||
continue
|
||||
}
|
||||
// GET_SCAN reads the kernel's BSS cache, without triggering a scan.
|
||||
// Only the explicit associated status proves a current connection.
|
||||
bss, err := client.BSS(iface)
|
||||
if err != nil || bss == nil || bss.Status != native.BSSStatusAssociated {
|
||||
continue
|
||||
}
|
||||
reading := system.WiFi{SSID: validSSID(bss.SSID)}
|
||||
// Station statistics may require permissions unavailable in default
|
||||
// containers. Keep association even when RSSI cannot be read. Do not
|
||||
// substitute cached scan signal, which may be arbitrarily old.
|
||||
stations, err := client.StationInfo(iface)
|
||||
if err == nil {
|
||||
for _, station := range stations {
|
||||
if station == nil || len(bss.BSSID) == 0 || !bytes.Equal(station.HardwareAddr, bss.BSSID) {
|
||||
continue
|
||||
}
|
||||
signal := float64(station.Signal)
|
||||
if signal >= -150 && signal < 0 {
|
||||
reading.Signal = &signal
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
result[iface.Name] = reading
|
||||
}
|
||||
return result
|
||||
}
|
||||
133
agent/wifi/wifi_linux_test.go
Normal file
133
agent/wifi/wifi_linux_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
//go:build linux
|
||||
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
native "github.com/mdlayher/wifi"
|
||||
)
|
||||
|
||||
type fakeLinuxClient struct {
|
||||
interfaces []*native.Interface
|
||||
bss map[string]*native.BSS
|
||||
stations map[string][]*native.StationInfo
|
||||
interfacesErr, bssErr, stationErr, deadlineErr error
|
||||
deadline time.Time
|
||||
stationCalls int
|
||||
}
|
||||
|
||||
func (f *fakeLinuxClient) Interfaces() ([]*native.Interface, error) {
|
||||
return f.interfaces, f.interfacesErr
|
||||
}
|
||||
func (f *fakeLinuxClient) BSS(i *native.Interface) (*native.BSS, error) {
|
||||
return f.bss[i.Name], f.bssErr
|
||||
}
|
||||
func (f *fakeLinuxClient) StationInfo(i *native.Interface) ([]*native.StationInfo, error) {
|
||||
f.stationCalls++
|
||||
return f.stations[i.Name], f.stationErr
|
||||
}
|
||||
func (f *fakeLinuxClient) SetDeadline(d time.Time) error { f.deadline = d; return f.deadlineErr }
|
||||
func (f *fakeLinuxClient) Close() error { return nil }
|
||||
|
||||
func connectedClient() *fakeLinuxClient {
|
||||
mac := net.HardwareAddr{1, 2, 3, 4, 5, 6}
|
||||
return &fakeLinuxClient{
|
||||
interfaces: []*native.Interface{{Name: "wlan0", Type: native.InterfaceTypeStation}},
|
||||
bss: map[string]*native.BSS{"wlan0": {Status: native.BSSStatusAssociated, SSID: "home", BSSID: mac}},
|
||||
stations: map[string][]*native.StationInfo{"wlan0": {{HardwareAddr: mac, Signal: -52}}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxSnapshots(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
modify func(*fakeLinuxClient)
|
||||
want int
|
||||
wantSignal bool
|
||||
}{
|
||||
{"connected", func(f *fakeLinuxClient) {}, 1, true},
|
||||
{"multiple", func(f *fakeLinuxClient) {
|
||||
f.interfaces = append(f.interfaces, &native.Interface{Name: "wlan1", Type: native.InterfaceTypeStation})
|
||||
f.bss["wlan1"] = f.bss["wlan0"]
|
||||
}, 2, true},
|
||||
{"unsupported", func(f *fakeLinuxClient) { f.interfacesErr = errors.New("unsupported") }, 0, false},
|
||||
{"association denied", func(f *fakeLinuxClient) { f.bssErr = errors.New("denied") }, 0, false},
|
||||
{"disconnected", func(f *fakeLinuxClient) { f.bss["wlan0"] = nil }, 0, false},
|
||||
{"authenticated only", func(f *fakeLinuxClient) { f.bss["wlan0"].Status = native.BSSStatusAuthenticated }, 0, false},
|
||||
{"cached nearby BSS", func(f *fakeLinuxClient) { f.bss["wlan0"].Status = native.BSSStatusNotAssociated }, 0, false},
|
||||
{"access point", func(f *fakeLinuxClient) { f.interfaces[0].Type = native.InterfaceTypeAP }, 0, false},
|
||||
{"ad hoc", func(f *fakeLinuxClient) { f.bss["wlan0"].Status = native.BSSStatusIBSSJoined }, 0, false},
|
||||
{"station permission denied", func(f *fakeLinuxClient) {
|
||||
f.stationErr = errors.New("permission denied")
|
||||
f.bss["wlan0"].Signal = -4200
|
||||
}, 1, false},
|
||||
{"no station data", func(f *fakeLinuxClient) { f.stations = nil }, 1, false},
|
||||
{"different AP", func(f *fakeLinuxClient) { f.stations["wlan0"][0].HardwareAddr = net.HardwareAddr{9, 8, 7, 6, 5, 4} }, 1, false},
|
||||
{"missing signal", func(f *fakeLinuxClient) { f.stations["wlan0"][0].Signal = 0 }, 1, false},
|
||||
{"invalid signal", func(f *fakeLinuxClient) { f.stations["wlan0"][0].Signal = -151 }, 1, false},
|
||||
{"deadline failure", func(f *fakeLinuxClient) { f.deadlineErr = errors.New("deadline") }, 0, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f := connectedClient()
|
||||
tc.modify(f)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
got := collectLinux(ctx, f)
|
||||
if len(got) != tc.want {
|
||||
t.Fatalf("got %#v", got)
|
||||
}
|
||||
if tc.want > 0 && (got["wlan0"].Signal != nil) != tc.wantSignal {
|
||||
t.Fatalf("signal: %#v", got["wlan0"])
|
||||
}
|
||||
if tc.wantSignal && *got["wlan0"].Signal != -52 {
|
||||
t.Fatal(got)
|
||||
}
|
||||
if tc.want == 0 && f.stationCalls != 0 {
|
||||
t.Fatal("queried station without association")
|
||||
}
|
||||
deadline, _ := ctx.Deadline()
|
||||
if f.deadline != deadline {
|
||||
t.Fatal("deadline not shared")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconnect(t *testing.T) {
|
||||
f := connectedClient()
|
||||
if len(collectLinux(context.Background(), f)) != 1 {
|
||||
t.Fatal("initial")
|
||||
}
|
||||
f.bss["wlan0"].Status = native.BSSStatusNotAssociated
|
||||
if len(collectLinux(context.Background(), f)) != 0 {
|
||||
t.Fatal("stale association")
|
||||
}
|
||||
f.bss["wlan0"].Status = native.BSSStatusAssociated
|
||||
f.bss["wlan0"].SSID = "new"
|
||||
if collectLinux(context.Background(), f)["wlan0"].SSID != "new" {
|
||||
t.Fatal("stale SSID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxInvalidSSID(t *testing.T) {
|
||||
f := connectedClient()
|
||||
f.bss["wlan0"].SSID = "raw\xff"
|
||||
got := collectLinux(context.Background(), f)
|
||||
if len(got) != 1 || got["wlan0"].SSID != "" || got["wlan0"].Signal == nil {
|
||||
t.Fatal(got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxCancelled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
f := connectedClient()
|
||||
if len(collectLinux(ctx, f)) != 0 || f.stationCalls != 0 {
|
||||
t.Fatal("ignored cancellation")
|
||||
}
|
||||
}
|
||||
35
agent/wifi/wifi_test.go
Normal file
35
agent/wifi/wifi_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
)
|
||||
|
||||
func TestSSIDWireSafety(t *testing.T) {
|
||||
for _, tc := range []struct{ input, want string }{
|
||||
{"home", "home"}, {"网络 café", "网络 café"}, {"", ""},
|
||||
{"raw\xffssid", ""}, {"truncated\xe2\x82", ""},
|
||||
} {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
ssid := validSSID(tc.input)
|
||||
if ssid != tc.want {
|
||||
t.Fatalf("got %q, want %q", ssid, tc.want)
|
||||
}
|
||||
signal := -50.0
|
||||
payload := map[string]system.WiFi{"wlan0": {SSID: ssid, Signal: &signal}}
|
||||
wire, err := cbor.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded map[string]system.WiFi
|
||||
if err := cbor.Unmarshal(wire, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decoded["wlan0"].Signal == nil || *decoded["wlan0"].Signal != signal || decoded["wlan0"].SSID != tc.want {
|
||||
t.Fatal(decoded)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
11
agent/wifi/wifi_unsupported.go
Normal file
11
agent/wifi/wifi_unsupported.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !linux && !windows && !darwin
|
||||
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
)
|
||||
|
||||
func collect(context.Context) map[string]system.WiFi { return nil }
|
||||
99
agent/wifi/wifi_windows.go
Normal file
99
agent/wifi/wifi_windows.go
Normal file
@@ -0,0 +1,99 @@
|
||||
//go:build windows
|
||||
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"unsafe"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var wlan = windows.NewLazySystemDLL("wlanapi.dll")
|
||||
var wlanOpen = wlan.NewProc("WlanOpenHandle")
|
||||
var wlanClose = wlan.NewProc("WlanCloseHandle")
|
||||
var wlanEnum = wlan.NewProc("WlanEnumInterfaces")
|
||||
var wlanQuery = wlan.NewProc("WlanQueryInterface")
|
||||
var wlanFree = wlan.NewProc("WlanFreeMemory")
|
||||
|
||||
type wlanInterface struct {
|
||||
GUID windows.GUID
|
||||
Description [256]uint16
|
||||
State uint32
|
||||
}
|
||||
|
||||
type wlanConnection struct {
|
||||
State uint32
|
||||
Mode uint32
|
||||
Profile [256]uint16
|
||||
SSIDLength uint32
|
||||
SSID [32]byte
|
||||
// Only the prefix through DOT11_SSID is read.
|
||||
}
|
||||
|
||||
func collect(ctx context.Context) map[string]system.WiFi {
|
||||
result := make(map[string]system.WiFi)
|
||||
for _, proc := range []*windows.LazyProc{wlanOpen, wlanClose, wlanEnum, wlanQuery, wlanFree} {
|
||||
if proc.Find() != nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
var handle windows.Handle
|
||||
var version uint32
|
||||
if rc, _, _ := wlanOpen.Call(2, 0, uintptr(unsafe.Pointer(&version)), uintptr(unsafe.Pointer(&handle))); rc != 0 {
|
||||
return result
|
||||
}
|
||||
defer wlanClose.Call(uintptr(handle), 0)
|
||||
var list unsafe.Pointer
|
||||
if rc, _, _ := wlanEnum.Call(uintptr(handle), 0, uintptr(unsafe.Pointer(&list))); rc != 0 || list == nil {
|
||||
return result
|
||||
}
|
||||
defer wlanFree.Call(uintptr(list))
|
||||
count := *(*uint32)(list)
|
||||
if count > 1024 {
|
||||
return result
|
||||
}
|
||||
interfaces := unsafe.Slice((*wlanInterface)(unsafe.Add(list, 8)), int(count))
|
||||
for _, iface := range interfaces {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if iface.State != 1 {
|
||||
continue
|
||||
} // wlan_interface_state_connected
|
||||
reading := system.WiFi{}
|
||||
// SSID access may be denied by location privacy policy. Association comes
|
||||
// from the interface state, so missing SSID does not suppress valid RSSI.
|
||||
if data, size := queryWLAN(handle, &iface.GUID, 7); data != nil {
|
||||
if size >= uint32(unsafe.Sizeof(wlanConnection{})) {
|
||||
connection := (*wlanConnection)(data)
|
||||
if connection.State == 1 && connection.SSIDLength <= 32 {
|
||||
reading.SSID = validSSID(string(connection.SSID[:connection.SSIDLength]))
|
||||
}
|
||||
}
|
||||
wlanFree.Call(uintptr(data))
|
||||
}
|
||||
// Native RSSI LONG, not the quality percentage in association attributes.
|
||||
if data, size := queryWLAN(handle, &iface.GUID, 0x10000102); data != nil {
|
||||
if size >= 4 {
|
||||
signal := float64(*(*int32)(data))
|
||||
if signal >= -150 && signal < 0 {
|
||||
reading.Signal = &signal
|
||||
}
|
||||
}
|
||||
wlanFree.Call(uintptr(data))
|
||||
}
|
||||
result[iface.GUID.String()] = reading
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func queryWLAN(handle windows.Handle, guid *windows.GUID, opcode uintptr) (unsafe.Pointer, uint32) {
|
||||
var data unsafe.Pointer
|
||||
var size uint32
|
||||
if rc, _, _ := wlanQuery.Call(uintptr(handle), uintptr(unsafe.Pointer(guid)), opcode, 0, uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&data)), 0); rc != 0 {
|
||||
return nil, 0
|
||||
}
|
||||
return data, size
|
||||
}
|
||||
Reference in New Issue
Block a user