diff --git a/Source/NETworkManager.Converters/BandwidthBytesToSpeedConverter.cs b/Source/NETworkManager.Converters/BandwidthBytesToSpeedConverter.cs
index 6288cc1d1d..1f6d2a02cf 100644
--- a/Source/NETworkManager.Converters/BandwidthBytesToSpeedConverter.cs
+++ b/Source/NETworkManager.Converters/BandwidthBytesToSpeedConverter.cs
@@ -1,21 +1,31 @@
-using System;
+using System;
using System.Globalization;
using System.Windows.Data;
using NETworkManager.Utilities;
namespace NETworkManager.Converters;
+///
+/// Converts a byte-per-second value to a human-readable speed string.
+/// Pass the converter parameter "bytes" to format as byte/s (e.g. "1.54 MB/s");
+/// any other value (default) formats as bit/s (e.g. "12.3 Mbit/s") to match the chart axis.
+///
public sealed class BandwidthBytesToSpeedConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
- return value != null
- ? $"{FileSizeConverter.GetBytesReadable((long)value * 8)}it/s ({FileSizeConverter.GetBytesReadable((long)value)}/s)"
- : "-/-";
+ if (value == null)
+ return "-/-";
+
+ var bytesPerSecond = (long)value;
+
+ return parameter as string == "bytes"
+ ? $"{FileSizeConverter.GetBytesReadable(bytesPerSecond)}/s"
+ : $"{FileSizeConverter.GetBytesReadable(bytesPerSecond * 8)}it/s";
}
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/BandwidthBytesWithSizeConverter.cs b/Source/NETworkManager.Converters/BandwidthBytesWithSizeConverter.cs
deleted file mode 100644
index 114095183c..0000000000
--- a/Source/NETworkManager.Converters/BandwidthBytesWithSizeConverter.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows.Data;
-using NETworkManager.Utilities;
-
-namespace NETworkManager.Converters;
-
-public sealed class BandwidthBytesWithSizeConverter : IValueConverter
-{
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- return value != null ? $"{(long)value} ({FileSizeConverter.GetBytesReadable((long)value)})" : "-/-";
- }
-
- 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/BytesToExactStringConverter.cs b/Source/NETworkManager.Converters/BytesToExactStringConverter.cs
new file mode 100644
index 0000000000..062dad8cb0
--- /dev/null
+++ b/Source/NETworkManager.Converters/BytesToExactStringConverter.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using NETworkManager.Localization.Resources;
+
+namespace NETworkManager.Converters;
+
+///
+/// Converts a byte count to its exact value with thousands separators and a "Bytes" unit
+/// (e.g. "6,783,176,192 Bytes"). Intended for tooltips that complement a human-readable size.
+///
+public sealed class BytesToExactStringConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value != null ? string.Format(culture, "{0:N0} {1}", (long)value, Strings.Bytes) : "-/-";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Source/NETworkManager.Converters/LvlChartsBandwidthValueConverter.cs b/Source/NETworkManager.Converters/LvlChartsBandwidthValueConverter.cs
deleted file mode 100644
index a2a9f6b09d..0000000000
--- a/Source/NETworkManager.Converters/LvlChartsBandwidthValueConverter.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using System.Globalization;
-using System.Windows.Data;
-using NETworkManager.Utilities;
-
-namespace NETworkManager.Converters;
-
-public sealed class LvlChartsBandwidthValueConverter : IValueConverter
-{
- ///
- ///
- /// ChartPoint.Instance (object)
- ///
- ///
- ///
- ///
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (value is LvlChartsDefaultInfo info)
- return $"{FileSizeConverter.GetBytesReadable((long)info.Value * 8)}it/s";
-
- return "-/-";
- }
-
- 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.Documentation/DocumentationManager.cs b/Source/NETworkManager.Documentation/DocumentationManager.cs
index 6347394f27..f1631e0a45 100644
--- a/Source/NETworkManager.Documentation/DocumentationManager.cs
+++ b/Source/NETworkManager.Documentation/DocumentationManager.cs
@@ -255,6 +255,7 @@ public static DocumentationIdentifier GetIdentifierBySettingsName(SettingsName n
SettingsName.Profiles => DocumentationIdentifier.SettingsProfiles,
SettingsName.Settings => DocumentationIdentifier.SettingsSettings,
SettingsName.Dashboard => GetIdentifierByApplicationName(ApplicationName.Dashboard),
+ SettingsName.NetworkInterface => GetIdentifierByApplicationName(ApplicationName.NetworkInterface),
SettingsName.IPScanner => GetIdentifierByApplicationName(ApplicationName.IPScanner),
SettingsName.PortScanner => GetIdentifierByApplicationName(ApplicationName.PortScanner),
SettingsName.PingMonitor => GetIdentifierByApplicationName(ApplicationName.PingMonitor),
diff --git a/Source/NETworkManager.Models/Network/BandwidthMeter.cs b/Source/NETworkManager.Models/Network/BandwidthMeter.cs
index 22b4afd548..33018b3634 100644
--- a/Source/NETworkManager.Models/Network/BandwidthMeter.cs
+++ b/Source/NETworkManager.Models/Network/BandwidthMeter.cs
@@ -1,5 +1,7 @@
-using System;
+using System;
+using System.Diagnostics;
using System.Linq;
+using System.Net.NetworkInformation;
using System.Windows.Threading;
namespace NETworkManager.Models.Network;
@@ -30,6 +32,12 @@ private void Timer_Tick(object sender, EventArgs e)
#region Variables
+ ///
+ /// The default sample interval in milliseconds. Kept as a constant because the speed
+ /// calculation and the consuming view model derive timing from it.
+ ///
+ public const double DefaultUpdateInterval = 1000;
+
public double UpdateInterval
{
get;
@@ -42,12 +50,13 @@ public double UpdateInterval
field = value;
}
- } = 1000;
+ } = DefaultUpdateInterval;
public bool IsRunning => _timer.IsEnabled;
private readonly DispatcherTimer _timer = new();
private readonly System.Net.NetworkInformation.NetworkInterface _networkInterface;
+ private readonly Stopwatch _stopwatch = new();
private long _previousBytesSent;
private long _previousBytesReceived;
private bool _canUpdate; // Collect initial data for correct calculation
@@ -76,34 +85,60 @@ public void Stop()
{
_timer.Stop();
- // Reset
+ // Reset, so the next sample re-seeds the baseline (avoids a speed spike after a pause).
_canUpdate = false;
}
private void Update()
{
- var stats = _networkInterface.GetIPv4Statistics();
+ // The interface may have been removed/disabled after this meter was created.
+ if (_networkInterface == null)
+ return;
- var totalBytesSent = stats.BytesSent;
- var totalBytesReceived = stats.BytesReceived;
+ IPInterfaceStatistics stats;
- var byteSentSpeed = totalBytesSent - _previousBytesSent;
- var byteReceivedSpeed = totalBytesReceived - _previousBytesReceived;
+ try
+ {
+ // IPStatistics covers both IPv4 and IPv6 traffic on the interface.
+ stats = _networkInterface.GetIPStatistics();
+ }
+ catch (NetworkInformationException)
+ {
+ // Transient failure (e.g. adapter going down) - skip this tick.
+ return;
+ }
- _previousBytesSent = stats.BytesSent;
- _previousBytesReceived = stats.BytesReceived;
+ var totalBytesSent = stats.BytesSent;
+ var totalBytesReceived = stats.BytesReceived;
- // Need to collect initial data for correct calculation...
+ // First sample after start/resume: seed the baseline and start timing, no speed yet.
if (!_canUpdate)
{
+ _previousBytesSent = totalBytesSent;
+ _previousBytesReceived = totalBytesReceived;
+ _stopwatch.Restart();
_canUpdate = true;
return;
}
- OnUpdateSpeed(new BandwidthMeterSpeedArgs(DateTime.Now, totalBytesReceived, totalBytesSent, byteReceivedSpeed,
- byteSentSpeed));
+ var elapsedSeconds = _stopwatch.Elapsed.TotalSeconds;
+ _stopwatch.Restart();
+
+ // Clamp negative deltas: cumulative counters can reset/wrap (driver reset, disable/enable).
+ var deltaSent = Math.Max(0, totalBytesSent - _previousBytesSent);
+ var deltaReceived = Math.Max(0, totalBytesReceived - _previousBytesReceived);
+
+ // Derive a true per-second rate from the measured elapsed time (robust against timer jitter).
+ var byteSentSpeed = elapsedSeconds > 0 ? (long)(deltaSent / elapsedSeconds) : 0;
+ var byteReceivedSpeed = elapsedSeconds > 0 ? (long)(deltaReceived / elapsedSeconds) : 0;
+
+ _previousBytesSent = totalBytesSent;
+ _previousBytesReceived = totalBytesReceived;
+
+ OnUpdateSpeed(new BandwidthMeterSpeedArgs(DateTime.Now, totalBytesReceived, totalBytesSent,
+ byteReceivedSpeed, byteSentSpeed));
}
#endregion
-}
\ No newline at end of file
+}
diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
index 30df1a5117..0252eb58a4 100644
--- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
+++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
@@ -104,6 +104,7 @@ public static class GlobalStaticConfiguration
// Application: Network Interface
public static ExportFileType NetworkInterface_ExportFileType => ExportFileType.Csv;
+ public static int NetworkInterface_BandwidthChartTime => 60;
// Application: WiFi
public static bool WiFi_Show2dot4GHzNetworks => true;
diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs
index 09a73eccb0..ffbcdc1f37 100644
--- a/Source/NETworkManager.Settings/SettingsInfo.cs
+++ b/Source/NETworkManager.Settings/SettingsInfo.cs
@@ -894,6 +894,22 @@ public ExportFileType NetworkInterface_ExportFileType
}
} = GlobalStaticConfiguration.NetworkInterface_ExportFileType;
+ ///
+ /// Gets or sets the bandwidth chart time window in seconds.
+ ///
+ public int NetworkInterface_BandwidthChartTime
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = GlobalStaticConfiguration.NetworkInterface_BandwidthChartTime;
+
#endregion
#region WiFi
diff --git a/Source/NETworkManager.Settings/SettingsName.cs b/Source/NETworkManager.Settings/SettingsName.cs
index 949de02c75..e54842d6c2 100644
--- a/Source/NETworkManager.Settings/SettingsName.cs
+++ b/Source/NETworkManager.Settings/SettingsName.cs
@@ -14,6 +14,7 @@ public enum SettingsName
Profiles,
Settings,
Dashboard,
+ NetworkInterface,
IPScanner,
PortScanner,
PingMonitor,
diff --git a/Source/NETworkManager.Settings/SettingsViewManager.cs b/Source/NETworkManager.Settings/SettingsViewManager.cs
index 08950281ac..7843409dff 100644
--- a/Source/NETworkManager.Settings/SettingsViewManager.cs
+++ b/Source/NETworkManager.Settings/SettingsViewManager.cs
@@ -48,6 +48,8 @@ public static class SettingsViewManager
// Applications
new(SettingsName.Dashboard, ApplicationManager.GetIcon(ApplicationName.Dashboard),
SettingsGroup.Application),
+ new(SettingsName.NetworkInterface, ApplicationManager.GetIcon(ApplicationName.NetworkInterface),
+ SettingsGroup.Application),
new(SettingsName.IPScanner, ApplicationManager.GetIcon(ApplicationName.IPScanner),
SettingsGroup.Application),
new(SettingsName.PortScanner, ApplicationManager.GetIcon(ApplicationName.PortScanner),
diff --git a/Source/NETworkManager/Controls/LiveChartsBandwidthTooltip.xaml b/Source/NETworkManager/Controls/LiveChartsBandwidthTooltip.xaml
new file mode 100644
index 0000000000..0d03d5e0e2
--- /dev/null
+++ b/Source/NETworkManager/Controls/LiveChartsBandwidthTooltip.xaml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/Controls/LiveChartsBandwidthTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsBandwidthTooltip.xaml.cs
new file mode 100644
index 0000000000..85abf5f6b4
--- /dev/null
+++ b/Source/NETworkManager/Controls/LiveChartsBandwidthTooltip.xaml.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+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.Utilities;
+using SkiaSharp;
+
+namespace NETworkManager.Controls;
+
+public partial class LiveChartsBandwidthTooltip : IChartTooltip, INotifyPropertyChanged
+{
+ private readonly Popup _popup;
+
+ public LiveChartsBandwidthTooltip()
+ {
+ InitializeComponent();
+ DataContext = this;
+ _popup = new Popup
+ {
+ AllowsTransparency = true,
+ Placement = PlacementMode.MousePoint,
+ StaysOpen = true,
+ Child = this
+ };
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public string HeaderText
+ {
+ get;
+ private set
+ {
+ if (field == value) return;
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public ObservableCollection TooltipEntries { get; } = [];
+
+ public void Show(IEnumerable tooltipPoints, Chart chart)
+ {
+ var points = tooltipPoints.ToList();
+ if (points.Count == 0) return;
+
+ if (points[0].Context.DataSource is LvlChartsDefaultInfo firstInfo)
+ HeaderText = DateTimeHelper.DateTimeToTimeString(firstInfo.DateTime);
+
+ TooltipEntries.Clear();
+ foreach (var point in points)
+ {
+ var value = point.Context.DataSource is LvlChartsDefaultInfo info
+ ? $"{FileSizeConverter.GetBytesReadable((long)info.Value * 8)}it/s"
+ : "-/-";
+ TooltipEntries.Add(new TooltipEntry(
+ SkColorToBrush(GetSeriesColor(point)),
+ value,
+ point.Context.Series.Name ?? string.Empty));
+ }
+
+ _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 Value, string SeriesName);
+}
diff --git a/Source/NETworkManager/Controls/LvlChartsBandwidthTooltip.xaml b/Source/NETworkManager/Controls/LvlChartsBandwidthTooltip.xaml
deleted file mode 100644
index 8542c1077a..0000000000
--- a/Source/NETworkManager/Controls/LvlChartsBandwidthTooltip.xaml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Source/NETworkManager/Controls/LvlChartsBandwidthTooltip.xaml.cs b/Source/NETworkManager/Controls/LvlChartsBandwidthTooltip.xaml.cs
deleted file mode 100644
index 916a23bcc3..0000000000
--- a/Source/NETworkManager/Controls/LvlChartsBandwidthTooltip.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 LvlChartsBandwidthTooltip : IChartTooltip
-{
- public LvlChartsBandwidthTooltip()
- {
- 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/ViewModels/NetworkInterfaceSettingsViewModel.cs b/Source/NETworkManager/ViewModels/NetworkInterfaceSettingsViewModel.cs
new file mode 100644
index 0000000000..450a2c5a5c
--- /dev/null
+++ b/Source/NETworkManager/ViewModels/NetworkInterfaceSettingsViewModel.cs
@@ -0,0 +1,55 @@
+using NETworkManager.Settings;
+
+namespace NETworkManager.ViewModels;
+
+///
+/// Represents the settings for the Network Interface.
+///
+public class NetworkInterfaceSettingsViewModel : ViewModelBase
+{
+ #region Variables
+
+ private readonly bool _isLoading;
+
+ ///
+ /// Gets or sets the bandwidth chart time window in seconds.
+ ///
+ public int BandwidthChartTime
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ if (!_isLoading)
+ SettingsManager.Current.NetworkInterface_BandwidthChartTime = value;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ #endregion
+
+ #region Constructor, load settings
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public NetworkInterfaceSettingsViewModel()
+ {
+ _isLoading = true;
+
+ LoadSettings();
+
+ _isLoading = false;
+ }
+
+ private void LoadSettings()
+ {
+ BandwidthChartTime = SettingsManager.Current.NetworkInterface_BandwidthChartTime;
+ }
+
+ #endregion
+}
diff --git a/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs b/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs
index 1c265f963e..ec851aa94d 100644
--- a/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs
+++ b/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs
@@ -1,6 +1,9 @@
-using LiveCharts;
-using LiveCharts.Configurations;
-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.Controls;
using MahApps.Metro.SimpleChildWindow;
@@ -14,6 +17,7 @@
using NETworkManager.Settings;
using NETworkManager.Utilities;
using NETworkManager.Views;
+using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -689,15 +693,17 @@ public GridLength ProfileWidth
///
/// Initializes a new instance of the class.
///
- /// The dialog coordinator instance.
public NetworkInterfaceViewModel()
{
_isLoading = true;
- _ = LoadNetworkInterfaces();
-
+ // Initialize the bandwidth chart before loading interfaces, so the chart collections
+ // exist by the time the (possibly synchronous) interface load selects an interface and
+ // starts the bandwidth meter.
InitialBandwidthChart();
+ _ = LoadNetworkInterfaces();
+
// Profiles
CreateTags();
@@ -716,44 +722,193 @@ public NetworkInterfaceViewModel()
NetworkChange.NetworkAvailabilityChanged += (_, _) => ReloadNetworkInterfaces();
NetworkChange.NetworkAddressChanged += (_, _) => ReloadNetworkInterfaces();
+ // React to settings changes (e.g. the configurable bandwidth chart time window)
+ SettingsManager.Current.PropertyChanged += SettingsManager_PropertyChanged;
+
LoadSettings();
_isLoading = false;
}
///
- /// Initializes the bandwidth chart configuration.
+ /// The visible chart time window in seconds, configurable via the settings.
///
+ private static double BandwidthChartWindowSeconds => SettingsManager.Current.NetworkInterface_BandwidthChartTime;
+
+ ///
+ /// Extra share of samples kept beyond the visible window so the chart has a little
+ /// off-screen history (smoother left edge while live + minor pan-back headroom).
+ ///
+ private const double BandwidthValuesHeadroom = 1.1;
+
+ private int _maxBandwidthValues;
+ private bool _updatingBandwidthAxisFromCode;
+ private DateTime _bandwidthSessionStartTime;
+
+ private ObservableCollection _bandwidthReceivedValues;
+ private ObservableCollection _bandwidthSentValues;
+
+ // Capped rolling history kept off-screen so the chart can be rebuilt when the user
+ // returns to live mode after panning/zooming (see BandwidthGoLiveAction).
+ private readonly List _bandwidthReceivedHistory = [];
+ private readonly List _bandwidthSentHistory = [];
+
private void InitialBandwidthChart()
{
- var dayConfig = Mappers.Xy()
- .X(dayModel => (double)dayModel.DateTime.Ticks / TimeSpan.FromHours(1).Ticks)
- .Y(dayModel => dayModel.Value);
+ _bandwidthReceivedValues = [];
+ _bandwidthSentValues = [];
+ _bandwidthSessionStartTime = DateTime.Now;
- Series = new SeriesCollection(dayConfig)
- {
- new LineSeries
+ // Start in live mode so samples are plotted immediately, independent of when the meter
+ // starts relative to this initialization.
+ IsBandwidthLiveMode = true;
+
+ UpdateMaxBandwidthValues();
+
+ var downloadColor = SKColor.Parse("#1ba1e2");
+ var uploadColor = SKColor.Parse("#7fba00");
+
+ 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);
+
+ Series =
+ [
+ new LineSeries
{
- Title = "Download",
- Values = new ChartValues(),
- PointGeometry = null
+ Name = Strings.Download,
+ Values = _bandwidthReceivedValues,
+ Mapping = (info, _) => double.IsNaN(info.Value)
+ ? Coordinate.Empty
+ : new((info.DateTime - _bandwidthSessionStartTime).TotalSeconds, info.Value),
+ GeometrySize = 0,
+ LineSmoothness = 0.3,
+ DataPadding = new LvcPoint(0, 0),
+ Stroke = new SolidColorPaint(downloadColor) { StrokeThickness = 1.5f },
+ Fill = new SolidColorPaint(downloadColor.WithAlpha(0x33))
},
- new LineSeries
+ new LineSeries
{
- Title = "Upload",
- Values = new ChartValues(),
- PointGeometry = null
+ Name = Strings.Upload,
+ Values = _bandwidthSentValues,
+ Mapping = (info, _) => double.IsNaN(info.Value)
+ ? Coordinate.Empty
+ : new((info.DateTime - _bandwidthSessionStartTime).TotalSeconds, info.Value),
+ GeometrySize = 0,
+ LineSmoothness = 0.3,
+ DataPadding = new LvcPoint(0, 0),
+ Stroke = new SolidColorPaint(uploadColor) { StrokeThickness = 1.5f },
+ Fill = new SolidColorPaint(uploadColor.WithAlpha(0x33))
+ }
+ ];
+
+ BandwidthXAxes =
+ [
+ new Axis
+ {
+ Labeler = value => DateTimeHelper.DateTimeToTimeString(
+ _bandwidthSessionStartTime.AddSeconds(value)),
+ TextSize = 10,
+ Padding = new Padding(0, 4, 0, 0),
+ LabelsPaint = new SolidColorPaint(labelColor),
+ SeparatorsPaint = new SolidColorPaint(separatorColor)
+ {
+ StrokeThickness = 1,
+ PathEffect = new DashEffect([10f, 10f])
+ },
+ MinStep = BandwidthChartWindowSeconds / 4.0,
+ ForceStepToMin = true
+ }
+ ];
+
+ BandwidthYAxes =
+ [
+ new Axis
+ {
+ MinLimit = 0,
+ Labeler = value => $"{FileSizeConverter.GetBytesReadable((long)value * 8)}it/s",
+ TextSize = 11,
+ Padding = new Padding(4, 0),
+ LabelsPaint = new SolidColorPaint(labelColor),
+ SeparatorsPaint = new SolidColorPaint(separatorColor)
+ {
+ StrokeThickness = 1,
+ PathEffect = new DashEffect([10f, 10f])
+ }
+ }
+ ];
+
+ BandwidthLegendTextPaint = new SolidColorPaint(labelColor) { SKTypeface = SKTypeface.Default };
+
+ BandwidthXAxes[0].PropertyChanged += (_, args) =>
+ {
+ if (_updatingBandwidthAxisFromCode)
+ return;
+
+ if (args.PropertyName is not (nameof(Axis.MinLimit) or nameof(Axis.MaxLimit)))
+ return;
+
+ IsBandwidthLiveMode = false;
+
+ var axis = BandwidthXAxes[0];
+ if (axis.MinLimit.HasValue && axis.MaxLimit.HasValue)
+ {
+ _updatingBandwidthAxisFromCode = true;
+ axis.MinStep = (axis.MaxLimit.Value - axis.MinLimit.Value) / 4.0;
+ _updatingBandwidthAxisFromCode = false;
}
};
- FormatterDate = value =>
- DateTimeHelper.DateTimeToTimeString(new DateTime((long)(value * TimeSpan.FromHours(1).Ticks)));
- FormatterSpeed = value => $"{FileSizeConverter.GetBytesReadable((long)value * 8)}it/s";
+ UpdateBandwidthXAxisWindow(DateTime.Now);
}
- public Func FormatterDate { get; set; }
- public Func FormatterSpeed { get; set; }
- public SeriesCollection Series { get; set; }
+ ///
+ /// Gets the series collection for the bandwidth chart (download/upload).
+ ///
+ public ISeries[] Series { get; private set; }
+
+ ///
+ /// Gets the X-axes configuration for the bandwidth chart.
+ ///
+ public Axis[] BandwidthXAxes { get; private set; }
+
+ ///
+ /// Gets the Y-axes configuration for the bandwidth chart.
+ ///
+ public Axis[] BandwidthYAxes { get; private set; }
+
+ ///
+ /// Gets the themed paint used for the chart legend text.
+ ///
+ public SolidColorPaint BandwidthLegendTextPaint { get; private set; }
+
+ ///
+ /// Gets a value indicating whether the bandwidth chart is in live (auto-scrolling) mode.
+ ///
+ public bool IsBandwidthLiveMode
+ {
+ get;
+ private set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ private void UpdateMaxBandwidthValues()
+ {
+ // Number of samples that fit in the visible window (derived from the meter's sample
+ // interval), plus a little off-screen headroom.
+ var samplesPerWindow = BandwidthChartWindowSeconds * 1000 / BandwidthMeter.DefaultUpdateInterval;
+ _maxBandwidthValues = (int)Math.Ceiling(samplesPerWindow * BandwidthValuesHeadroom);
+ }
///
/// Loads the network interfaces.
@@ -1523,21 +1678,83 @@ private void ResizeProfile(bool dueToChangedSize)
private void ResetBandwidthChart()
{
- if (Series == null)
+ if (_bandwidthReceivedValues == null)
return;
- Series[0].Values.Clear();
- Series[1].Values.Clear();
+ _bandwidthReceivedValues.Clear();
+ _bandwidthSentValues.Clear();
+ _bandwidthReceivedHistory.Clear();
+ _bandwidthSentHistory.Clear();
- var currentDateTime = DateTime.Now;
+ _bandwidthSessionStartTime = DateTime.Now;
+ IsBandwidthLiveMode = true;
- for (var i = 60; i > 0; i--)
- {
- var bandwidthInfo = new LvlChartsDefaultInfo(currentDateTime.AddSeconds(-i), double.NaN);
+ UpdateBandwidthXAxisWindow(DateTime.Now);
+ }
- Series[0].Values.Add(bandwidthInfo);
- Series[1].Values.Add(bandwidthInfo);
- }
+ private void UpdateBandwidthXAxisWindow(DateTime now)
+ {
+ _updatingBandwidthAxisFromCode = true;
+ var axis = BandwidthXAxes[0];
+ var elapsed = (now - _bandwidthSessionStartTime).TotalSeconds;
+ axis.MinStep = BandwidthChartWindowSeconds / 4.0;
+ axis.MinLimit = elapsed - BandwidthChartWindowSeconds;
+ axis.MaxLimit = elapsed;
+ _updatingBandwidthAxisFromCode = false;
+ }
+
+ ///
+ /// Rescales the Y axis to the current values. The step is derived from a 20% padded
+ /// max and MaxLimit is set to step * 3, so the top label lands exactly on MaxLimit.
+ ///
+ private void UpdateBandwidthYAxis()
+ {
+ var maxVal = _bandwidthReceivedValues.Concat(_bandwidthSentValues)
+ .Where(p => !double.IsNaN(p.Value))
+ .Select(p => p.Value)
+ .DefaultIfEmpty(0)
+ .Max();
+
+ if (!(maxVal > 0))
+ return;
+
+ var yAxis = BandwidthYAxes[0];
+ var step = Math.Ceiling(maxVal * 1.2 / 3.0);
+ yAxis.MinStep = step;
+ yAxis.MaxLimit = step * 3;
+ }
+
+ private void TrimBandwidthHistory(List history)
+ {
+ var excess = history.Count - _maxBandwidthValues;
+ if (excess > 0)
+ history.RemoveRange(0, excess);
+ }
+
+ ///
+ /// Gets the command to return the bandwidth chart to live (auto-scrolling) mode.
+ ///
+ public ICommand BandwidthGoLiveCommand => new RelayCommand(_ => BandwidthGoLiveAction());
+
+ private void BandwidthGoLiveAction()
+ {
+ IsBandwidthLiveMode = true;
+
+ // Samples received while inspecting were not added to the chart, so rebuild the
+ // rolling buffers from the most recent history to resume at the current time.
+ var recentReceived = _bandwidthReceivedHistory
+ .Skip(Math.Max(0, _bandwidthReceivedHistory.Count - _maxBandwidthValues));
+ var recentSent = _bandwidthSentHistory
+ .Skip(Math.Max(0, _bandwidthSentHistory.Count - _maxBandwidthValues));
+
+ _bandwidthReceivedValues = new ObservableCollection(recentReceived);
+ _bandwidthSentValues = new ObservableCollection(recentSent);
+
+ ((LineSeries)Series[0]).Values = _bandwidthReceivedValues;
+ ((LineSeries)Series[1]).Values = _bandwidthSentValues;
+
+ UpdateBandwidthXAxisWindow(DateTime.Now);
+ UpdateBandwidthYAxis();
}
private bool _resetBandwidthStatisticOnNextUpdate;
@@ -1560,6 +1777,8 @@ private void ResumeBandwidthMeter()
if (_bandwidthMeter is not { IsRunning: false })
return;
+ // The meter is only paused while this view is hidden (i.e. another application is shown).
+ // Returning to it starts a fresh measurement: reset the chart and statistics, then start.
ResetBandwidthChart();
_resetBandwidthStatisticOnNextUpdate = true;
@@ -1685,6 +1904,20 @@ private void SearchDispatcherTimer_Tick(object sender, EventArgs e)
IsSearching = false;
}
+ private void SettingsManager_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ switch (e.PropertyName)
+ {
+ case nameof(SettingsInfo.NetworkInterface_BandwidthChartTime):
+ UpdateMaxBandwidthValues();
+
+ // Re-apply the visible window immediately so the change is reflected while running.
+ if (IsBandwidthLiveMode && _bandwidthMeter is { IsRunning: true })
+ UpdateBandwidthXAxisWindow(DateTime.Now);
+ break;
+ }
+ }
+
private void BandwidthMeter_UpdateSpeed(object sender, BandwidthMeterSpeedArgs e)
{
// Reset statistics
@@ -1700,26 +1933,52 @@ private void BandwidthMeter_UpdateSpeed(object sender, BandwidthMeterSpeedArgs e
// Measured time
BandwidthMeasuredTime = DateTime.Now - BandwidthStartTime;
- // Current download/upload
+ // Interface totals (cumulative, since boot) + current speed
BandwidthTotalBytesReceived = e.TotalBytesReceived;
BandwidthTotalBytesSent = e.TotalBytesSent;
BandwidthBytesReceivedSpeed = e.ByteReceivedSpeed;
BandwidthBytesSentSpeed = e.ByteSentSpeed;
- // Total download/upload
+ // A counter reset (e.g. adapter disable/enable, driver reset, sleep/resume) can drop the
+ // cumulative totals below the session baseline. Re-baseline in that case so the session
+ // amounts below never go negative; they then resume counting from the reset point.
+ if (BandwidthTotalBytesReceived < _bandwidthTotalBytesReceivedTemp)
+ _bandwidthTotalBytesReceivedTemp = BandwidthTotalBytesReceived;
+
+ if (BandwidthTotalBytesSent < _bandwidthTotalBytesSentTemp)
+ _bandwidthTotalBytesSentTemp = BandwidthTotalBytesSent;
+
+ // Amount transferred since the measurement started (this session)
BandwidthDiffBytesReceived = BandwidthTotalBytesReceived - _bandwidthTotalBytesReceivedTemp;
BandwidthDiffBytesSent = BandwidthTotalBytesSent - _bandwidthTotalBytesSentTemp;
// Add chart entry
- Series[0].Values.Add(new LvlChartsDefaultInfo(e.DateTime, e.ByteReceivedSpeed));
- Series[1].Values.Add(new LvlChartsDefaultInfo(e.DateTime, e.ByteSentSpeed));
+ var receivedInfo = new LvlChartsDefaultInfo(e.DateTime, e.ByteReceivedSpeed);
+ var sentInfo = new LvlChartsDefaultInfo(e.DateTime, e.ByteSentSpeed);
+
+ // Always record history (capped) so the chart can be rebuilt when returning to live mode.
+ _bandwidthReceivedHistory.Add(receivedInfo);
+ _bandwidthSentHistory.Add(sentInfo);
+ TrimBandwidthHistory(_bandwidthReceivedHistory);
+ TrimBandwidthHistory(_bandwidthSentHistory);
+
+ // While the user inspects the chart (panned/zoomed, i.e. not live), keep the view
+ // frozen: skip updating the visible buffer and axes. New samples are still recorded
+ // above and become visible again via BandwidthGoLiveCommand.
+ if (!IsBandwidthLiveMode)
+ return;
+
+ _bandwidthReceivedValues.Add(receivedInfo);
+ _bandwidthSentValues.Add(sentInfo);
+
+ if (_bandwidthReceivedValues.Count > _maxBandwidthValues)
+ _bandwidthReceivedValues.RemoveAt(0);
- // Remove data older than 60 seconds
- if (Series[0].Values.Count > 59)
- Series[0].Values.RemoveAt(0);
+ if (_bandwidthSentValues.Count > _maxBandwidthValues)
+ _bandwidthSentValues.RemoveAt(0);
- if (Series[1].Values.Count > 59)
- Series[1].Values.RemoveAt(0);
+ UpdateBandwidthXAxisWindow(e.DateTime);
+ UpdateBandwidthYAxis();
}
private void NetworkInterface_UserHasCanceled(object sender, EventArgs e)
diff --git a/Source/NETworkManager/ViewModels/SettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsViewModel.cs
index 36b73fd514..be82a8a755 100644
--- a/Source/NETworkManager/ViewModels/SettingsViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SettingsViewModel.cs
@@ -120,6 +120,7 @@ public SettingsViewInfo SelectedSettingsView
private SettingsSettingsView _settingsSettingsView;
private SettingsProfilesView _settingsProfilesView;
private DashboardSettingsView _dashboardSettingsView;
+ private NetworkInterfaceSettingsView _networkInterfaceSettingsView;
private IPScannerSettingsView _ipScannerSettingsView;
private PortScannerSettingsView _portScannerSettingsView;
private PingMonitorSettingsView _pingMonitorSettingsView;
@@ -265,6 +266,11 @@ private void ChangeSettingsContent(SettingsViewInfo settingsViewInfo)
SettingsContent = _dashboardSettingsView;
break;
+ case SettingsName.NetworkInterface:
+ _networkInterfaceSettingsView ??= new NetworkInterfaceSettingsView();
+
+ SettingsContent = _networkInterfaceSettingsView;
+ break;
case SettingsName.IPScanner:
_ipScannerSettingsView ??= new IPScannerSettingsView();
diff --git a/Source/NETworkManager/Views/NetworkInterfaceSettingsView.xaml b/Source/NETworkManager/Views/NetworkInterfaceSettingsView.xaml
new file mode 100644
index 0000000000..64e1a969ee
--- /dev/null
+++ b/Source/NETworkManager/Views/NetworkInterfaceSettingsView.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/Views/NetworkInterfaceSettingsView.xaml.cs b/Source/NETworkManager/Views/NetworkInterfaceSettingsView.xaml.cs
new file mode 100644
index 0000000000..c0ce345c91
--- /dev/null
+++ b/Source/NETworkManager/Views/NetworkInterfaceSettingsView.xaml.cs
@@ -0,0 +1,14 @@
+using NETworkManager.ViewModels;
+
+namespace NETworkManager.Views;
+
+public partial class NetworkInterfaceSettingsView
+{
+ private readonly NetworkInterfaceSettingsViewModel _viewModel = new();
+
+ public NetworkInterfaceSettingsView()
+ {
+ InitializeComponent();
+ DataContext = _viewModel;
+ }
+}
diff --git a/Source/NETworkManager/Views/NetworkInterfaceView.xaml b/Source/NETworkManager/Views/NetworkInterfaceView.xaml
index ffbddcdcc1..a8054f4002 100644
--- a/Source/NETworkManager/Views/NetworkInterfaceView.xaml
+++ b/Source/NETworkManager/Views/NetworkInterfaceView.xaml
@@ -14,7 +14,7 @@
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
xmlns:settings="clr-namespace:NETworkManager.Settings;assembly=NETworkManager.Settings"
xmlns:internalControls="clr-namespace:NETworkManager.Controls"
- xmlns:liveChart="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
+ xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
xmlns:profiles="clr-namespace:NETworkManager.Profiles;assembly=NETworkManager.Profiles"
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
xmlns:networkManager="clr-namespace:NETworkManager"
@@ -30,8 +30,8 @@
+
-
@@ -841,28 +841,62 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -897,19 +931,23 @@
+ Text="{Binding BandwidthBytesReceivedSpeed, Converter={StaticResource BandwidthBytesToSpeedConverter}}"
+ ToolTip="{Binding BandwidthBytesReceivedSpeed, Converter={StaticResource BandwidthBytesToSpeedConverter}, ConverterParameter=bytes}" />
+ Text="{Binding BandwidthBytesSentSpeed, Converter={StaticResource BandwidthBytesToSpeedConverter}}"
+ ToolTip="{Binding BandwidthBytesSentSpeed, Converter={StaticResource BandwidthBytesToSpeedConverter}, ConverterParameter=bytes}" />
+ Text="{Binding BandwidthTotalBytesReceived, Converter={StaticResource Bytes1000ToSizeConverter}}"
+ ToolTip="{Binding BandwidthTotalBytesReceived, Converter={StaticResource BytesToExactStringConverter}}" />
+ Text="{Binding BandwidthTotalBytesSent, Converter={StaticResource Bytes1000ToSizeConverter}}"
+ ToolTip="{Binding BandwidthTotalBytesSent, Converter={StaticResource BytesToExactStringConverter}}" />
@@ -920,11 +958,13 @@
+ Text="{Binding BandwidthDiffBytesReceived, Converter={StaticResource Bytes1000ToSizeConverter}}"
+ ToolTip="{Binding BandwidthDiffBytesReceived, Converter={StaticResource BytesToExactStringConverter}}" />
+ Text="{Binding BandwidthDiffBytesSent, Converter={StaticResource Bytes1000ToSizeConverter}}"
+ ToolTip="{Binding BandwidthDiffBytesSent, Converter={StaticResource BytesToExactStringConverter}}" />
@@ -1540,7 +1580,7 @@
-
@@ -1548,7 +1588,7 @@
IsChecked="{Binding Path=ProfileFilterTagsMatchAll}"
Margin="10,0,0,0" />
-