From a4b81bb6e9c695ce188d9cdff1e57e1711c966d1 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 31 May 2026 22:33:59 +0200
Subject: [PATCH 01/16] Feature: WiFi Channel Width + LiveChart2 migration
---
.../LvlChartsHeaderConverter.cs | 24 --
.../NETworkManager.Converters.csproj | 2 -
.../WiFiDBMReverseConverter.cs | 18 -
.../Resources/Strings.Designer.cs | 11 +-
.../Resources/Strings.resx | 3 +
Source/NETworkManager.Models/Network/WiFi.cs | 53 ++-
.../Network/WiFiNetworkInfo.cs | 5 +
.../NETworkManager.Models/Network/WlanApi.cs | 404 ++++++++++++++++++
.../LiveChartsWiFiChannelTooltip.xaml | 37 ++
.../LiveChartsWiFiChannelTooltip.xaml.cs | 95 ++++
.../Controls/LvlChartsWiFiChannelTooltip.xaml | 57 ---
.../LvlChartsWiFiChannelTooltip.xaml.cs | 35 --
.../Controls/WiFiChannelPoint.cs | 21 +
Source/NETworkManager/NETworkManager.csproj | 1 -
.../ViewModels/WiFiViewModel.cs | 404 +++++++++++++++---
Source/NETworkManager/Views/WiFiView.xaml | 232 +++++-----
Source/NETworkManager/Views/WiFiView.xaml.cs | 24 ++
17 files changed, 1097 insertions(+), 329 deletions(-)
delete mode 100644 Source/NETworkManager.Converters/LvlChartsHeaderConverter.cs
delete mode 100644 Source/NETworkManager.Converters/WiFiDBMReverseConverter.cs
create mode 100644 Source/NETworkManager.Models/Network/WlanApi.cs
create mode 100644 Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml
create mode 100644 Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
delete mode 100644 Source/NETworkManager/Controls/LvlChartsWiFiChannelTooltip.xaml
delete mode 100644 Source/NETworkManager/Controls/LvlChartsWiFiChannelTooltip.xaml.cs
create mode 100644 Source/NETworkManager/Controls/WiFiChannelPoint.cs
diff --git a/Source/NETworkManager.Converters/LvlChartsHeaderConverter.cs b/Source/NETworkManager.Converters/LvlChartsHeaderConverter.cs
deleted file mode 100644
index d89474cab7..0000000000
--- a/Source/NETworkManager.Converters/LvlChartsHeaderConverter.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows.Data;
-using LiveCharts.Wpf;
-
-namespace NETworkManager.Converters;
-
-public sealed class LvlChartsHeaderConverter : IValueConverter
-{
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is not TooltipData info)
- return "-/-";
-
- var index = info.SharedValue ?? -1;
-
- return Math.Abs(index - -1) < 0 ? "-/-" : info.XFormatter.Invoke(index);
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager.Converters/NETworkManager.Converters.csproj b/Source/NETworkManager.Converters/NETworkManager.Converters.csproj
index 0457284fc5..82df0afa01 100644
--- a/Source/NETworkManager.Converters/NETworkManager.Converters.csproj
+++ b/Source/NETworkManager.Converters/NETworkManager.Converters.csproj
@@ -27,8 +27,6 @@
-
-
diff --git a/Source/NETworkManager.Converters/WiFiDBMReverseConverter.cs b/Source/NETworkManager.Converters/WiFiDBMReverseConverter.cs
deleted file mode 100644
index 7160f17813..0000000000
--- a/Source/NETworkManager.Converters/WiFiDBMReverseConverter.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows.Data;
-
-namespace NETworkManager.Converters;
-
-public sealed class WiFiDBMReverseConverter : IValueConverter
-{
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- return !(value is double) ? "-/-" : $"-{100 - (double)value} dBm";
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index 08463b220d..c03df53bca 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -1673,7 +1673,16 @@ public static string Channels {
return ResourceManager.GetString("Channels", resourceCulture);
}
}
-
+
+ ///
+ /// Looks up a localized string similar to Channel width.
+ ///
+ public static string ChannelWidth {
+ get {
+ return ResourceManager.GetString("ChannelWidth", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Chassis Id.
///
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index 886495042e..7d10eb50e0 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -2274,6 +2274,9 @@ $$hostname$$ --> Hostname
Channel
+
+ Channel width
+
2.4 GHz
diff --git a/Source/NETworkManager.Models/Network/WiFi.cs b/Source/NETworkManager.Models/Network/WiFi.cs
index 0066ee502a..94fe53bb61 100644
--- a/Source/NETworkManager.Models/Network/WiFi.cs
+++ b/Source/NETworkManager.Models/Network/WiFi.cs
@@ -1,11 +1,13 @@
using log4net;
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.Devices.WiFi;
using Windows.Networking.Connectivity;
using Windows.Security.Credentials;
+using Microsoft.CodeAnalysis.Emit;
using NETworkManager.Models.Lookup;
namespace NETworkManager.Models.Network;
@@ -68,18 +70,25 @@ public static async Task GetNetworksAsync(WiFiAdapter adapt
// Try to get the current connected Wi-Fi network of this network adapter
var (_, bssid) = TryGetConnectedNetworkFromWiFiAdapter(adapter.NetworkAdapter.NetworkAdapterId.ToString());
+ // The WinRT API does not expose the channel bandwidth, so it is read from the native
+ // BSS list (wlanapi.dll) and matched by BSSID below. Failures yield an empty map and we
+ // fall back to a heuristic per network.
+ var channelWidths = WlanApi.GetBssChannelWidths(adapter.NetworkAdapter.NetworkAdapterId);
+
var wifiNetworkInfos = new List();
foreach (var availableNetwork in adapter.NetworkReport.AvailableNetworks)
{
var channelFrequencyInGigahertz = ConvertChannelFrequencyToGigahertz(availableNetwork.ChannelCenterFrequencyInKilohertz);
+ var radio = GetWiFiRadioFromChannelFrequency(channelFrequencyInGigahertz);
var wifiNetworkInfo = new WiFiNetworkInfo
{
AvailableNetwork = availableNetwork,
- Radio = GetWiFiRadioFromChannelFrequency(channelFrequencyInGigahertz),
+ Radio = radio,
ChannelCenterFrequencyInGigahertz = channelFrequencyInGigahertz,
Channel = GetChannelFromChannelFrequency(channelFrequencyInGigahertz),
+ ChannelBandwidth = GetChannelBandwidth(channelWidths, availableNetwork.Bssid, radio, availableNetwork.PhyKind),
IsHidden = string.IsNullOrEmpty(availableNetwork.Ssid),
IsConnected = availableNetwork.Bssid.Equals(bssid, StringComparison.OrdinalIgnoreCase),
NetworkAuthenticationType = GetHumanReadableNetworkAuthenticationType(availableNetwork.SecuritySettings.NetworkAuthenticationType),
@@ -258,8 +267,8 @@ public static async Task IsWpsAvailable(WiFiAdapter adapter, WiFiAvailable
/// Get the Wi-Fi channel from channel frequency.
///
/// Input like 2.422 or 5.240.
- /// WiFi channel like 3 or 48.
- public static int GetChannelFromChannelFrequency(double gigahertz)
+ /// Wi-Fi channel like 3 or 48.
+ private static int GetChannelFromChannelFrequency(double gigahertz)
{
return gigahertz switch
{
@@ -313,7 +322,7 @@ public static int GetChannelFromChannelFrequency(double gigahertz)
///
/// Frequency in kilohertz like 2422000 or 5240000.
/// Frequency in gigahertz like 2.422 or 5.240.
- public static double ConvertChannelFrequencyToGigahertz(int kilohertz)
+ private static double ConvertChannelFrequencyToGigahertz(int kilohertz)
{
return Convert.ToDouble(kilohertz) / 1000 / 1000;
}
@@ -323,7 +332,7 @@ public static double ConvertChannelFrequencyToGigahertz(int kilohertz)
///
/// Frequency in gigahertz like 2.412 or 5.180.
/// Radio like 2.4 GHz, 5 GHz, etc. as .
- public static WiFiRadio GetWiFiRadioFromChannelFrequency(double gigahertz)
+ private static WiFiRadio GetWiFiRadioFromChannelFrequency(double gigahertz)
{
return gigahertz switch
{
@@ -334,6 +343,40 @@ public static WiFiRadio GetWiFiRadioFromChannelFrequency(double gigahertz)
};
}
+ ///
+ /// Determines the channel bandwidth (MHz) for a network. Prefers the value parsed from the
+ /// native BSS list (); if unavailable, falls back to a heuristic based
+ /// on the radio band and PHY kind, and finally to 20 MHz.
+ ///
+ private static int GetChannelBandwidth(IReadOnlyDictionary channelWidths, string networkBssid,
+ WiFiRadio radio, WiFiPhyKind phyKind)
+ {
+ if (!string.IsNullOrEmpty(networkBssid) && channelWidths.TryGetValue(networkBssid, out var width) && width > 0)
+ return width;
+
+ return GetHeuristicChannelBandwidth(radio, phyKind);
+ }
+
+ ///
+ /// Estimates the channel bandwidth (MHz) from the radio band and PHY kind when the exact
+ /// value could not be read from the BSS list.
+ ///
+ private static int GetHeuristicChannelBandwidth(WiFiRadio radio, WiFiPhyKind phyKind)
+ {
+ return radio switch
+ {
+ WiFiRadio.GHz2dot4 => phyKind == WiFiPhyKind.HT ? 40 : 20,
+ WiFiRadio.GHz5 => phyKind switch
+ {
+ WiFiPhyKind.Vht or WiFiPhyKind.HE or WiFiPhyKind.Eht => 80,
+ WiFiPhyKind.HT => 40,
+ _ => 20
+ },
+ WiFiRadio.GHz6 => phyKind is WiFiPhyKind.HE or WiFiPhyKind.Eht ? 160 : 20,
+ _ => 20
+ };
+ }
+
///
/// Get the human-readable network authentication type.
///
diff --git a/Source/NETworkManager.Models/Network/WiFiNetworkInfo.cs b/Source/NETworkManager.Models/Network/WiFiNetworkInfo.cs
index 9f49908d03..e4bd401946 100644
--- a/Source/NETworkManager.Models/Network/WiFiNetworkInfo.cs
+++ b/Source/NETworkManager.Models/Network/WiFiNetworkInfo.cs
@@ -37,6 +37,11 @@ public WiFiNetworkInfo()
///
public int Channel { get; set; }
+ ///
+ /// The channel bandwidth in MHz (e.g. 20, 40, 80, 160). A value of 0 means unknown.
+ ///
+ public int ChannelBandwidth { get; set; }
+
///
/// Indicates if the Wi-Fi network Ssid is hidden.
///
diff --git a/Source/NETworkManager.Models/Network/WlanApi.cs b/Source/NETworkManager.Models/Network/WlanApi.cs
new file mode 100644
index 0000000000..6979946b30
--- /dev/null
+++ b/Source/NETworkManager.Models/Network/WlanApi.cs
@@ -0,0 +1,404 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using log4net;
+
+namespace NETworkManager.Models.Network;
+
+///
+/// Native Wi-Fi (wlanapi.dll) interop used to determine the channel bandwidth
+/// (20 / 40 / 80 / 160 MHz) of nearby access points.
+///
+/// The Windows Runtime Wi-Fi API () does
+/// not expose the channel width. The native BSS list (WlanGetNetworkBssList) however
+/// returns the raw 802.11 information elements (IEs) of each beacon / probe response. The width
+/// is parsed from the HT-Operation (802.11n), VHT-Operation (802.11ac) and HE-Operation
+/// (802.11ax / 6 GHz) elements.
+///
+/// All calls are wrapped in try/catch; on any failure an empty result is returned so the caller
+/// can fall back to a heuristic (see ).
+///
+public static class WlanApi
+{
+ private static readonly ILog Log = LogManager.GetLogger(typeof(WlanApi));
+
+ private const uint WlanApiVersion2 = 0x00000002;
+ private const uint ErrorSuccess = 0;
+
+ // 802.11 information element identifiers.
+ private const byte EidHtOperation = 61;
+ private const byte EidVhtOperation = 192;
+ private const byte EidExtension = 255;
+ private const byte ExtEidHeOperation = 36;
+
+ private enum DOT11_BSS_TYPE
+ {
+ // ReSharper disable UnusedMember.Local
+ Infrastructure = 1,
+ Independent = 2,
+ Any = 3
+ // ReSharper restore UnusedMember.Local
+ }
+
+ [DllImport("wlanapi.dll")]
+ private static extern uint WlanOpenHandle(uint dwClientVersion, IntPtr pReserved,
+ out uint pdwNegotiatedVersion, out IntPtr phClientHandle);
+
+ [DllImport("wlanapi.dll")]
+ private static extern uint WlanCloseHandle(IntPtr hClientHandle, IntPtr pReserved);
+
+ [DllImport("wlanapi.dll")]
+ private static extern void WlanFreeMemory(IntPtr pMemory);
+
+ [DllImport("wlanapi.dll")]
+ private static extern uint WlanGetNetworkBssList(IntPtr hClientHandle, ref Guid pInterfaceGuid,
+ IntPtr pDot11Ssid, DOT11_BSS_TYPE dot11BssType, [MarshalAs(UnmanagedType.Bool)] bool bSecurityEnabled,
+ IntPtr pReserved, out IntPtr ppWlanBssList);
+
+ [DllImport("wlanapi.dll")]
+ private static extern uint WlanEnumInterfaces(IntPtr hClientHandle, IntPtr pReserved,
+ out IntPtr ppInterfaceList);
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ private struct WLAN_INTERFACE_INFO
+ {
+ public Guid InterfaceGuid;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
+ public string strInterfaceDescription;
+
+ public uint isState;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct DOT11_SSID
+ {
+ public uint uSSIDLength;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
+ public byte[] ucSSID;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct WLAN_RATE_SET
+ {
+ public uint uRateSetLength;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 126)]
+ public ushort[] usRateSet;
+ }
+
+ ///
+ /// Mirror of the native WLAN_BSS_ENTRY structure. Only ,
+ /// and are used; the remaining fields are
+ /// required so the layout (and therefore ) matches the
+ /// native struct exactly.
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ private struct WLAN_BSS_ENTRY
+ {
+ public DOT11_SSID dot11Ssid;
+ public uint uPhyId;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
+ public byte[] dot11Bssid;
+
+ public uint dot11BssType;
+ public uint dot11BssPhyType;
+ public int lRssi;
+ public uint uLinkQuality;
+
+ // Native type is BOOLEAN (1 byte), not BOOL (4 bytes).
+ [MarshalAs(UnmanagedType.U1)]
+ public bool bInRegDomain;
+
+ public ushort usBeaconPeriod;
+ public ulong ullTimestamp;
+ public ulong ullHostTimestamp;
+ public ushort usCapabilityInformation;
+ public uint ulChCenterFrequency;
+ public WLAN_RATE_SET wlanRateSet;
+ public uint ulIeOffset;
+ public uint ulIeSize;
+ }
+
+ ///
+ /// Returns a map of BSSID (lowercase, colon-separated, e.g. aa:bb:cc:dd:ee:ff) to the
+ /// channel bandwidth in MHz for all access points visible to the given Wi-Fi interface.
+ /// BSSIDs whose width could not be parsed are omitted from the result.
+ ///
+ /// GUID of the Wi-Fi interface (NetworkAdapter.NetworkAdapterId).
+ /// A (possibly empty) map of BSSID to channel bandwidth in MHz.
+ public static Dictionary GetBssChannelWidths(Guid interfaceGuid)
+ {
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ var clientHandle = IntPtr.Zero;
+
+ try
+ {
+ var ret = WlanOpenHandle(WlanApiVersion2, IntPtr.Zero, out _, out clientHandle);
+
+ if (ret != ErrorSuccess)
+ {
+ Log.Warn($"WlanOpenHandle failed with error code {ret}.");
+ return result;
+ }
+
+ // The WinRT NetworkAdapterId normally matches the native WLAN interface GUID. If it does
+ // not (driver/OS specifics), fall back to enumerating all WLAN interfaces and merging
+ // their BSS lists (matching is done by BSSID, so mixing interfaces is harmless).
+ if (!TryQueryBssList(clientHandle, interfaceGuid, result))
+ foreach (var guid in EnumerateInterfaces(clientHandle))
+ TryQueryBssList(clientHandle, guid, result);
+ }
+ catch (Exception ex)
+ {
+ Log.Error("Error while reading the Wi-Fi BSS list via wlanapi.dll.", ex);
+ }
+ finally
+ {
+ if (clientHandle != IntPtr.Zero)
+ WlanCloseHandle(clientHandle, IntPtr.Zero);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Queries the BSS list for a single WLAN interface and adds the parsed channel widths to
+ /// . Returns if the query succeeded.
+ ///
+ private static bool TryQueryBssList(IntPtr clientHandle, Guid interfaceGuid, Dictionary result)
+ {
+ var bssListPtr = IntPtr.Zero;
+
+ try
+ {
+ var guid = interfaceGuid;
+
+ var ret = WlanGetNetworkBssList(clientHandle, ref guid, IntPtr.Zero, DOT11_BSS_TYPE.Any, false,
+ IntPtr.Zero, out bssListPtr);
+
+ if (ret != ErrorSuccess || bssListPtr == IntPtr.Zero)
+ {
+ Log.Warn($"WlanGetNetworkBssList failed with error code {ret}.");
+ return false;
+ }
+
+ // WLAN_BSS_LIST: DWORD dwTotalSize; DWORD dwNumberOfItems; WLAN_BSS_ENTRY[] entries.
+ // The entry array starts after the two DWORDs (8-byte aligned).
+ var totalSize = (long)(uint)Marshal.ReadInt32(bssListPtr, 0);
+ var numberOfItems = Marshal.ReadInt32(bssListPtr, 4);
+ var entrySize = Marshal.SizeOf();
+
+ for (var i = 0; i < numberOfItems; i++)
+ {
+ var entryByteOffset = 8 + i * entrySize;
+ var entry = Marshal.PtrToStructure(IntPtr.Add(bssListPtr, entryByteOffset));
+
+ // Skip invalid entries and guard against reading outside the allocated buffer
+ // (defensive: a struct layout mismatch would otherwise risk an invalid read).
+ // The documented maximum IE data blob size is 2,324 bytes.
+ if (entry.dot11Bssid is not { Length: 6 } || entry.ulIeSize is 0 or > 2324 || entry.ulIeOffset == 0)
+ continue;
+
+ var ieStart = entryByteOffset + (long)entry.ulIeOffset;
+
+ if (ieStart + entry.ulIeSize > totalSize)
+ continue;
+
+ var ie = new byte[entry.ulIeSize];
+ Marshal.Copy(IntPtr.Add(bssListPtr, (int)ieStart), ie, 0, (int)entry.ulIeSize);
+
+ var width = GetBandwidthFromInformationElements(ie);
+
+ if (width > 0)
+ result[FormatBssid(entry.dot11Bssid)] = width;
+ }
+
+ return true;
+ }
+ finally
+ {
+ if (bssListPtr != IntPtr.Zero)
+ WlanFreeMemory(bssListPtr);
+ }
+ }
+
+ ///
+ /// Returns the GUIDs of all WLAN interfaces known to the native Wi-Fi service.
+ ///
+ private static List EnumerateInterfaces(IntPtr clientHandle)
+ {
+ var guids = new List();
+
+ var listPtr = IntPtr.Zero;
+
+ try
+ {
+ if (WlanEnumInterfaces(clientHandle, IntPtr.Zero, out listPtr) != ErrorSuccess || listPtr == IntPtr.Zero)
+ return guids;
+
+ // WLAN_INTERFACE_INFO_LIST: DWORD dwNumberOfItems; DWORD dwIndex; WLAN_INTERFACE_INFO[] entries.
+ var numberOfItems = Marshal.ReadInt32(listPtr, 0);
+ var infoSize = Marshal.SizeOf();
+ var arrayPtr = IntPtr.Add(listPtr, 8);
+
+ for (var i = 0; i < numberOfItems; i++)
+ {
+ var info = Marshal.PtrToStructure(IntPtr.Add(arrayPtr, i * infoSize));
+ guids.Add(info.InterfaceGuid);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error("Error while enumerating Wi-Fi interfaces via wlanapi.dll.", ex);
+ }
+ finally
+ {
+ if (listPtr != IntPtr.Zero)
+ WlanFreeMemory(listPtr);
+ }
+
+ return guids;
+ }
+
+ ///
+ /// Walks the TLV-encoded 802.11 information elements and returns the widest channel
+ /// bandwidth (MHz) advertised by the HT-, VHT- or HE-Operation element. Returns 0 if none
+ /// of these elements are present.
+ ///
+ private static int GetBandwidthFromInformationElements(byte[] ie)
+ {
+ int htWidth = 0, vhtWidth = 0, heWidth = 0;
+
+ var pos = 0;
+
+ while (pos + 2 <= ie.Length)
+ {
+ var id = ie[pos];
+ int len = ie[pos + 1];
+ var data = pos + 2;
+
+ if (data + len > ie.Length)
+ break;
+
+ switch (id)
+ {
+ case EidHtOperation when len >= 2:
+ {
+ // HT Operation Information byte 0: bits 0-1 secondary channel offset,
+ // bit 2 STA channel width (0 = 20 MHz, 1 = any/40 MHz).
+ var info = ie[data + 1];
+ var secondaryChannelOffset = info & 0x03;
+ var staChannelWidth = (info >> 2) & 0x01;
+ htWidth = staChannelWidth == 1 && secondaryChannelOffset != 0 ? 40 : 20;
+ break;
+ }
+ case EidVhtOperation when len >= 3:
+ vhtWidth = ParseVhtWidth(ie[data], ie[data + 1], ie[data + 2], htWidth);
+ break;
+ case EidExtension when len >= 1 && ie[data] == ExtEidHeOperation:
+ heWidth = ParseHeWidth(ie, data + 1, len - 1, htWidth);
+ break;
+ }
+
+ pos = data + len;
+ }
+
+ return Math.Max(htWidth, Math.Max(vhtWidth, heWidth));
+ }
+
+ ///
+ /// Resolves the channel width from a VHT-Operation channel-width field plus its two center
+ /// frequency segments. Falls back to when the VHT field signals
+ /// "20/40 MHz".
+ ///
+ private static int ParseVhtWidth(int channelWidth, int segment0, int segment1, int htWidth)
+ {
+ switch (channelWidth)
+ {
+ case 0: // 20/40 MHz -> defer to HT
+ return htWidth > 0 ? htWidth : 20;
+ case 1: // 80 MHz, unless the segments indicate 160 / 80+80 MHz
+ if (segment1 == 0)
+ return 80;
+ var diff = Math.Abs(segment0 - segment1);
+ return diff is 8 or > 16 ? 160 : 80;
+ case 2: // 160 MHz (deprecated encoding)
+ case 3: // 80+80 MHz (deprecated encoding) - treated as 160 MHz for display
+ return 160;
+ default:
+ return 80;
+ }
+ }
+
+ ///
+ /// Parses the HE-Operation element (802.11ax). Optional sub-fields (VHT Operation
+ /// Information, 6 GHz Operation Information) are only present when the corresponding bit in
+ /// the HE Operation Parameters is set; their offsets therefore depend on the preceding
+ /// fields. This is a best-effort parse: on any ambiguity 0 is returned so the caller can
+ /// fall back to a heuristic.
+ ///
+ /// The full information element buffer.
+ /// Offset of the HE Operation Parameters (after the extension id byte).
+ /// Remaining length of the HE-Operation element after the extension id.
+ /// Width parsed from the HT-Operation element (used as VHT fallback).
+ private static int ParseHeWidth(byte[] ie, int offset, int length, int htWidth)
+ {
+ // HE Operation Parameters (3) + BSS Color (1) + Basic HE-MCS And NSS Set (2) = 6 bytes minimum.
+ if (length < 6)
+ return 0;
+
+ var end = offset + length;
+
+ // HE Operation Parameters bit layout: B14 VHT Operation Information Present,
+ // B15 Co-Hosted BSS, B16 ER SU Disable, B17 6 GHz Operation Information Present.
+ var heOperationParameters = ie[offset] | (ie[offset + 1] << 8) | (ie[offset + 2] << 16);
+ var vhtOperationInformationPresent = ((heOperationParameters >> 14) & 1) == 1;
+ var coHostedBss = ((heOperationParameters >> 15) & 1) == 1;
+ var sixGhzOperationInformationPresent = ((heOperationParameters >> 17) & 1) == 1;
+
+ var cursor = offset + 3 + 1 + 2; // skip parameters, BSS color, basic HE-MCS
+ var width = 0;
+
+ if (vhtOperationInformationPresent)
+ {
+ if (cursor + 3 <= end)
+ width = Math.Max(width, ParseVhtWidth(ie[cursor], ie[cursor + 1], ie[cursor + 2], htWidth));
+
+ cursor += 3;
+ }
+
+ if (coHostedBss)
+ cursor += 1;
+
+ if (sixGhzOperationInformationPresent && cursor + 2 <= end)
+ {
+ // 6 GHz Operation Information: Primary Channel (1), Control (1), ...
+ // Control byte bits 0-1 = channel width (0 = 20, 1 = 40, 2 = 80, 3 = 160/80+80 MHz).
+ var channelWidth = ie[cursor + 1] & 0x03;
+ var sixGhzWidth = channelWidth switch
+ {
+ 0 => 20,
+ 1 => 40,
+ 2 => 80,
+ 3 => 160,
+ _ => 0
+ };
+ width = Math.Max(width, sixGhzWidth);
+ }
+
+ return width;
+ }
+
+ ///
+ /// Formats a 6-byte BSSID into the lowercase, colon-separated representation used by the
+ /// Windows Runtime API (e.g. aa:bb:cc:dd:ee:ff).
+ ///
+ private static string FormatBssid(byte[] mac)
+ {
+ return $"{mac[0]:x2}:{mac[1]:x2}:{mac[2]:x2}:{mac[3]:x2}:{mac[4]:x2}:{mac[5]:x2}";
+ }
+}
diff --git a/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml
new file mode 100644
index 0000000000..30ae582560
--- /dev/null
+++ b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
new file mode 100644
index 0000000000..6184ce5716
--- /dev/null
+++ b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls.Primitives;
+using System.Windows.Media;
+using LiveChartsCore;
+using LiveChartsCore.Kernel;
+using LiveChartsCore.Kernel.Sketches;
+using LiveChartsCore.SkiaSharpView;
+using LiveChartsCore.SkiaSharpView.Painting;
+using NETworkManager.Localization.Resources;
+using NETworkManager.Models.Network;
+using SkiaSharp;
+
+namespace NETworkManager.Controls;
+
+public partial class LiveChartsWiFiChannelTooltip : IChartTooltip, INotifyPropertyChanged
+{
+ private readonly Popup _popup;
+
+ public LiveChartsWiFiChannelTooltip()
+ {
+ InitializeComponent();
+ DataContext = this;
+ _popup = new Popup
+ {
+ AllowsTransparency = true,
+ Placement = PlacementMode.MousePoint,
+ StaysOpen = true,
+ Child = this
+ };
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public ObservableCollection TooltipEntries { get; } = [];
+
+ public void Show(IEnumerable tooltipPoints, Chart chart)
+ {
+ // Each network is drawn as a trapezoid (multiple points). List every network once,
+ // regardless of which vertex the pointer is closest to, so the tooltip works across the
+ // whole trapezoid and not only at its center.
+ var seenNetworks = new HashSet();
+
+ TooltipEntries.Clear();
+
+ foreach (var point in tooltipPoints)
+ {
+ if (point.Context.DataSource is not WiFiChannelPoint info)
+ continue;
+
+ var network = info.Network;
+
+ if (!seenNetworks.Add(network))
+ continue;
+
+ var ssid = network.IsHidden ? Strings.HiddenNetwork : network.AvailableNetwork.Ssid;
+ var detail =
+ $"{network.AvailableNetwork.NetworkRssiInDecibelMilliwatts} dBm · {Strings.Channel} {network.Channel} · {network.ChannelBandwidth} MHz";
+
+ TooltipEntries.Add(new TooltipEntry(SkColorToBrush(GetSeriesColor(point)), ssid, detail));
+ }
+
+ if (TooltipEntries.Count == 0)
+ {
+ _popup.IsOpen = false;
+ return;
+ }
+
+ _popup.PlacementTarget = chart.View as FrameworkElement;
+ _popup.IsOpen = true;
+ }
+
+ public void Hide(Chart chart)
+ {
+ _popup.IsOpen = false;
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ private static SKColor GetSeriesColor(ChartPoint point)
+ {
+ if (point.Context.Series is LineSeries ls && ls.Stroke is SolidColorPaint paint)
+ return paint.Color;
+ return SKColors.Gray;
+ }
+
+ private static Brush SkColorToBrush(SKColor color)
+ => new SolidColorBrush(Color.FromArgb(color.Alpha, color.Red, color.Green, color.Blue));
+
+ public record TooltipEntry(Brush SeriesColor, string Ssid, string Detail);
+}
diff --git a/Source/NETworkManager/Controls/LvlChartsWiFiChannelTooltip.xaml b/Source/NETworkManager/Controls/LvlChartsWiFiChannelTooltip.xaml
deleted file mode 100644
index 3b44e78a3e..0000000000
--- a/Source/NETworkManager/Controls/LvlChartsWiFiChannelTooltip.xaml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Source/NETworkManager/Controls/LvlChartsWiFiChannelTooltip.xaml.cs b/Source/NETworkManager/Controls/LvlChartsWiFiChannelTooltip.xaml.cs
deleted file mode 100644
index a0960b68f4..0000000000
--- a/Source/NETworkManager/Controls/LvlChartsWiFiChannelTooltip.xaml.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
-using LiveCharts;
-using LiveCharts.Wpf;
-
-namespace NETworkManager.Controls;
-
-public partial class LvlChartsWiFiChannelTooltip : IChartTooltip
-{
- public LvlChartsWiFiChannelTooltip()
- {
- InitializeComponent();
-
- DataContext = this;
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- public TooltipData Data
- {
- get;
- set
- {
- field = value;
- OnPropertyChanged();
- }
- }
-
- public TooltipSelectionMode? SelectionMode { get; set; }
-
- protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager/Controls/WiFiChannelPoint.cs b/Source/NETworkManager/Controls/WiFiChannelPoint.cs
new file mode 100644
index 0000000000..5b93dd292d
--- /dev/null
+++ b/Source/NETworkManager/Controls/WiFiChannelPoint.cs
@@ -0,0 +1,21 @@
+using NETworkManager.Models.Network;
+
+namespace NETworkManager.Controls;
+
+///
+/// A single data point of a Wi-Fi channel chart series. The X coordinate is expressed in
+/// channel-number space (which is linear with the channel frequency), the Y coordinate is the
+/// signal strength in dBm. Each point carries a reference to the originating network so the
+/// chart tooltip can display its details.
+///
+/// X coordinate in channel-number space.
+/// Signal strength in dBm (Y coordinate).
+/// The Wi-Fi network this point belongs to.
+public class WiFiChannelPoint(double channelAxis, double dbm, WiFiNetworkInfo network)
+{
+ public double ChannelAxis { get; } = channelAxis;
+
+ public double Dbm { get; } = dbm;
+
+ public WiFiNetworkInfo Network { get; } = network;
+}
diff --git a/Source/NETworkManager/NETworkManager.csproj b/Source/NETworkManager/NETworkManager.csproj
index 817363ce92..4852efab27 100644
--- a/Source/NETworkManager/NETworkManager.csproj
+++ b/Source/NETworkManager/NETworkManager.csproj
@@ -60,7 +60,6 @@
-
diff --git a/Source/NETworkManager/ViewModels/WiFiViewModel.cs b/Source/NETworkManager/ViewModels/WiFiViewModel.cs
index 254601c740..deb9903eb3 100644
--- a/Source/NETworkManager/ViewModels/WiFiViewModel.cs
+++ b/Source/NETworkManager/ViewModels/WiFiViewModel.cs
@@ -1,7 +1,12 @@
-using LiveCharts;
-using LiveCharts.Wpf;
+using LiveChartsCore;
+using LiveChartsCore.Drawing;
+using LiveChartsCore.Kernel;
+using LiveChartsCore.SkiaSharpView;
+using LiveChartsCore.SkiaSharpView.Painting;
+using LiveChartsCore.SkiaSharpView.Painting.Effects;
using log4net;
using MahApps.Metro.SimpleChildWindow;
+using NETworkManager.Controls;
using NETworkManager.Localization;
using NETworkManager.Localization.Resources;
using NETworkManager.Models.Export;
@@ -9,6 +14,7 @@
using NETworkManager.Settings;
using NETworkManager.Utilities;
using NETworkManager.Views;
+using SkiaSharp;
using System;
using System.Collections;
using System.Collections.Generic;
@@ -191,7 +197,7 @@ public string Search
}
}
- public bool Show2dot4GHzNetworks
+ public bool Show2Dot4GHzNetworks
{
get;
set
@@ -251,7 +257,7 @@ public bool Show6GHzNetworks
public ObservableCollection Networks
{
get;
- set
+ init
{
if (value != null && value == field)
return;
@@ -289,28 +295,64 @@ public IList SelectedNetworks
}
} = new ArrayList();
- public SeriesCollection Radio2dot4GHzSeries { get; set; } = [];
+ public ISeries[] Radio2Dot4GHzSeries
+ {
+ get;
+ private set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
- public string[] Radio2dot4GHzLabels { get; set; } =
- [" ", " ", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", " ", " "];
+ public ISeries[] Radio5GHzSeries
+ {
+ get;
+ private set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
- public SeriesCollection Radio5GHzSeries { get; set; } = [];
+ // 6 GHz spans a very wide range (channels 1-233). It is split into a lower (1-125) and an upper
+ // (129-233) chart for readability, similar to the UniFi channel view.
+ public ISeries[] Radio6GHzLowerSeries
+ {
+ get;
+ private set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
- public string[] Radio5GHzLabels { get; set; } =
- [
- " ", " ", "36", "40", "44", "48", "52", "56", "60", "64", "", "", "", "", "100", "104", "108", "112", "116",
- "120", "124", "128", "132", "136", "140", "144", "149", "153", "157", "161", "165", " ", " "
- ];
+ public ISeries[] Radio6GHzUpperSeries
+ {
+ get;
+ private set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
- public SeriesCollection Radio6GHzSeries { get; set; } = [];
+ public Axis[] Radio2Dot4GHzXAxes { get; private set; }
+ public Axis[] Radio5GHzXAxes { get; private set; }
+ public Axis[] Radio6GHzLowerXAxes { get; private set; }
+ public Axis[] Radio6GHzUpperXAxes { get; private set; }
- public string[] Radio6GHzLabels { get; set; } =
- [
+ public Axis[] Radio2Dot4GHzYAxes { get; private set; }
+ public Axis[] Radio5GHzYAxes { get; private set; }
+ public Axis[] Radio6GHzLowerYAxes { get; private set; }
+ public Axis[] Radio6GHzUpperYAxes { get; private set; }
- ];
+ public RectangularSection[] Radio2Dot4GHzSections { get; private set; }
+ public RectangularSection[] Radio5GHzSections { get; private set; }
+ public RectangularSection[] Radio6GHzLowerSections { get; private set; }
+ public RectangularSection[] Radio6GHzUpperSections { get; private set; }
- public Func FormattedDbm { get; set; } =
- value => $"- {100 - value} dBm"; // Reverse y-axis 0 to -100
+ public SolidColorPaint LegendTextPaint { get; private set; }
public bool IsStatusMessageDisplayed
{
@@ -398,7 +440,11 @@ public WiFiViewModel()
{
_isLoading = true;
- // Check if Microsoft.Windows.SDK.Contracts is available
+ // Set up the channel charts (axes, sections, paints) unconditionally so the chart bindings
+ // are never null, even on the code paths that return early below.
+ InitializeCharts();
+
+ // Check if Microsoft.Windows.SDK.Contracts is available
SdkContractAvailable = ApiInformation.IsTypePresent("Windows.Devices.WiFi.WiFiAdapter");
if (!SdkContractAvailable)
@@ -429,7 +475,7 @@ public WiFiViewModel()
return false;
// Filter by frequency
- if ((info.Radio == WiFiRadio.GHz2dot4 && !Show2dot4GHzNetworks) ||
+ if ((info.Radio == WiFiRadio.GHz2dot4 && !Show2Dot4GHzNetworks) ||
(info.Radio == WiFiRadio.GHz5 && !Show5GHzNetworks) ||
(info.Radio == WiFiRadio.GHz6 && !Show6GHzNetworks))
{
@@ -474,7 +520,7 @@ public WiFiViewModel()
private void LoadSettings()
{
- Show2dot4GHzNetworks = SettingsManager.Current.WiFi_Show2dot4GHzNetworks;
+ Show2Dot4GHzNetworks = SettingsManager.Current.WiFi_Show2dot4GHzNetworks;
Show5GHzNetworks = SettingsManager.Current.WiFi_Show5GHzNetworks;
Show6GHzNetworks = SettingsManager.Current.WiFi_Show6GHzNetworks;
}
@@ -633,10 +679,13 @@ private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = fals
// Clear the values after the scan to make the UI smoother
Log.Debug("ScanAsync - Clearing old values...");
Networks.Clear();
- Radio2dot4GHzSeries.Clear();
- Radio5GHzSeries.Clear();
Log.Debug("ScanAsync - Adding new values...");
+ List series2Dot4GHz = [];
+ List series5GHz = [];
+ List series6GHzLower = [];
+ List series6GHzUpper = [];
+
foreach (var network in wiFiNetworkScanInfo.WiFiNetworkInfos)
{
Log.Debug("ScanAsync - Add network: " + network.AvailableNetwork.Ssid + " with channel frequency: " +
@@ -647,21 +696,32 @@ private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = fals
switch (network.Radio)
{
case WiFiRadio.GHz2dot4:
- Radio2dot4GHzSeries.Add(GetSeriesCollection(network));
+ series2Dot4GHz.Add(BuildNetworkSeries(network, series2Dot4GHz.Count));
break;
- case WiFiRadio.GHz5:
- Radio5GHzSeries.Add(GetSeriesCollection(network));
+ case WiFiRadio.GHz5:
+ series5GHz.Add(BuildNetworkSeries(network, series5GHz.Count));
break;
- // ToDo: Implement 6 GHz
- /*
- case WiFiRadio.GHz6:
- break;
- */
+ case WiFiRadio.GHz6:
+ // Split by channel center into the lower (1-125) and upper (129-233) chart.
+ var centerChannel = FrequencyToChannelAxis(
+ network.ChannelCenterFrequencyInGigahertz * 1000, WiFiRadio.GHz6);
+
+ if (centerChannel < SixGHzSplitChannel)
+ series6GHzLower.Add(BuildNetworkSeries(network, series6GHzLower.Count));
+ else
+ series6GHzUpper.Add(BuildNetworkSeries(network, series6GHzUpper.Count));
+
+ break;
}
}
+ Radio2Dot4GHzSeries = [.. series2Dot4GHz];
+ Radio5GHzSeries = [.. series5GHz];
+ Radio6GHzLowerSeries = [.. series6GHzLower];
+ Radio6GHzUpperSeries = [.. series6GHzUpper];
+
statusMessage = string.Format(Strings.LastScanAtX,
wiFiNetworkScanInfo.Timestamp.ToLongTimeString());
}
@@ -674,8 +734,10 @@ private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = fals
// Clear the existing old values if an error occurs
Networks.Clear();
- Radio2dot4GHzSeries.Clear();
- Radio5GHzSeries.Clear();
+ Radio2Dot4GHzSeries = [];
+ Radio5GHzSeries = [];
+ Radio6GHzLowerSeries = [];
+ Radio6GHzUpperSeries = [];
}
finally
{
@@ -689,62 +751,262 @@ private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = fals
}
}
- private ChartValues GetDefaultChartValues(WiFiRadio radio)
- {
- ChartValues values = [];
+ ///
+ /// Color palette used to assign a distinct, deterministic color to each network series.
+ /// Assigning the stroke explicitly (instead of relying on the LiveCharts2 theme) keeps the
+ /// legend, the chart and the tooltip in sync.
+ ///
+ private static readonly SKColor[] ChartPalette =
+ [
+ SKColor.Parse("#1ba1e2"), SKColor.Parse("#a4c400"), SKColor.Parse("#f0a30a"),
+ SKColor.Parse("#e51400"), SKColor.Parse("#6a00ff"), SKColor.Parse("#00aba9"),
+ SKColor.Parse("#d80073"), SKColor.Parse("#60a917"), SKColor.Parse("#fa6800"),
+ SKColor.Parse("#0050ef"), SKColor.Parse("#aa00ff"), SKColor.Parse("#825a2c")
+ ];
- var size = radio switch
- {
- WiFiRadio.GHz2dot4 => Radio2dot4GHzLabels.Length,
- WiFiRadio.GHz5 => Radio5GHzLabels.Length,
- WiFiRadio.GHz6 => Radio6GHzLabels.Length,
- _ => 0
- };
+ // The Y axis is plotted in 0..100 space (signal strength + 100) so the area fill drops to the
+ // bottom baseline; the axis labeler converts back to real dBm for display.
+ private const double SignalOffset = 100;
+
+ // 2.4 GHz operating channels are 1, 2, 3, ... 14 (every channel number), labeled with the channel
+ private static readonly HashSet ValidChannels2Dot4GHz = BuildChannelSet(1, 14, 1);
+
+ // 5 GHz operating channels are 36, 40, 44, ... 165 (every 4 in channel-number space), labeled with
+ private static readonly HashSet ValidChannels5GHz =
+ [
+ .. BuildChannelSet(36, 64, 4),
+ .. BuildChannelSet(100, 165, 4)
+ ];
+
+ // 6 GHz operating channels are 1, 5, 9, ... 233 (every 4 in channel-number space), labeled with the
+ private static readonly HashSet ValidChannels6GHz = BuildChannelSet(1, 233, 4);
+
+ // Channel that splits the 6 GHz band into the lower (1-125) and upper (129-233) chart. 127 sits
+ // cleanly between the operating channels 125 and 129.
+ private const int SixGHzSplitChannel = 127;
- for (var i = 0; i < size; i++)
- values.Add(-1);
+ // The 5 GHz band has a large unused range between channel 64 (UNII-2) and channel 100
+ // (UNII-2 Extended). On a frequency-linear axis this would render as dead space, so it is
+ // compressed: channels above 64 are shifted left, leaving only a small visual gap that still
+ // provides room for the channel-width trapezoids.
+ private const int FiveGHzGapStartChannel = 64;
+ private const int FiveGHzGapEndChannel = 100;
+ private const int FiveGHzGapDisplayUnits = 16;
+ private const int FiveGHzGapShift = FiveGHzGapEndChannel - FiveGHzGapStartChannel - FiveGHzGapDisplayUnits;
- return values;
+ private static HashSet BuildChannelSet(int first, int last, int step)
+ {
+ var set = new HashSet();
+
+ for (var channel = first; channel <= last; channel += step)
+ set.Add(channel);
+
+ return set;
}
- private ChartValues GetChartValues(WiFiNetworkInfo network, int index)
+ ///
+ /// Builds axes, signal-quality sections and paints for all channel charts. Called once from
+ /// the constructor; the per-network series are (re)built on each scan.
+ ///
+ private void InitializeCharts()
{
- var values = GetDefaultChartValues(network.Radio);
+ var labelColor = Application.Current?.TryFindResource("MahApps.Brushes.Gray5") is System.Windows.Media.SolidColorBrush gray5
+ ? new SKColor(gray5.Color.R, gray5.Color.G, gray5.Color.B, gray5.Color.A)
+ : new SKColor(0x68, 0x68, 0x68);
+
+ var separatorColor = Application.Current?.TryFindResource("MahApps.Brushes.Gray8") is System.Windows.Media.SolidColorBrush gray8
+ ? new SKColor(gray8.Color.R, gray8.Color.G, gray8.Color.B, gray8.Color.A)
+ : new SKColor(0x80, 0x80, 0x80);
+
+ LegendTextPaint = new SolidColorPaint(labelColor) { SKTypeface = SKTypeface.Default };
+
+ // (min, max) in display space. The lower bound is extended below the first channel so the
+ // left flank of its trapezoid reaches the baseline instead of being clipped. The 5 GHz
+ // upper bound is reduced because the 64..100 dead zone is compressed.
+ Radio2Dot4GHzXAxes = BuildXAxes(-2, 16, ValidChannels2Dot4GHz, false, labelColor);
+ Radio5GHzXAxes = BuildXAxes(30, 152, ValidChannels5GHz, true, labelColor);
+ Radio6GHzLowerXAxes = BuildXAxes(-2, 128, ValidChannels6GHz, false, labelColor);
+ Radio6GHzUpperXAxes = BuildXAxes(126, 236, ValidChannels6GHz, false, labelColor);
+
+ Radio2Dot4GHzYAxes = BuildYAxes(labelColor, separatorColor);
+ Radio5GHzYAxes = BuildYAxes(labelColor, separatorColor);
+ Radio6GHzLowerYAxes = BuildYAxes(labelColor, separatorColor);
+ Radio6GHzUpperYAxes = BuildYAxes(labelColor, separatorColor);
+
+ Radio2Dot4GHzSections = BuildSections();
+ Radio5GHzSections = BuildSections();
+ Radio6GHzLowerSections = BuildSections();
+ Radio6GHzUpperSections = BuildSections();
+ }
- var reverseMilliwatts = 100 - network.AvailableNetwork.NetworkRssiInDecibelMilliwatts * -1;
+ ///
+ /// Builds an X-axis in channel-number space (which is linear with the channel frequency).
+ /// Only channels contained in are labeled; all other tick
+ /// positions stay blank. When is set, the 5 GHz gap shift
+ /// is reversed before the label lookup.
+ ///
+ private static Axis[] BuildXAxes(double min, double max, HashSet validChannels, bool applyFiveGHzGap,
+ SKColor labelColor)
+ {
+ return
+ [
+ new Axis
+ {
+ Name = Strings.Channel,
+ NamePaint = new SolidColorPaint(labelColor),
+ NameTextSize = 11,
+ MinLimit = min,
+ MaxLimit = max,
+ MinStep = 1,
+ ForceStepToMin = true,
+ Labeler = value =>
+ {
+ var channel = (int)Math.Round(value);
+
+ if (applyFiveGHzGap && channel > FiveGHzGapStartChannel)
+ channel += FiveGHzGapShift;
+
+ return validChannels.Contains(channel) ? channel.ToString() : string.Empty;
+ },
+ TextSize = 10,
+ Padding = new Padding(0, 4, 0, 0),
+ LabelsPaint = new SolidColorPaint(labelColor),
+ // No vertical grid lines (matches the legacy chart).
+ SeparatorsPaint = null
+ }
+ ];
+ }
- // ToDo: Implement channel width (20, 40, 80, 160)
- values[index - 2] = -1;
- values[index - 1] = reverseMilliwatts;
- values[index] = reverseMilliwatts;
- values[index + 1] = reverseMilliwatts;
- values[index + 2] = -1;
+ ///
+ /// Builds the Y-axis (signal strength in dBm, -100..0) shared by all three channel charts.
+ ///
+ private static Axis[] BuildYAxes(SKColor labelColor, SKColor separatorColor)
+ {
+ return
+ [
+ new Axis
+ {
+ Name = Strings.SignalStrength,
+ NamePaint = new SolidColorPaint(labelColor),
+ NameTextSize = 11,
+ MinLimit = 0,
+ MaxLimit = 100,
+ MinStep = 10,
+ ForceStepToMin = true,
+ // Plotted in 0..100 space; convert back to real dBm for the label.
+ Labeler = value => $"{(int)Math.Round(value) - (int)SignalOffset} dBm",
+ TextSize = 11,
+ Padding = new Padding(4, 0),
+ LabelsPaint = new SolidColorPaint(labelColor),
+ SeparatorsPaint = new SolidColorPaint(separatorColor)
+ {
+ StrokeThickness = 1,
+ PathEffect = new DashEffect([10f, 10f])
+ }
+ }
+ ];
+ }
- return values;
+ ///
+ /// Builds the colored signal-quality bands (Cisco/MetaGeek thresholds), identical for all
+ /// bands. Returns fresh instances so they are not shared between charts.
+ ///
+ private static RectangularSection[] BuildSections()
+ {
+ // Thresholds in 0..100 space (dBm + 100): -30 -> 70, -67 -> 33, -70 -> 30, -80 -> 20.
+ return
+ [
+ NewSection(70, 100, "#5EA4BF"), // Excellent (>= -30 dBm)
+ NewSection(33, 70, "#badc58"), // Good (-67…-30 dBm)
+ NewSection(30, 33, "#f9ca24"), // Reliable (-70…-67 dBm)
+ NewSection(20, 30, "#FF970D"), // Weak (-80…-70 dBm)
+ NewSection(0, 20, "#A4442B") // Poor (< -80 dBm)
+ ];
}
- private LineSeries GetSeriesCollection(WiFiNetworkInfo network)
+ private static RectangularSection NewSection(double yi, double yj, string color)
{
- var radioLabels = network.Radio switch
+ return new RectangularSection
{
- WiFiRadio.GHz2dot4 => Radio2dot4GHzLabels,
- WiFiRadio.GHz5 => Radio5GHzLabels,
- WiFiRadio.GHz6 => Radio6GHzLabels,
- _ => []
+ Yi = yi,
+ Yj = yj,
+ Fill = new SolidColorPaint(SKColor.Parse(color).WithAlpha(0x40))
};
+ }
- var index = Array.IndexOf(radioLabels, $"{network.Channel}");
+ ///
+ /// Builds a line series rendering a single network as a trapezoid centered on its channel
+ /// center frequency. The width of the trapezoid reflects the channel bandwidth.
+ ///
+ private static LineSeries BuildNetworkSeries(WiFiNetworkInfo network, int colorIndex)
+ {
+ var color = ChartPalette[colorIndex % ChartPalette.Length];
+
+ double dbm = network.AvailableNetwork.NetworkRssiInDecibelMilliwatts;
+ var center = ChannelNumberToDisplay(
+ FrequencyToChannelAxis(network.ChannelCenterFrequencyInGigahertz * 1000, network.Radio), network.Radio);
+
+ // Channel-number space uses 5 MHz per unit, so a bandwidth in MHz spans bandwidth/5 units.
+ var bandwidth = network.ChannelBandwidth > 0 ? network.ChannelBandwidth : 20;
+ var half = bandwidth / 10.0;
+ var inner = half * 0.7;
+ const double floor = -100; // baseline in real dBm; mapped to 0 in the chart's 0..100 space
+
+ // Flat-topped trapezoid: rises from the noise floor at the channel edges to the RSSI plateau.
+ WiFiChannelPoint[] points =
+ [
+ new(center - half, floor, network),
+ new(center - inner, dbm, network),
+ new(center, dbm, network),
+ new(center + inner, dbm, network),
+ new(center + half, floor, network)
+ ];
+
+ var name = network.IsHidden
+ ? $"{Strings.HiddenNetwork} ({network.AvailableNetwork.Bssid})"
+ : $"{network.AvailableNetwork.Ssid} ({network.AvailableNetwork.Bssid})";
+
+ return new LineSeries
+ {
+ Name = name,
+ Values = points,
+ // Plot in 0..100 space (dBm + 100) so the area fill drops to the bottom baseline.
+ Mapping = (point, _) => new Coordinate(point.ChannelAxis, point.Dbm + SignalOffset),
+ GeometrySize = 0,
+ LineSmoothness = 0,
+ Stroke = new SolidColorPaint(color) { StrokeThickness = 1.5f },
+ Fill = new SolidColorPaint(color.WithAlpha(0x33))
+ };
+ }
- return new LineSeries
+ ///
+ /// Converts a channel center frequency (MHz) to channel-number space for the chart X axis.
+ /// This mapping is linear with frequency (5 MHz per unit).
+ ///
+ private static double FrequencyToChannelAxis(double frequencyMHz, WiFiRadio radio)
+ {
+ return radio switch
{
- Title = $"{network.AvailableNetwork.Ssid} ({network.AvailableNetwork.Bssid})",
- Values = GetChartValues(network, index),
- PointGeometry = null,
- LineSmoothness = 0
+ WiFiRadio.GHz2dot4 => (frequencyMHz - 2407) / 5.0,
+ WiFiRadio.GHz5 => (frequencyMHz - 5000) / 5.0,
+ WiFiRadio.GHz6 => (frequencyMHz - 5950) / 5.0,
+ _ => 0
};
}
- private Task Connect()
+ ///
+ /// Maps a channel number to its display position on the X axis. For 5 GHz the unused
+ /// 64..100 range is compressed so it does not render as dead space.
+ ///
+ private static double ChannelNumberToDisplay(double channelNumber, WiFiRadio radio)
+ {
+ if (radio == WiFiRadio.GHz5 && channelNumber > FiveGHzGapStartChannel)
+ return channelNumber - FiveGHzGapShift;
+
+ return channelNumber;
+ }
+
+ private void Connect()
{
var selectedAdapter = SelectedAdapter;
var selectedNetwork = SelectedNetwork;
@@ -866,7 +1128,7 @@ private Task Connect()
ConfigurationManager.Current.IsChildWindowOpen = true;
- return Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
+ Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
}
private async void Disconnect()
diff --git a/Source/NETworkManager/Views/WiFiView.xaml b/Source/NETworkManager/Views/WiFiView.xaml
index 030faf1ecc..5794351fc0 100644
--- a/Source/NETworkManager/Views/WiFiView.xaml
+++ b/Source/NETworkManager/Views/WiFiView.xaml
@@ -8,7 +8,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:NETworkManager.ViewModels"
- xmlns:liveChart="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
+ xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
xmlns:networkManager="clr-namespace:NETworkManager"
xmlns:controls="clr-namespace:NETworkManager.Controls"
xmlns:controls2="clr-namespace:NETworkManager.Controls;assembly=NETworkManager.Controls"
@@ -53,11 +53,11 @@
-
-
+
@@ -267,7 +267,7 @@
+
diff --git a/Source/NETworkManager/Views/WiFiView.xaml.cs b/Source/NETworkManager/Views/WiFiView.xaml.cs
index f673e179e8..c12c886e00 100644
--- a/Source/NETworkManager/Views/WiFiView.xaml.cs
+++ b/Source/NETworkManager/Views/WiFiView.xaml.cs
@@ -8,6 +8,8 @@ public partial class WiFiView
{
private readonly WiFiViewModel _viewModel;
+ private int _channelTabControlSelectedIndex = 0;
+
public WiFiView()
{
_viewModel = new WiFiViewModel();
@@ -32,4 +34,26 @@ private void ContextMenu_Opened(object sender, RoutedEventArgs e)
if (sender is ContextMenu menu)
menu.DataContext = _viewModel;
}
+
+ ///
+ /// Restores the previously selected channel chart tab. The content is rebuilt when switching
+ /// between the outer tabs, so the selection is restored from the view model on load.
+ ///
+ private void ChannelsTabControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is TabControl tabControl)
+ tabControl.SelectedIndex = _channelTabControlSelectedIndex;
+ }
+
+ ///
+ /// Persists the selected channel chart tab. Ignores selection changes that occur while the
+ /// control is not yet loaded (e.g. during the initial template apply) so the saved value is
+ /// not overwritten with the default, and ignores bubbled events from nested selectors.
+ ///
+ private void ChannelsTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (sender is TabControl tabControl && tabControl.IsLoaded &&
+ ReferenceEquals(e.OriginalSource, tabControl))
+ _channelTabControlSelectedIndex = tabControl.SelectedIndex;
+ }
}
\ No newline at end of file
From be661691337a80dcf8345d490189afec2c2bce2b Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 31 May 2026 22:56:19 +0200
Subject: [PATCH 02/16] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
.../Controls/LiveChartsWiFiChannelTooltip.xaml.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
index 6184ce5716..9bdfe8c03a 100644
--- a/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
+++ b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
@@ -57,8 +57,9 @@ public void Show(IEnumerable tooltipPoints, Chart chart)
continue;
var ssid = network.IsHidden ? Strings.HiddenNetwork : network.AvailableNetwork.Ssid;
+ var widthText = network.ChannelBandwidth > 0 ? $"{network.ChannelBandwidth} MHz" : "-/-";
var detail =
- $"{network.AvailableNetwork.NetworkRssiInDecibelMilliwatts} dBm · {Strings.Channel} {network.Channel} · {network.ChannelBandwidth} MHz";
+ $"{network.AvailableNetwork.NetworkRssiInDecibelMilliwatts} dBm · {Strings.Channel} {network.Channel} · {Strings.ChannelWidth} {widthText}";
TooltipEntries.Add(new TooltipEntry(SkColorToBrush(GetSeriesColor(point)), ssid, detail));
}
From cfbb2cdd0fffefd0e9b622c2a315f968ac3abc4d Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 31 May 2026 22:56:53 +0200
Subject: [PATCH 03/16] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
Source/NETworkManager/Views/WiFiView.xaml | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/Source/NETworkManager/Views/WiFiView.xaml b/Source/NETworkManager/Views/WiFiView.xaml
index 5794351fc0..239118a7b8 100644
--- a/Source/NETworkManager/Views/WiFiView.xaml
+++ b/Source/NETworkManager/Views/WiFiView.xaml
@@ -614,9 +614,19 @@
MinWidth="80" />
+ MinWidth="100">
+
+
+
+
Date: Sun, 31 May 2026 22:57:30 +0200
Subject: [PATCH 04/16] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
Source/NETworkManager/Views/WiFiView.xaml | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/Source/NETworkManager/Views/WiFiView.xaml b/Source/NETworkManager/Views/WiFiView.xaml
index 239118a7b8..7d04289697 100644
--- a/Source/NETworkManager/Views/WiFiView.xaml
+++ b/Source/NETworkManager/Views/WiFiView.xaml
@@ -714,8 +714,13 @@
-
-
+
+
+
+
+
+
+
From dacab2f8bb27bd04583533f037f0359026d8dce6 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 31 May 2026 22:57:50 +0200
Subject: [PATCH 05/16] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
Source/NETworkManager/Views/WiFiView.xaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Source/NETworkManager/Views/WiFiView.xaml b/Source/NETworkManager/Views/WiFiView.xaml
index 7d04289697..41c971b6dc 100644
--- a/Source/NETworkManager/Views/WiFiView.xaml
+++ b/Source/NETworkManager/Views/WiFiView.xaml
@@ -777,7 +777,7 @@
-
Date: Sun, 31 May 2026 22:57:59 +0200
Subject: [PATCH 06/16] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
Source/NETworkManager/Views/WiFiView.xaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Source/NETworkManager/Views/WiFiView.xaml b/Source/NETworkManager/Views/WiFiView.xaml
index 41c971b6dc..fe36cd06cb 100644
--- a/Source/NETworkManager/Views/WiFiView.xaml
+++ b/Source/NETworkManager/Views/WiFiView.xaml
@@ -796,7 +796,7 @@
-
Date: Sun, 31 May 2026 22:58:09 +0200
Subject: [PATCH 07/16] Fix: Copilot feedback
---
Source/NETworkManager.Models/Network/WiFi.cs | 2 --
Source/NETworkManager/Views/WiFiView.xaml.cs | 4 ++--
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/Source/NETworkManager.Models/Network/WiFi.cs b/Source/NETworkManager.Models/Network/WiFi.cs
index 94fe53bb61..a7665aaf2b 100644
--- a/Source/NETworkManager.Models/Network/WiFi.cs
+++ b/Source/NETworkManager.Models/Network/WiFi.cs
@@ -1,13 +1,11 @@
using log4net;
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.Devices.WiFi;
using Windows.Networking.Connectivity;
using Windows.Security.Credentials;
-using Microsoft.CodeAnalysis.Emit;
using NETworkManager.Models.Lookup;
namespace NETworkManager.Models.Network;
diff --git a/Source/NETworkManager/Views/WiFiView.xaml.cs b/Source/NETworkManager/Views/WiFiView.xaml.cs
index c12c886e00..088a6c178a 100644
--- a/Source/NETworkManager/Views/WiFiView.xaml.cs
+++ b/Source/NETworkManager/Views/WiFiView.xaml.cs
@@ -37,7 +37,7 @@ private void ContextMenu_Opened(object sender, RoutedEventArgs e)
///
/// Restores the previously selected channel chart tab. The content is rebuilt when switching
- /// between the outer tabs, so the selection is restored from the view model on load.
+ /// between the outer tabs, so the selection is restored from the view when the control loads.
///
private void ChannelsTabControl_Loaded(object sender, RoutedEventArgs e)
{
@@ -52,7 +52,7 @@ private void ChannelsTabControl_Loaded(object sender, RoutedEventArgs e)
///
private void ChannelsTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
- if (sender is TabControl tabControl && tabControl.IsLoaded &&
+ if (sender is TabControl { IsLoaded: true } tabControl &&
ReferenceEquals(e.OriginalSource, tabControl))
_channelTabControlSelectedIndex = tabControl.SelectedIndex;
}
From 9398203d970425a1175766904d05dc6ff0364b13 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 31 May 2026 22:58:16 +0200
Subject: [PATCH 08/16] Docs: #3462
---
Website/docs/application/wifi.md | 17 +++++++++--------
Website/docs/changelog/next-release.md | 12 +++++++++++-
2 files changed, 20 insertions(+), 9 deletions(-)
diff --git a/Website/docs/application/wifi.md b/Website/docs/application/wifi.md
index 9e26634677..1d6db4a779 100644
--- a/Website/docs/application/wifi.md
+++ b/Website/docs/application/wifi.md
@@ -34,12 +34,6 @@ Open `Windows Settings > Privacy & security > Location`, enable access for Deskt
:::
-:::note
-
-Due to limitations of the `Windows.Devices.WiFi` API, the channel bandwidth cannot be detected.
-
-:::
-

### Context menu
@@ -59,12 +53,19 @@ Due to limitations of the `Windows.Devices.WiFi` API, the channel bandwidth cann
## Channels
-On the **Channels** tab, all wireless networks of the selected wireless network adapter are displayed in a graphical view with channel and signal strength. This can be useful to identify overlapping wireless networks that do not originate from the same access point.
+On the **Channels** tab, all wireless networks of the selected wireless network adapter are displayed in a graphical view with channel, channel width, and signal strength. This can be useful to identify overlapping wireless networks that do not originate from the same access point.
+
+The tab is split into two sub-tabs:
+
+- **2.4 & 5 GHz** — shows networks on the 2.4 GHz and 5 GHz bands in a single chart each.
+- **6 GHz** — shows 6 GHz networks in two separate charts: lower channels (1–125) and upper channels (129–233).
+
+Each network is drawn as a proportional band reflecting its channel width (20, 40, 80, or 160 MHz), so overlapping networks can be identified more accurately.

:::note
-Move the mouse over a channel to display all wireless networks occupying that channel in a tooltip.
+Move the mouse over a network band to display details such as SSID, channel, channel width, and signal strength in a tooltip.
:::
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index bc012d8056..73d9ca6827 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -36,6 +36,11 @@ Release date: **xx.xx.2025**
- New **Speed Test** widget to measure download/upload speed, latency, and jitter against [`speed.cloudflare.com`](https://speed.cloudflare.com/). The test is user-initiated and shows download (Mbps), upload (Mbps), latency (ms), jitter (ms), ISP, and server location. A privacy disclaimer is shown before use. [#3440](https://github.com/BornToBeRoot/NETworkManager/pull/3440)
+**WiFi**
+
+- Added **Channel Width** column (in MHz) to the network list. Channel bandwidth is retrieved via the native `WlanApi`, bypassing the `Windows.Devices.WiFi` API limitation. Typical values: 20, 40, 80, 160 MHz. [#3462](https://github.com/BornToBeRoot/NETworkManager/pull/3462)
+- The **Channels** tab now supports 6 GHz networks and is split into two sub-tabs: **2.4 & 5 GHz** and **6 GHz**. The 6 GHz view uses separate lower (channels 1–125) and upper (channels 129–233) charts for readability. [#3462](https://github.com/BornToBeRoot/NETworkManager/pull/3462)
+
**PowerShell**
- DPI scaling is now applied correctly when NETworkManager is moved to a monitor with a different DPI scaling factor. The embedded PowerShell (conhost) window now rescales its font automatically using the Windows Console API (`AttachConsole` + `SetCurrentConsoleFontEx`), bypassing the OS limitation that prevents `WM_DPICHANGED` from being forwarded to cross-process child windows. [#3352](https://github.com/BornToBeRoot/NETworkManager/pull/3352)
@@ -61,6 +66,11 @@ Release date: **xx.xx.2025**
## Improvements
+**WiFi**
+
+- Channel width is now visualized in the channel charts as a proportional band, making overlapping networks easier to identify. [#3462](https://github.com/BornToBeRoot/NETworkManager/pull/3462)
+- Migrated the WiFi channel charts from LiveCharts to LiveCharts2. [#3462](https://github.com/BornToBeRoot/NETworkManager/pull/3462)
+
**IP Scanner**
- MAC address resolution now uses ARP (IPv4) or NDP (IPv6) from the neighbor cache, with NetBIOS as fallback. The detail panel shows a single **MAC Address** section instead of separate ARP and NetBIOS entries. [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
@@ -124,7 +134,7 @@ Release date: **xx.xx.2025**
## Dependencies, Refactoring & Documentation
-- Migrated from `LiveCharts` to `LiveCharts2` (`LiveChartsCore.SkiaSharpView.WPF`) for chart rendering. [#3449](https://github.com/BornToBeRoot/NETworkManager/pull/3449) [#3457](https://github.com/BornToBeRoot/NETworkManager/pull/3457)
+- Migrated from `LiveCharts` to `LiveCharts2` (`LiveChartsCore.SkiaSharpView.WPF`) for chart rendering. [#3449](https://github.com/BornToBeRoot/NETworkManager/pull/3449) [#3457](https://github.com/BornToBeRoot/NETworkManager/pull/3457) [#3462](https://github.com/BornToBeRoot/NETworkManager/pull/3462)
- Replace fire-and-forget `.ConfigureAwait(false)` calls with explicit discard assignments (`_ = SomeAsyncOperation()`) across command handlers, startup/load paths and profile callbacks. [#3441](https://github.com/BornToBeRoot/NETworkManager/pull/3441)
- Code cleanup & refactoring
- Language files updated via [#transifex](https://github.com/BornToBeRoot/NETworkManager/pulls?q=author%3Aapp%2Ftransifex-integration)
From ec9c194abd836c7326d4b3170fe5470c0673b2d3 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 31 May 2026 23:02:27 +0200
Subject: [PATCH 09/16] Fix: Use converter for channel width
---
.../WiFiChannelBandwidthToStringConverter.cs | 18 ++++++++++++++++++
Source/NETworkManager/Views/WiFiView.xaml | 17 ++++-------------
2 files changed, 22 insertions(+), 13 deletions(-)
create mode 100644 Source/NETworkManager.Converters/WiFiChannelBandwidthToStringConverter.cs
diff --git a/Source/NETworkManager.Converters/WiFiChannelBandwidthToStringConverter.cs b/Source/NETworkManager.Converters/WiFiChannelBandwidthToStringConverter.cs
new file mode 100644
index 0000000000..0a6b35c934
--- /dev/null
+++ b/Source/NETworkManager.Converters/WiFiChannelBandwidthToStringConverter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace NETworkManager.Converters;
+
+public sealed class WiFiChannelBandwidthToStringConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value is not int bandwidth || bandwidth == 0 ? "-/-" : $"{bandwidth} MHz";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/Source/NETworkManager/Views/WiFiView.xaml b/Source/NETworkManager/Views/WiFiView.xaml
index fe36cd06cb..2e6105eb18 100644
--- a/Source/NETworkManager/Views/WiFiView.xaml
+++ b/Source/NETworkManager/Views/WiFiView.xaml
@@ -28,6 +28,7 @@
+
-
-
-
-
+ MinWidth="100" />
Date: Sun, 31 May 2026 23:29:36 +0200
Subject: [PATCH 10/16] Fix: Some bug fixes
---
.../Resources/Strings.Designer.cs | 11 ++++++-
.../Resources/Strings.resx | 3 ++
.../LiveChartsBandwidthTooltip.xaml.cs | 2 +-
.../LiveChartsPingTimeTooltip.xaml.cs | 2 +-
.../LiveChartsWiFiChannelTooltip.xaml.cs | 8 ++---
.../ViewModels/WiFiViewModel.cs | 33 ++++++++++++++-----
.../Views/NetworkInterfaceView.xaml | 2 +-
Source/NETworkManager/Views/WiFiView.xaml | 11 ++-----
8 files changed, 45 insertions(+), 27 deletions(-)
diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index c03df53bca..5379e77ebd 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -4813,7 +4813,16 @@ public static string GHz5 {
return ResourceManager.GetString("GHz5", resourceCulture);
}
}
-
+
+ ///
+ /// Looks up a localized string similar to 2.4 & 5 GHz.
+ ///
+ public static string GHz2dot4And5 {
+ get {
+ return ResourceManager.GetString("GHz2dot4And5", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to 6 GHz.
///
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index 7d10eb50e0..3539a167ac 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -2283,6 +2283,9 @@ $$hostname$$ --> Hostname
5 GHz
+
+ 2.4 & 5 GHz
+
Signal strength
diff --git a/Source/NETworkManager/Controls/LiveChartsBandwidthTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsBandwidthTooltip.xaml.cs
index 85abf5f6b4..d004da9521 100644
--- a/Source/NETworkManager/Controls/LiveChartsBandwidthTooltip.xaml.cs
+++ b/Source/NETworkManager/Controls/LiveChartsBandwidthTooltip.xaml.cs
@@ -82,7 +82,7 @@ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName
private static SKColor GetSeriesColor(ChartPoint point)
{
- if (point.Context.Series is LineSeries ls && ls.Stroke is SolidColorPaint paint)
+ if (point.Context.Series is LineSeries { Stroke: SolidColorPaint paint })
return paint.Color;
return SKColors.Gray;
}
diff --git a/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs
index 8a802b9e48..2e3db4b47e 100644
--- a/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs
+++ b/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs
@@ -82,7 +82,7 @@ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName
private static SKColor GetSeriesColor(ChartPoint point)
{
- if (point.Context.Series is LineSeries ls && ls.Stroke is SolidColorPaint paint)
+ if (point.Context.Series is LineSeries { Stroke: SolidColorPaint paint })
return paint.Color;
return SKColors.Gray;
}
diff --git a/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
index 9bdfe8c03a..bb907b2f01 100644
--- a/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
+++ b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
-using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
@@ -59,7 +58,7 @@ public void Show(IEnumerable tooltipPoints, Chart chart)
var ssid = network.IsHidden ? Strings.HiddenNetwork : network.AvailableNetwork.Ssid;
var widthText = network.ChannelBandwidth > 0 ? $"{network.ChannelBandwidth} MHz" : "-/-";
var detail =
- $"{network.AvailableNetwork.NetworkRssiInDecibelMilliwatts} dBm · {Strings.Channel} {network.Channel} · {Strings.ChannelWidth} {widthText}";
+ $"{network.AvailableNetwork.NetworkRssiInDecibelMilliwatts} dBm · {Strings.Channel} {network.Channel} · {widthText}";
TooltipEntries.Add(new TooltipEntry(SkColorToBrush(GetSeriesColor(point)), ssid, detail));
}
@@ -79,12 +78,9 @@ public void Hide(Chart chart)
_popup.IsOpen = false;
}
- protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
- => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
-
private static SKColor GetSeriesColor(ChartPoint point)
{
- if (point.Context.Series is LineSeries ls && ls.Stroke is SolidColorPaint paint)
+ if (point.Context.Series is LineSeries { Stroke: SolidColorPaint paint })
return paint.Color;
return SKColors.Gray;
}
diff --git a/Source/NETworkManager/ViewModels/WiFiViewModel.cs b/Source/NETworkManager/ViewModels/WiFiViewModel.cs
index deb9903eb3..a275f1f9eb 100644
--- a/Source/NETworkManager/ViewModels/WiFiViewModel.cs
+++ b/Source/NETworkManager/ViewModels/WiFiViewModel.cs
@@ -952,15 +952,30 @@ private static LineSeries BuildNetworkSeries(WiFiNetworkInfo n
var inner = half * 0.7;
const double floor = -100; // baseline in real dBm; mapped to 0 in the chart's 0..100 space
- // Flat-topped trapezoid: rises from the noise floor at the channel edges to the RSSI plateau.
- WiFiChannelPoint[] points =
- [
- new(center - half, floor, network),
- new(center - inner, dbm, network),
- new(center, dbm, network),
- new(center + inner, dbm, network),
- new(center + half, floor, network)
- ];
+ // Dense points at 0.5-channel steps so that CompareOnlyX always finds this series when the
+ // mouse is anywhere inside the trapezoid, even when a narrower network shares the same area.
+ // With only 5 corner points the nearest point of a wide network can be far from the mouse
+ // while a narrower network's corner is closer, causing the wide network to be omitted from
+ // the tooltip despite the mouse being visually inside it.
+ const double step = 0.5;
+ var rampWidth = half - inner; // > 0 always (inner = half * 0.7)
+ var pointList = new List();
+
+ for (var x = center - half; x <= center + half + step * 0.01; x += step)
+ {
+ double y;
+
+ if (x <= center - inner)
+ y = floor + (dbm - floor) * (x - (center - half)) / rampWidth;
+ else if (x >= center + inner)
+ y = dbm - (dbm - floor) * (x - (center + inner)) / rampWidth;
+ else
+ y = dbm;
+
+ pointList.Add(new WiFiChannelPoint(x, y, network));
+ }
+
+ var points = pointList.ToArray();
var name = network.IsHidden
? $"{Strings.HiddenNetwork} ({network.AvailableNetwork.Bssid})"
diff --git a/Source/NETworkManager/Views/NetworkInterfaceView.xaml b/Source/NETworkManager/Views/NetworkInterfaceView.xaml
index a8054f4002..31e7d04cef 100644
--- a/Source/NETworkManager/Views/NetworkInterfaceView.xaml
+++ b/Source/NETworkManager/Views/NetworkInterfaceView.xaml
@@ -846,7 +846,7 @@
Series="{Binding Series}"
XAxes="{Binding BandwidthXAxes}"
YAxes="{Binding BandwidthYAxes}"
- LegendPosition="Right"
+ LegendPosition="Bottom"
LegendTextPaint="{Binding BandwidthLegendTextPaint}"
LegendTextSize="12"
DrawMarginFrame="{x:Null}"
diff --git a/Source/NETworkManager/Views/WiFiView.xaml b/Source/NETworkManager/Views/WiFiView.xaml
index 2e6105eb18..0ea63b4fd0 100644
--- a/Source/NETworkManager/Views/WiFiView.xaml
+++ b/Source/NETworkManager/Views/WiFiView.xaml
@@ -617,7 +617,7 @@
Header="{x:Static Member=localization:Strings.ChannelWidth}"
Binding="{Binding Path=(network:WiFiNetworkInfo.ChannelBandwidth), Converter={StaticResource ResourceKey=WiFiChannelBandwidthToStringConverter}}"
SortMemberPath="ChannelBandwidth"
- MinWidth="100" />
+ MinWidth="120" />
-
-
-
-
-
-
-
+
+
From bea0ff0c95da4d43c1c8664351419831d6885ab4 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Mon, 1 Jun 2026 00:08:01 +0200
Subject: [PATCH 11/16] Feature: WiFi legends + bug fixes
---
.../LiveChartsWiFiChannelTooltip.xaml | 25 ++-
.../LiveChartsWiFiChannelTooltip.xaml.cs | 9 +-
.../Resources/Styles/TextBlockStyles.xaml | 1 -
.../ViewModels/WiFiViewModel.cs | 99 ++++++++++--
Source/NETworkManager/Views/WiFiView.xaml | 146 ++++++++++++++++--
5 files changed, 235 insertions(+), 45 deletions(-)
diff --git a/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml
index 30ae582560..e6238c7fb8 100644
--- a/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml
+++ b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml
@@ -11,7 +11,7 @@
-
+
@@ -22,13 +22,22 @@
-
-
+
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
index bb907b2f01..d46f1fe62f 100644
--- a/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
+++ b/Source/NETworkManager/Controls/LiveChartsWiFiChannelTooltip.xaml.cs
@@ -56,11 +56,12 @@ public void Show(IEnumerable tooltipPoints, Chart chart)
continue;
var ssid = network.IsHidden ? Strings.HiddenNetwork : network.AvailableNetwork.Ssid;
+ var bssid = network.AvailableNetwork.Bssid;
+ var dbm = $"{network.AvailableNetwork.NetworkRssiInDecibelMilliwatts} dBm";
var widthText = network.ChannelBandwidth > 0 ? $"{network.ChannelBandwidth} MHz" : "-/-";
- var detail =
- $"{network.AvailableNetwork.NetworkRssiInDecibelMilliwatts} dBm · {Strings.Channel} {network.Channel} · {widthText}";
+ var channelDetail = $"{Strings.Channel} {network.Channel} · {widthText}";
- TooltipEntries.Add(new TooltipEntry(SkColorToBrush(GetSeriesColor(point)), ssid, detail));
+ TooltipEntries.Add(new TooltipEntry(SkColorToBrush(GetSeriesColor(point)), ssid, bssid, dbm, channelDetail));
}
if (TooltipEntries.Count == 0)
@@ -88,5 +89,5 @@ private static SKColor GetSeriesColor(ChartPoint point)
private static Brush SkColorToBrush(SKColor color)
=> new SolidColorBrush(Color.FromArgb(color.Alpha, color.Red, color.Green, color.Blue));
- public record TooltipEntry(Brush SeriesColor, string Ssid, string Detail);
+ public record TooltipEntry(Brush SeriesColor, string Ssid, string Bssid, string Dbm, string ChannelDetail);
}
diff --git a/Source/NETworkManager/Resources/Styles/TextBlockStyles.xaml b/Source/NETworkManager/Resources/Styles/TextBlockStyles.xaml
index a2b2fd109d..379671f3d5 100644
--- a/Source/NETworkManager/Resources/Styles/TextBlockStyles.xaml
+++ b/Source/NETworkManager/Resources/Styles/TextBlockStyles.xaml
@@ -55,7 +55,6 @@
diff --git a/Source/NETworkManager/ViewModels/WiFiViewModel.cs b/Source/NETworkManager/ViewModels/WiFiViewModel.cs
index a275f1f9eb..9c455e67af 100644
--- a/Source/NETworkManager/ViewModels/WiFiViewModel.cs
+++ b/Source/NETworkManager/ViewModels/WiFiViewModel.cs
@@ -24,6 +24,7 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
+using System.Windows.Media;
using System.Windows.Input;
using System.Windows.Threading;
using Windows.Devices.WiFi;
@@ -352,7 +353,45 @@ private set
public RectangularSection[] Radio6GHzLowerSections { get; private set; }
public RectangularSection[] Radio6GHzUpperSections { get; private set; }
- public SolidColorPaint LegendTextPaint { get; private set; }
+ public WiFiChannelLegendEntry[] Radio2Dot4GHzLegend
+ {
+ get;
+ private set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
+
+ public WiFiChannelLegendEntry[] Radio5GHzLegend
+ {
+ get;
+ private set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
+
+ public WiFiChannelLegendEntry[] Radio6GHzLowerLegend
+ {
+ get;
+ private set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
+
+ public WiFiChannelLegendEntry[] Radio6GHzUpperLegend
+ {
+ get;
+ private set
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
public bool IsStatusMessageDisplayed
{
@@ -685,6 +724,10 @@ private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = fals
List series5GHz = [];
List series6GHzLower = [];
List series6GHzUpper = [];
+ List legend2Dot4GHz = [];
+ List legend5GHz = [];
+ List legend6GHzLower = [];
+ List legend6GHzUpper = [];
foreach (var network in wiFiNetworkScanInfo.WiFiNetworkInfos)
{
@@ -696,11 +739,15 @@ private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = fals
switch (network.Radio)
{
case WiFiRadio.GHz2dot4:
- series2Dot4GHz.Add(BuildNetworkSeries(network, series2Dot4GHz.Count));
+ var (s24, l24) = BuildNetworkSeries(network, series2Dot4GHz.Count);
+ series2Dot4GHz.Add(s24);
+ legend2Dot4GHz.Add(l24);
break;
- case WiFiRadio.GHz5:
- series5GHz.Add(BuildNetworkSeries(network, series5GHz.Count));
+ case WiFiRadio.GHz5:
+ var (s5, l5) = BuildNetworkSeries(network, series5GHz.Count);
+ series5GHz.Add(s5);
+ legend5GHz.Add(l5);
break;
case WiFiRadio.GHz6:
@@ -709,9 +756,17 @@ private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = fals
network.ChannelCenterFrequencyInGigahertz * 1000, WiFiRadio.GHz6);
if (centerChannel < SixGHzSplitChannel)
- series6GHzLower.Add(BuildNetworkSeries(network, series6GHzLower.Count));
+ {
+ var (s6L, l6L) = BuildNetworkSeries(network, series6GHzLower.Count);
+ series6GHzLower.Add(s6L);
+ legend6GHzLower.Add(l6L);
+ }
else
- series6GHzUpper.Add(BuildNetworkSeries(network, series6GHzUpper.Count));
+ {
+ var (s6U, l6U) = BuildNetworkSeries(network, series6GHzUpper.Count);
+ series6GHzUpper.Add(s6U);
+ legend6GHzUpper.Add(l6U);
+ }
break;
}
@@ -721,6 +776,10 @@ private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = fals
Radio5GHzSeries = [.. series5GHz];
Radio6GHzLowerSeries = [.. series6GHzLower];
Radio6GHzUpperSeries = [.. series6GHzUpper];
+ Radio2Dot4GHzLegend = [.. legend2Dot4GHz];
+ Radio5GHzLegend = [.. legend5GHz];
+ Radio6GHzLowerLegend = [.. legend6GHzLower];
+ Radio6GHzUpperLegend = [.. legend6GHzUpper];
statusMessage = string.Format(Strings.LastScanAtX,
wiFiNetworkScanInfo.Timestamp.ToLongTimeString());
@@ -738,6 +797,10 @@ private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = fals
Radio5GHzSeries = [];
Radio6GHzLowerSeries = [];
Radio6GHzUpperSeries = [];
+ Radio2Dot4GHzLegend = [];
+ Radio5GHzLegend = [];
+ Radio6GHzLowerLegend = [];
+ Radio6GHzUpperLegend = [];
}
finally
{
@@ -810,16 +873,14 @@ private static HashSet BuildChannelSet(int first, int last, int step)
///
private void InitializeCharts()
{
- var labelColor = Application.Current?.TryFindResource("MahApps.Brushes.Gray5") is System.Windows.Media.SolidColorBrush gray5
+ var labelColor = Application.Current?.TryFindResource("MahApps.Brushes.Gray5") is SolidColorBrush gray5
? new SKColor(gray5.Color.R, gray5.Color.G, gray5.Color.B, gray5.Color.A)
: new SKColor(0x68, 0x68, 0x68);
- var separatorColor = Application.Current?.TryFindResource("MahApps.Brushes.Gray8") is System.Windows.Media.SolidColorBrush gray8
+ var separatorColor = Application.Current?.TryFindResource("MahApps.Brushes.Gray8") is SolidColorBrush gray8
? new SKColor(gray8.Color.R, gray8.Color.G, gray8.Color.B, gray8.Color.A)
: new SKColor(0x80, 0x80, 0x80);
- LegendTextPaint = new SolidColorPaint(labelColor) { SKTypeface = SKTypeface.Default };
-
// (min, max) in display space. The lower bound is extended below the first channel so the
// left flank of its trapezoid reaches the baseline instead of being clipped. The 5 GHz
// upper bound is reduced because the 64..100 dead zone is compressed.
@@ -935,10 +996,10 @@ private static RectangularSection NewSection(double yi, double yj, string color)
}
///
- /// Builds a line series rendering a single network as a trapezoid centered on its channel
- /// center frequency. The width of the trapezoid reflects the channel bandwidth.
+ /// Builds a line series and a matching legend entry for a single network, rendered as a
+ /// trapezoid centered on its channel center frequency.
///
- private static LineSeries BuildNetworkSeries(WiFiNetworkInfo network, int colorIndex)
+ private static (LineSeries Series, WiFiChannelLegendEntry Legend) BuildNetworkSeries(WiFiNetworkInfo network, int colorIndex)
{
var color = ChartPalette[colorIndex % ChartPalette.Length];
@@ -981,7 +1042,7 @@ private static LineSeries BuildNetworkSeries(WiFiNetworkInfo n
? $"{Strings.HiddenNetwork} ({network.AvailableNetwork.Bssid})"
: $"{network.AvailableNetwork.Ssid} ({network.AvailableNetwork.Bssid})";
- return new LineSeries
+ var series = new LineSeries
{
Name = name,
Values = points,
@@ -992,6 +1053,12 @@ private static LineSeries BuildNetworkSeries(WiFiNetworkInfo n
Stroke = new SolidColorPaint(color) { StrokeThickness = 1.5f },
Fill = new SolidColorPaint(color.WithAlpha(0x33))
};
+
+ var wpfColor = Color.FromArgb(color.Alpha, color.Red, color.Green, color.Blue);
+ var legendSsid = network.IsHidden ? Strings.HiddenNetwork : network.AvailableNetwork.Ssid;
+ var legend = new WiFiChannelLegendEntry(new SolidColorBrush(wpfColor), legendSsid, network.AvailableNetwork.Bssid);
+
+ return (series, legend);
}
///
@@ -1253,4 +1320,6 @@ private void HideConnectionStatusMessageTimer_Tick(object sender, EventArgs e)
}
#endregion
-}
\ No newline at end of file
+}
+
+public record WiFiChannelLegendEntry(SolidColorBrush Color, string Ssid, string Bssid);
\ No newline at end of file
diff --git a/Source/NETworkManager/Views/WiFiView.xaml b/Source/NETworkManager/Views/WiFiView.xaml
index 0ea63b4fd0..c0bfa3d076 100644
--- a/Source/NETworkManager/Views/WiFiView.xaml
+++ b/Source/NETworkManager/Views/WiFiView.xaml
@@ -710,9 +710,11 @@
+
+
@@ -722,9 +724,7 @@
XAxes="{Binding Path=Radio2Dot4GHzXAxes}"
YAxes="{Binding Path=Radio2Dot4GHzYAxes}"
Sections="{Binding Path=Radio2Dot4GHzSections}"
- LegendPosition="Bottom"
- LegendTextPaint="{Binding Path=LegendTextPaint}"
- LegendTextSize="12"
+ LegendPosition="Hidden"
FindingStrategy="CompareOnlyX"
DrawMarginFrame="{x:Null}"
EasingFunction="{x:Null}"
@@ -733,17 +733,44 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -759,9 +815,11 @@
+
+
@@ -771,9 +829,7 @@
XAxes="{Binding Path=Radio6GHzLowerXAxes}"
YAxes="{Binding Path=Radio6GHzLowerYAxes}"
Sections="{Binding Path=Radio6GHzLowerSections}"
- LegendPosition="Bottom"
- LegendTextPaint="{Binding Path=LegendTextPaint}"
- LegendTextSize="12"
+ LegendPosition="Hidden"
FindingStrategy="CompareOnlyX"
DrawMarginFrame="{x:Null}"
EasingFunction="{x:Null}"
@@ -782,17 +838,44 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -893,7 +1005,7 @@
-
+