From b467eb97d8d53ac8fd23c701798a6e419ac084de Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Wed, 8 Apr 2026 21:27:17 +0200 Subject: [PATCH 1/8] Get the test setup to work again --- .../Arduino/samples/ApiChecker/TestCases.cs | 2 +- .../samples/Monitor/Arduino.Monitor.cs | 106 ++++++++++++++---- .../samples/Monitor/Arduino.Monitor.csproj | 2 + .../samples/Monitor/CommandLineOptions.cs | 28 +++++ 4 files changed, 116 insertions(+), 22 deletions(-) create mode 100644 src/devices/Arduino/samples/Monitor/CommandLineOptions.cs diff --git a/src/devices/Arduino/samples/ApiChecker/TestCases.cs b/src/devices/Arduino/samples/ApiChecker/TestCases.cs index 5a2505bb06..d9e2d20089 100644 --- a/src/devices/Arduino/samples/ApiChecker/TestCases.cs +++ b/src/devices/Arduino/samples/ApiChecker/TestCases.cs @@ -78,7 +78,7 @@ private static int GetAnalogPin(ArduinoBoard board, int analogChannel) private static void TestI2cBmp280(ArduinoBoard board) { - using var device = board.CreateI2cDevice(new I2cConnectionSettings(0, Bmp280.DefaultI2cAddress)); + using var device = board.CreateI2cDevice(new I2cConnectionSettings(0, 0x76)); using var bmp = new Bmp280(device); bmp.StandbyTime = StandbyTime.Ms250; bmp.SetPowerMode(Bmx280PowerMode.Normal); diff --git a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs index d66f11d92c..eedce363ef 100644 --- a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs +++ b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs @@ -13,15 +13,18 @@ using System.Linq; using System.Text; using System.Threading; +using CommandLine; using Iot.Device.Adc; using Iot.Device.Arduino; using Iot.Device.Arduino.Sample; using Iot.Device.Bmxx80; using Iot.Device.Bmxx80.PowerMode; +using Iot.Device.Board; using Iot.Device.Button; using Iot.Device.Common; using Iot.Device.HardwareMonitor; using UnitsNet; +using UnitsNet.Units; namespace Arduino.Samples { @@ -33,18 +36,32 @@ internal class Program /// The first argument gives the Port name. Default "COM4" public static void Main(string[] args) { - string portName = "COM4"; - if (args.Length > 0) + var parser = new Parser(x => { - portName = args[0]; + x.AutoHelp = true; + x.AutoVersion = true; + x.CaseInsensitiveEnumValues = true; + x.ParsingCulture = CultureInfo.InvariantCulture; + x.CaseSensitive = false; + x.HelpWriter = Console.Out; + }); + + var parsed = parser.ParseArguments(args); + if (parsed.Errors.Any()) + { + // Errors are already printed by the parser, just exit. + return; } - using (var port = new SerialPort(portName, 115200)) + CommandLineOptions options = parsed.Value; + + using (var port = new SerialPort(options.PortName, options.BaudRate)) { - Console.WriteLine($"Connecting to Arduino on {portName}"); + Console.WriteLine($"Connecting to Arduino on {options.PortName}"); try { port.Open(); + port.BaseStream.ReadTimeout = 60000; } catch (UnauthorizedAccessException x) { @@ -56,7 +73,7 @@ public static void Main(string[] args) try { Console.WriteLine($"Firmware version: {board.FirmwareVersion}, Builder: {board.FirmwareName}"); - DisplayModes(board); + DisplayModes(board, options); } catch (TimeoutException x) { @@ -79,11 +96,11 @@ private static void BoardOnLogMessages(string message, Exception? exception) } } - public static void DisplayModes(ArduinoBoard board) + public static void DisplayModes(ArduinoBoard board, CommandLineOptions options) { const int ButtonPin = 2; - const int MaxMode = 10; - Length stationAltitude = Length.FromMeters(650); + const int MaxMode = 12; + Length stationAltitude = Length.FromMeters(options.Altitude); int mode = 0; var gpioController = board.CreateGpioController(); @@ -109,18 +126,33 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) button.Press += ChangeMode; - var device = board.CreateI2cDevice(new I2cConnectionSettings(0, Bmp280.DefaultI2cAddress)); + Console.WriteLine("Scanning for I2C devices..."); + var bus = board.CreateOrGetI2cBus(board.GetDefaultI2cBusNumber()); + var scanned = bus.PerformBusScan(lowest: 0x71); + + int assumedBmp280Address = Bmp280.DefaultI2cAddress; + if (scanned.Contains(Bmp280.DefaultI2cAddress)) + { + assumedBmp280Address = Bmp280.DefaultI2cAddress; + } + else if (scanned.Contains(Bmp280.SecondaryI2cAddress)) + { + assumedBmp280Address = Bmp280.SecondaryI2cAddress; + } + + var device = board.CreateI2cDevice(new I2cConnectionSettings(0, assumedBmp280Address)); Bmp280? bmp; try { bmp = new Bmp280(device); bmp.StandbyTime = StandbyTime.Ms250; bmp.SetPowerMode(Bmx280PowerMode.Normal); + Console.WriteLine($"Found BMP280 at {assumedBmp280Address}"); } catch (IOException) { bmp = null; - Console.WriteLine("BMP280 not available"); + Console.WriteLine($"BMP280 not available at detected address {assumedBmp280Address}"); } DhtSensor? dht = board.GetCommandHandler(); @@ -137,6 +169,8 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) string modeName = string.Empty; string previousModeName = string.Empty; int firstCharInText = 0; + Temperature temp = Temperature.Zero; + Pressure pressure = Pressure.Zero; while (true) { if (Console.KeyAvailable && Console.ReadKey(true).KeyChar == 'x') @@ -170,10 +204,10 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) } case 3: - modeName = "Temperature / Barometric Pressure"; - if (bmp != null && bmp.TryReadTemperature(out Temperature temp) && bmp.TryReadPressure(out Pressure p2)) + modeName = "Temperature / Reduced Pressure V1"; + if (bmp != null && bmp.TryReadTemperature(out temp) && bmp.TryReadPressure(out pressure)) { - Pressure p3 = WeatherHelper.CalculateBarometricPressure(p2, temp, stationAltitude); + Pressure p3 = WeatherHelper.CalculateBarometricPressure(pressure, temp, stationAltitude); disp.Output.ReplaceLine(1, string.Format(CultureInfo.CurrentCulture, "{0:s1} {1:s1}", temp, p3)); } else @@ -182,7 +216,37 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) } break; + case 4: + modeName = "Raw Pressure"; + if (bmp != null && bmp.TryReadPressure(out pressure)) + { + pressure = pressure.ToUnit(PressureUnit.Hectopascal); + disp.Output.ReplaceLine(1, string.Format(CultureInfo.CurrentCulture, "{0:s1}", pressure)); + } + else + { + disp.Output.ReplaceLine(1, "N/A"); + } + + break; + + case 5: + modeName = "Reduced Pressure V2"; + if (bmp != null && bmp.TryReadTemperature(out temp) && bmp.TryReadPressure(out pressure)) + { + Pressure p3 = WeatherHelper.CalculateSeaLevelPressure(pressure, stationAltitude, temp); + p3 = p3.ToUnit(PressureUnit.Hectopascal); + disp.Output.ReplaceLine(1, string.Format(CultureInfo.CurrentCulture, "{0:s1}", p3)); + } + else + { + disp.Output.ReplaceLine(1, "N/A"); + } + + break; + + case 6: modeName = "Temperature / Humidity"; if (dht.TryReadDht(3, 11, out temp, out var humidity)) { @@ -195,9 +259,9 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) break; - case 5: + case 7: modeName = "Dew point"; - if (bmp != null && bmp.TryReadPressure(out p2) && dht.TryReadDht(3, 11, out temp, out humidity)) + if (dht.TryReadDht(3, 11, out temp, out humidity)) { Temperature dewPoint = WeatherHelper.CalculateDewPoint(temp, humidity); disp.Output.ReplaceLine(1, dewPoint.ToString("s1", CultureInfo.CurrentCulture)); @@ -208,7 +272,7 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) } break; - case 6: + case 8: modeName = "CPU Temperature"; if (hardwareMonitor.TryGetAverageCpuTemperature(out temp)) { @@ -220,7 +284,7 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) } break; - case 7: + case 9: modeName = "GPU Temperature"; if (hardwareMonitor.TryGetAverageGpuTemperature(out temp)) { @@ -232,12 +296,12 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) } break; - case 8: + case 10: modeName = "CPU Load"; disp.Output.ReplaceLine(1, hardwareMonitor.GetCpuLoad().ToString("s1", CultureInfo.CurrentCulture)); break; - case 9: + case 11: modeName = "Total power dissipation"; var powerSources = hardwareMonitor.GetSensorList().Where(x => x.SensorType == SensorType.Power); Power totalPower = Power.Zero; @@ -252,7 +316,7 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) disp.Output.ReplaceLine(1, totalPower.ToString("s1", CultureInfo.CurrentCulture)); break; - case 10: + case 12: modeName = "Energy consumed"; var energySources = hardwareMonitor.GetSensorList().Where(x => x.SensorType == SensorType.Energy); Energy totalEnergy = Energy.FromWattHours(0); // Set up the desired output unit diff --git a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.csproj b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.csproj index 1308bd127d..64956b1ad4 100644 --- a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.csproj +++ b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.csproj @@ -8,10 +8,12 @@ + + diff --git a/src/devices/Arduino/samples/Monitor/CommandLineOptions.cs b/src/devices/Arduino/samples/Monitor/CommandLineOptions.cs new file mode 100644 index 0000000000..043e0794ea --- /dev/null +++ b/src/devices/Arduino/samples/Monitor/CommandLineOptions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CommandLine; + +namespace Iot.Device.Arduino.Sample +{ + internal class CommandLineOptions + { + public CommandLineOptions() + { + } + + [Option('p', "port", Default = "COM4", HelpText = "COM Port to use")] + public string PortName { get; set; } = "COM4"; + + [Option('b', "baud", Default = 115200, HelpText = "Connection speed")] + public int BaudRate { get; set; } = 115200; + + [Option("altitude", Default = 650, HelpText = "Specify the station altitude (geoidal height, meters)")] + public float Altitude { get; set; } = 650; + } +} From 9c3d001f3722b94f2ca577ad13901e3c27fea9d4 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Tue, 14 Apr 2026 07:28:49 +0200 Subject: [PATCH 2/8] Some extra output --- .../Arduino/samples/Monitor/Arduino.Monitor.cs | 16 +++++++++++++--- .../samples/Monitor/CommandLineOptions.cs | 3 +++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs index eedce363ef..4ec2ed1e78 100644 --- a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs +++ b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs @@ -55,6 +55,12 @@ public static void Main(string[] args) CommandLineOptions options = parsed.Value; + if (options.ListPorts) + { + Console.WriteLine("COM ports available:"); + Console.WriteLine(string.Join(", ", SerialPort.GetPortNames())); + } + using (var port = new SerialPort(options.PortName, options.BaudRate)) { Console.WriteLine($"Connecting to Arduino on {options.PortName}"); @@ -63,7 +69,7 @@ public static void Main(string[] args) port.Open(); port.BaseStream.ReadTimeout = 60000; } - catch (UnauthorizedAccessException x) + catch (Exception x) when (x is UnauthorizedAccessException || x is FileNotFoundException) { Console.WriteLine($"Could not open COM port: {x.Message} Possible reason: Arduino IDE connected or serial console open"); return; @@ -104,12 +110,15 @@ public static void DisplayModes(ArduinoBoard board, CommandLineOptions options) int mode = 0; var gpioController = board.CreateGpioController(); - GpioButton button = new GpioButton(ButtonPin, false, true, gpioController, false, TimeSpan.FromMilliseconds(200)); + GpioButton button = new GpioButton(ButtonPin, TimeSpan.FromDays(1), TimeSpan.FromSeconds(5), + false, true, gpioController, false, TimeSpan.FromMilliseconds(1000)); CharacterDisplay disp = new CharacterDisplay(board); Console.WriteLine("Display output test"); Console.WriteLine("The button on GPIO 2 changes modes"); Console.WriteLine("Press x to exit"); disp.Output.ScrollUpDelay = TimeSpan.FromMilliseconds(500); + disp.Output.ReplaceLine(0, "Initializing..."); + AutoResetEvent buttonClicked = new AutoResetEvent(false); void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) @@ -121,6 +130,7 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) mode = 1; } + Console.WriteLine($"Mode changed to {mode}"); buttonClicked.Set(); } @@ -165,7 +175,7 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) OpenHardwareMonitor hardwareMonitor = new OpenHardwareMonitor(); hardwareMonitor.EnableDerivedSensors(); - TimeSpan sleeptime = TimeSpan.FromMilliseconds(500); + TimeSpan sleeptime = TimeSpan.FromMilliseconds(400); string modeName = string.Empty; string previousModeName = string.Empty; int firstCharInText = 0; diff --git a/src/devices/Arduino/samples/Monitor/CommandLineOptions.cs b/src/devices/Arduino/samples/Monitor/CommandLineOptions.cs index 043e0794ea..8d9e004413 100644 --- a/src/devices/Arduino/samples/Monitor/CommandLineOptions.cs +++ b/src/devices/Arduino/samples/Monitor/CommandLineOptions.cs @@ -24,5 +24,8 @@ public CommandLineOptions() [Option("altitude", Default = 650, HelpText = "Specify the station altitude (geoidal height, meters)")] public float Altitude { get; set; } = 650; + + [Option("listports", Default = false, HelpText = "Print out the list of known COM ports")] + public bool ListPorts { get; set; } } } From 3c5ea07f3e3b7adddfb455d760c78f227a9243ae Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Tue, 14 Apr 2026 09:55:23 +0200 Subject: [PATCH 3/8] Make tool more responsive --- .../samples/Monitor/Arduino.Monitor.cs | 179 +++++++++--------- .../samples/Monitor/Arduino.Monitor.csproj | 2 + .../Arduino/samples/Monitor/SensorHandling.cs | 170 +++++++++++++++++ .../Arduino/samples/Monitor/SensorValues.cs | 32 ++++ 4 files changed, 289 insertions(+), 94 deletions(-) create mode 100644 src/devices/Arduino/samples/Monitor/SensorHandling.cs create mode 100644 src/devices/Arduino/samples/Monitor/SensorValues.cs diff --git a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs index 4ec2ed1e78..7f2ac443b7 100644 --- a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs +++ b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs @@ -136,51 +136,18 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) button.Press += ChangeMode; - Console.WriteLine("Scanning for I2C devices..."); - var bus = board.CreateOrGetI2cBus(board.GetDefaultI2cBusNumber()); - var scanned = bus.PerformBusScan(lowest: 0x71); - - int assumedBmp280Address = Bmp280.DefaultI2cAddress; - if (scanned.Contains(Bmp280.DefaultI2cAddress)) - { - assumedBmp280Address = Bmp280.DefaultI2cAddress; - } - else if (scanned.Contains(Bmp280.SecondaryI2cAddress)) - { - assumedBmp280Address = Bmp280.SecondaryI2cAddress; - } - - var device = board.CreateI2cDevice(new I2cConnectionSettings(0, assumedBmp280Address)); - Bmp280? bmp; - try - { - bmp = new Bmp280(device); - bmp.StandbyTime = StandbyTime.Ms250; - bmp.SetPowerMode(Bmx280PowerMode.Normal); - Console.WriteLine($"Found BMP280 at {assumedBmp280Address}"); - } - catch (IOException) - { - bmp = null; - Console.WriteLine($"BMP280 not available at detected address {assumedBmp280Address}"); - } - - DhtSensor? dht = board.GetCommandHandler(); - if (dht == null) - { - // Note that this is a software error, hardware support is not tested here. - Console.WriteLine("DHT Sensor module missing"); - return; - } - - OpenHardwareMonitor hardwareMonitor = new OpenHardwareMonitor(); - hardwareMonitor.EnableDerivedSensors(); - TimeSpan sleeptime = TimeSpan.FromMilliseconds(400); string modeName = string.Empty; + string modeData = string.Empty; string previousModeName = string.Empty; int firstCharInText = 0; - Temperature temp = Temperature.Zero; - Pressure pressure = Pressure.Zero; + TimeSpan sleeptime = TimeSpan.FromMilliseconds(400); + + // While accessing the display, we should not try to do any other sensor operation, + // as it may cause the display to display garbage (probably because we handle some bits while + // the display select line is set) + Object displayLock = new object(); + var sensors = new SensorHandling(board, displayLock); + SensorValues? values; while (true) { if (Console.KeyAvailable && Console.ReadKey(true).KeyChar == 'x') @@ -188,20 +155,17 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) break; } - // Default - sleeptime = TimeSpan.FromMilliseconds(500); - switch (mode) { case 0: modeName = "Display ready"; - disp.Output.ReplaceLine(1, "Button for mode"); + modeData = "Button for mode"; // Just text break; case 1: { modeName = "Time"; - disp.Output.ReplaceLine(1, DateTime.Now.ToLongTimeString()); + modeData = DateTime.Now.ToLongTimeString(); sleeptime = TimeSpan.FromMilliseconds(200); break; } @@ -209,136 +173,153 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) case 2: { modeName = "Date"; - disp.Output.ReplaceLine(1, DateTime.Now.ToShortDateString()); + modeData = DateTime.Now.ToShortDateString(); break; } case 3: modeName = "Temperature / Reduced Pressure V1"; - if (bmp != null && bmp.TryReadTemperature(out temp) && bmp.TryReadPressure(out pressure)) + values = sensors.GetSensor(SensorHandling.BmpSensor); + if (values.Temperature.HasValue && values.Pressure.HasValue) { - Pressure p3 = WeatherHelper.CalculateBarometricPressure(pressure, temp, stationAltitude); - disp.Output.ReplaceLine(1, string.Format(CultureInfo.CurrentCulture, "{0:s1} {1:s1}", temp, p3)); + Pressure p3 = WeatherHelper.CalculateBarometricPressure(values.Pressure.Value, values.Temperature.Value, stationAltitude); + modeData = string.Format(CultureInfo.CurrentCulture, "{0:s1} {1:s1}", values.Temperature.Value, p3); } else { - disp.Output.ReplaceLine(1, "N/A"); + modeData = "N/A"; } break; case 4: modeName = "Raw Pressure"; - if (bmp != null && bmp.TryReadPressure(out pressure)) + values = sensors.GetSensor(SensorHandling.BmpSensor); + if (values.Pressure.HasValue) { - pressure = pressure.ToUnit(PressureUnit.Hectopascal); - disp.Output.ReplaceLine(1, string.Format(CultureInfo.CurrentCulture, "{0:s1}", pressure)); + var pressure = values.Pressure.Value.ToUnit(PressureUnit.Hectopascal); + modeData = $"{pressure.Hectopascals:F2} hPa"; } else { - disp.Output.ReplaceLine(1, "N/A"); + modeData = "N/A"; } break; case 5: modeName = "Reduced Pressure V2"; - if (bmp != null && bmp.TryReadTemperature(out temp) && bmp.TryReadPressure(out pressure)) + values = sensors.GetSensor(SensorHandling.BmpSensor); + if (values.Temperature.HasValue && values.Pressure.HasValue) { - Pressure p3 = WeatherHelper.CalculateSeaLevelPressure(pressure, stationAltitude, temp); + Pressure p3 = WeatherHelper.CalculateSeaLevelPressure(values.Pressure.Value, stationAltitude, values.Temperature.Value); p3 = p3.ToUnit(PressureUnit.Hectopascal); - disp.Output.ReplaceLine(1, string.Format(CultureInfo.CurrentCulture, "{0:s1}", p3)); + modeData = string.Format(CultureInfo.CurrentCulture, "{0:s1}", p3); } else { - disp.Output.ReplaceLine(1, "N/A"); + modeData = "N/A"; } break; case 6: modeName = "Temperature / Humidity"; - if (dht.TryReadDht(3, 11, out temp, out var humidity)) + values = sensors.GetSensor(SensorHandling.DhtSensor); + if (values.Temperature.HasValue && values.Humidity.HasValue) { - disp.Output.ReplaceLine(1, string.Format(CultureInfo.CurrentCulture, "{0:s1} {1:s0}", temp, humidity)); + modeData = string.Format(CultureInfo.CurrentCulture, "{0:s1} {1:s0}", values.Temperature.Value, + values.Humidity.Value); } else { - disp.Output.ReplaceLine(1, "N/A"); + modeData = "N/A"; } break; case 7: modeName = "Dew point"; - if (dht.TryReadDht(3, 11, out temp, out humidity)) + values = sensors.GetSensor(SensorHandling.DhtSensor); + if (values.Temperature.HasValue && values.Humidity.HasValue) { - Temperature dewPoint = WeatherHelper.CalculateDewPoint(temp, humidity); - disp.Output.ReplaceLine(1, dewPoint.ToString("s1", CultureInfo.CurrentCulture)); + Temperature dewPoint = WeatherHelper.CalculateDewPoint(values.Temperature.Value, values.Humidity.Value); + modeData = dewPoint.ToString("s1", CultureInfo.CurrentCulture); } else { - disp.Output.ReplaceLine(1, "N/A"); + modeData = "N/A"; } break; case 8: modeName = "CPU Temperature"; - if (hardwareMonitor.TryGetAverageCpuTemperature(out temp)) + values = sensors.GetSensor(SensorHandling.Cpu); + if (values.Temperature.HasValue) { - disp.Output.ReplaceLine(1, temp.ToString("s1", CultureInfo.CurrentCulture)); + modeData = values.Temperature.Value.ToString("s1", CultureInfo.CurrentCulture); } else { - disp.Output.ReplaceLine(1, "N/A"); + modeData = "N/A"; } break; case 9: modeName = "GPU Temperature"; - if (hardwareMonitor.TryGetAverageGpuTemperature(out temp)) + values = sensors.GetSensor(SensorHandling.Gpu); + if (values.Temperature.HasValue) { - disp.Output.ReplaceLine(1, temp.ToString("s1", CultureInfo.CurrentCulture)); + modeData = values.Temperature.Value.ToString("s1", CultureInfo.CurrentCulture); } else { - disp.Output.ReplaceLine(1, "N/A"); + modeData = "N/A"; } break; case 10: modeName = "CPU Load"; - disp.Output.ReplaceLine(1, hardwareMonitor.GetCpuLoad().ToString("s1", CultureInfo.CurrentCulture)); + values = sensors.GetSensor(SensorHandling.Gpu); + if (values.Load.HasValue) + { + modeData = values.Load.Value.ToString("s1", CultureInfo.CurrentCulture); + } + else + { + modeData = "N/A"; + } + break; case 11: modeName = "Total power dissipation"; - var powerSources = hardwareMonitor.GetSensorList().Where(x => x.SensorType == SensorType.Power); - Power totalPower = Power.Zero; - foreach (var power in powerSources) + values = sensors.GetSensor(SensorHandling.Cpu); + + if (values.Power.HasValue) { - if (power.Name != "CPU Cores" && power.TryGetValue(out Power powerConsumption)) // included in CPU Package - { - totalPower = totalPower + powerConsumption; - } + modeData = values.Power.Value.ToString("s1", CultureInfo.CurrentCulture); + } + else + { + modeData = "N/A"; } - disp.Output.ReplaceLine(1, totalPower.ToString("s1", CultureInfo.CurrentCulture)); break; case 12: modeName = "Energy consumed"; - var energySources = hardwareMonitor.GetSensorList().Where(x => x.SensorType == SensorType.Energy); - Energy totalEnergy = Energy.FromWattHours(0); // Set up the desired output unit - foreach (var e in energySources) + values = sensors.GetSensor(SensorHandling.Cpu); + + if (values.Energy.HasValue) { - if (!e.Name.StartsWith("CPU Cores") && e.TryGetValue(out Energy powerConsumption)) // included in CPU Package - { - totalEnergy = totalEnergy + powerConsumption; - } + modeData = values.Energy.Value.ToString("s1", CultureInfo.CurrentCulture); + } + else + { + modeData = "N/A"; } - disp.Output.ReplaceLine(1, totalEnergy.ToString("s1", CultureInfo.CurrentCulture)); break; } @@ -355,25 +336,35 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) firstCharInText = 0; } - disp.Output.ReplaceLine(0, modeName.Substring(firstCharInText)); + lock (displayLock) + { + disp.Output.ReplaceLine(0, modeName.Substring(firstCharInText)); + } } if (modeName != previousModeName) { - disp.Output.ReplaceLine(0, modeName); + lock (displayLock) + { + disp.Output.ReplaceLine(0, modeName); + } previousModeName = modeName; firstCharInText = 0; } + lock (displayLock) + { + disp.Output.ReplaceLine(1, modeData); + } + buttonClicked.WaitOne(sleeptime); } - hardwareMonitor.Dispose(); + sensors.Dispose(); button.Dispose(); disp.Output.Clear(); disp.Dispose(); - bmp?.Dispose(); gpioController.Dispose(); } } diff --git a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.csproj b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.csproj index 64956b1ad4..26c462e784 100644 --- a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.csproj +++ b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.csproj @@ -14,6 +14,8 @@ + + diff --git a/src/devices/Arduino/samples/Monitor/SensorHandling.cs b/src/devices/Arduino/samples/Monitor/SensorHandling.cs new file mode 100644 index 0000000000..099aa9bc20 --- /dev/null +++ b/src/devices/Arduino/samples/Monitor/SensorHandling.cs @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Device.I2c; +using System.IO; +using System.Linq; +using System.Threading; +using Iot.Device.Bmxx80; +using Iot.Device.Bmxx80.PowerMode; +using Iot.Device.Board; +using Iot.Device.HardwareMonitor; +using UnitsNet; + +namespace Iot.Device.Arduino.Sample +{ + internal sealed class SensorHandling : IDisposable + { + private readonly object _displayLock; + public const string BmpSensor = "BMP"; + public const string DhtSensor = "DHT"; + public const string Cpu = "CPU"; + public const string Gpu = "GPU"; + private readonly I2cBus _bus; + private readonly Bmp280? _bmp280; + private readonly DhtSensor? _dht; + private readonly OpenHardwareMonitor _hardwareMonitor; + + private readonly Thread _thread; + private readonly CancellationTokenSource _cancellationTokenSource; + + private readonly ConcurrentDictionary _sensorValues; + + public SensorHandling(ArduinoBoard board, object displayLock) + { + _displayLock = displayLock; + _cancellationTokenSource = new CancellationTokenSource(); + _sensorValues = new ConcurrentDictionary(); + _sensorValues.TryAdd(BmpSensor, new SensorValues(BmpSensor)); + _sensorValues.TryAdd(DhtSensor, new SensorValues(DhtSensor)); + _sensorValues.TryAdd(Cpu, new SensorValues(Cpu)); + _sensorValues.TryAdd(Gpu, new SensorValues(Gpu)); + + Console.WriteLine("Scanning for I2C devices..."); + _bus = board.CreateOrGetI2cBus(board.GetDefaultI2cBusNumber()); + var scanned = _bus.PerformBusScan(lowest: 0x71); + + int assumedBmp280Address = Bmp280.DefaultI2cAddress; + if (scanned.Contains(Bmp280.DefaultI2cAddress)) + { + assumedBmp280Address = Bmp280.DefaultI2cAddress; + } + else if (scanned.Contains(Bmp280.SecondaryI2cAddress)) + { + assumedBmp280Address = Bmp280.SecondaryI2cAddress; + } + + var device = board.CreateI2cDevice(new I2cConnectionSettings(0, assumedBmp280Address)); + try + { + _bmp280 = new Bmp280(device); + _bmp280.StandbyTime = StandbyTime.Ms250; + _bmp280.SetPowerMode(Bmx280PowerMode.Normal); + Console.WriteLine($"Found BMP280 at {assumedBmp280Address}"); + } + catch (IOException) + { + _bmp280 = null; + Console.WriteLine($"BMP280 not available at detected address {assumedBmp280Address}"); + } + + _dht = board.GetCommandHandler(); + if (_dht == null) + { + // Note that this is a software error, hardware support is not tested here. + Console.WriteLine("DHT Sensor module missing"); + } + + _hardwareMonitor = new OpenHardwareMonitor(); + _hardwareMonitor.EnableDerivedSensors(); + _thread = new Thread(SensorThread); + _thread.Start(); + } + + public SensorValues GetSensor(string name) + { + return _sensorValues[name] with { }; + } + + private void SensorThread() + { + while (!_cancellationTokenSource.IsCancellationRequested) + { + lock (_displayLock) + { + if (_bmp280 != null && _bmp280.TryReadPressure(out var p) && _bmp280.TryReadTemperature(out var t)) + { + _sensorValues[BmpSensor].Pressure = p; + _sensorValues[BmpSensor].Temperature = t; + } + + if (_dht != null && _dht.TryReadDht(3, 11, out var t2, out var rh2)) + { + _sensorValues[DhtSensor].Humidity = rh2; + _sensorValues[DhtSensor].Temperature = t2; + } + } + + if (_hardwareMonitor.GetSensorList().Count > 0) + { + if (_hardwareMonitor.TryGetAverageCpuTemperature(out var t3)) + { + _sensorValues[Cpu].Temperature = t3; + } + + _sensorValues[Cpu].Load = _hardwareMonitor.GetCpuLoad(); + + if (_hardwareMonitor.TryGetAverageGpuTemperature(out var t4)) + { + _sensorValues[Gpu].Temperature = t4; + } + + var powerSources = _hardwareMonitor.GetSensorList().Where(x => x.SensorType == SensorType.Power); + Power totalPower = Power.Zero; + foreach (var power in powerSources) + { + if (power.Name != "CPU Cores" && + power.TryGetValue(out Power powerConsumption)) // included in CPU Package + { + totalPower = totalPower + powerConsumption; + } + } + + _sensorValues[Cpu].Power = totalPower; + + var energySources = _hardwareMonitor.GetSensorList().Where(x => x.SensorType == SensorType.Energy); + Energy totalEnergy = Energy.FromWattHours(0); // Set up the desired output unit + foreach (var e in energySources) + { + if (!e.Name.StartsWith("CPU Cores") && + e.TryGetValue(out Energy powerConsumption)) // included in CPU Package + { + totalEnergy = totalEnergy + powerConsumption; + } + } + + _sensorValues[Cpu].Energy = totalEnergy; + } + else + { + _sensorValues[Cpu].Clear(); + _sensorValues[Gpu].Clear(); + } + + Thread.Sleep(500); + } + } + + public void Dispose() + { + _cancellationTokenSource?.Cancel(); + _thread.Join(); + _hardwareMonitor.Dispose(); + _bmp280?.Dispose(); + _dht?.Dispose(); + _bus.Dispose(); + } + } +} diff --git a/src/devices/Arduino/samples/Monitor/SensorValues.cs b/src/devices/Arduino/samples/Monitor/SensorValues.cs new file mode 100644 index 0000000000..0ab42442fd --- /dev/null +++ b/src/devices/Arduino/samples/Monitor/SensorValues.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnitsNet; + +namespace Iot.Device.Arduino.Sample +{ + internal record SensorValues(string Name) + { + public Temperature? Temperature { get; set; } + public Pressure? Pressure { get; set; } + public RelativeHumidity? Humidity { get; set; } + public Ratio? Load { get; set; } + public Power? Power { get; set; } + public Energy? Energy { get; set; } + + public void Clear() + { + Temperature = null; + Pressure = null; + Humidity = null; + Load = null; + Power = null; + Energy = null; + } + } +} From a4f01e836d5007d00b6397efe7e61d882302b972 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Tue, 14 Apr 2026 11:07:48 +0200 Subject: [PATCH 4/8] Bug fixing --- src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs index 7f2ac443b7..43aaaf972e 100644 --- a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs +++ b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs @@ -280,7 +280,7 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) break; case 10: modeName = "CPU Load"; - values = sensors.GetSensor(SensorHandling.Gpu); + values = sensors.GetSensor(SensorHandling.Cpu); if (values.Load.HasValue) { modeData = values.Load.Value.ToString("s1", CultureInfo.CurrentCulture); From 72ca5c2e88d9c137cab23827bc71769375ba57d5 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Wed, 6 May 2026 07:17:06 +0200 Subject: [PATCH 5/8] Added a sample case for "Holding the button" --- .../samples/Monitor/Arduino.Monitor.cs | 50 +++++++++++++++++-- .../Arduino/samples/Monitor/SensorHandling.cs | 2 +- src/devices/Button/ButtonBase.cs | 5 +- .../Common/Iot/Device/Common/WeatherHelper.cs | 4 +- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs index 43aaaf972e..0cdb590c2b 100644 --- a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs +++ b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs @@ -105,13 +105,15 @@ private static void BoardOnLogMessages(string message, Exception? exception) public static void DisplayModes(ArduinoBoard board, CommandLineOptions options) { const int ButtonPin = 2; - const int MaxMode = 12; + const int MaxMode = 13; Length stationAltitude = Length.FromMeters(options.Altitude); int mode = 0; var gpioController = board.CreateGpioController(); - GpioButton button = new GpioButton(ButtonPin, TimeSpan.FromDays(1), TimeSpan.FromSeconds(5), - false, true, gpioController, false, TimeSpan.FromMilliseconds(1000)); + GpioButton button = new GpioButton(ButtonPin, TimeSpan.FromDays(1), TimeSpan.FromSeconds(2), + false, true, gpioController, false, TimeSpan.FromMilliseconds(200)); + button.IsHoldingEnabled = true; + CharacterDisplay disp = new CharacterDisplay(board); Console.WriteLine("Display output test"); Console.WriteLine("The button on GPIO 2 changes modes"); @@ -121,6 +123,10 @@ public static void DisplayModes(ArduinoBoard board, CommandLineOptions options) AutoResetEvent buttonClicked = new AutoResetEvent(false); + Pressure? qnhValue = null; + + bool buttonWasHolding = false; + void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) { mode++; @@ -134,7 +140,16 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) buttonClicked.Set(); } + void ButtonHolding(object? sender, ButtonHoldingEventArgs e) + { + if (e.HoldingState == ButtonHoldingState.Completed) + { + buttonWasHolding = true; + } + } + button.Press += ChangeMode; + button.Holding += ButtonHolding; string modeName = string.Empty; string modeData = string.Empty; @@ -148,6 +163,7 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) Object displayLock = new object(); var sensors = new SensorHandling(board, displayLock); SensorValues? values; + while (true) { if (Console.KeyAvailable && Console.ReadKey(true).KeyChar == 'x') @@ -184,6 +200,12 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) { Pressure p3 = WeatherHelper.CalculateBarometricPressure(values.Pressure.Value, values.Temperature.Value, stationAltitude); modeData = string.Format(CultureInfo.CurrentCulture, "{0:s1} {1:s1}", values.Temperature.Value, p3); + if (buttonWasHolding) + { + qnhValue = p3; + modeName = "QNH Value stored"; + buttonWasHolding = false; + } } else { @@ -215,6 +237,12 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) Pressure p3 = WeatherHelper.CalculateSeaLevelPressure(values.Pressure.Value, stationAltitude, values.Temperature.Value); p3 = p3.ToUnit(PressureUnit.Hectopascal); modeData = string.Format(CultureInfo.CurrentCulture, "{0:s1}", p3); + if (buttonWasHolding) + { + qnhValue = p3; + modeName = "QNH Value stored"; + buttonWasHolding = false; + } } else { @@ -320,6 +348,22 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs) modeData = "N/A"; } + break; + + case 13: + modeName = "Altitude from QNH"; + values = sensors.GetSensor(SensorHandling.BmpSensor); + if (qnhValue.HasValue && values.Pressure.HasValue && values.Temperature.HasValue) + { + Length altitude = WeatherHelper.CalculateAltitude(values.Pressure.Value, qnhValue.Value, + values.Temperature.Value); + modeData = $"{altitude.Meters:F2} m"; + } + else + { + modeData = "N/A"; + } + break; } diff --git a/src/devices/Arduino/samples/Monitor/SensorHandling.cs b/src/devices/Arduino/samples/Monitor/SensorHandling.cs index 099aa9bc20..1ecba9a556 100644 --- a/src/devices/Arduino/samples/Monitor/SensorHandling.cs +++ b/src/devices/Arduino/samples/Monitor/SensorHandling.cs @@ -153,7 +153,7 @@ private void SensorThread() _sensorValues[Gpu].Clear(); } - Thread.Sleep(500); + Thread.Sleep(100); } } diff --git a/src/devices/Button/ButtonBase.cs b/src/devices/Button/ButtonBase.cs index d279045695..664a620f32 100644 --- a/src/devices/Button/ButtonBase.cs +++ b/src/devices/Button/ButtonBase.cs @@ -130,13 +130,16 @@ protected void HandleButtonReleased() IsPressed = false; ButtonUp?.Invoke(this, new EventArgs()); - Press?.Invoke(this, new EventArgs()); if (IsHoldingEnabled && _holdingState == ButtonHoldingState.Started) { _holdingState = ButtonHoldingState.Completed; Holding?.Invoke(this, new ButtonHoldingEventArgs { HoldingState = ButtonHoldingState.Completed }); } + else + { + Press?.Invoke(this, new EventArgs()); + } if (IsDoublePressEnabled) { diff --git a/src/devices/Common/Iot/Device/Common/WeatherHelper.cs b/src/devices/Common/Iot/Device/Common/WeatherHelper.cs index b79e532500..9bf9c3c941 100644 --- a/src/devices/Common/Iot/Device/Common/WeatherHelper.cs +++ b/src/devices/Common/Iot/Device/Common/WeatherHelper.cs @@ -183,8 +183,8 @@ public static Density CalculateAbsoluteHumidity(Temperature airTemperature, Rela /// Temperature measured by the humidity sensor /// Humidity measured /// Temperature measured by better placed sensor - /// A corrected humidity. The value will be lower than the input value if the better placed sensor is cooler than - /// the "bad" sensor. + /// A corrected humidity. The value will be higher than the input value if the better placed sensor is cooler than + /// the "bad" sensor. With the same absolute humidity, a cooler environment can carry less water per volume of air. public static RelativeHumidity GetRelativeHumidityFromActualAirTemperature(Temperature airTemperatureFromHumiditySensor, RelativeHumidity relativeHumidityMeasured, Temperature airTemperatureFromBetterPlacedSensor) { From d23e1cf784ff3c38f14d1504b56788bb2258f921 Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Fri, 8 May 2026 15:19:29 +0200 Subject: [PATCH 6/8] Concept for some filter classes --- .../samples/Monitor/Arduino.Monitor.cs | 2 +- .../Iot/Device/Common/TimeSliceFilter.cs | 87 +++++++++++++++ .../Common/Iot/Device/Common/ValueFilter.cs | 102 ++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs create mode 100644 src/devices/Common/Iot/Device/Common/ValueFilter.cs diff --git a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs index 0cdb590c2b..de8066a9ac 100644 --- a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs +++ b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.cs @@ -355,7 +355,7 @@ void ButtonHolding(object? sender, ButtonHoldingEventArgs e) values = sensors.GetSensor(SensorHandling.BmpSensor); if (qnhValue.HasValue && values.Pressure.HasValue && values.Temperature.HasValue) { - Length altitude = WeatherHelper.CalculateAltitude(values.Pressure.Value, qnhValue.Value, + Length altitude = WeatherHelper.CalculateAltitude(values.Pressure.Value, qnhValue.Value, values.Temperature.Value); modeData = $"{altitude.Meters:F2} m"; } diff --git a/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs b/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs new file mode 100644 index 0000000000..6a3e2539e1 --- /dev/null +++ b/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace Iot.Device.Common +{ + /// + /// A filter that operates on a range of elements based on age + /// + /// A number type + public class TimeSliceFilter : ValueFilter<(T, DateTimeOffset), T> + where T : INumber + { + private TimeSpan _maxAge; + + /// + /// Create an instance of this class + /// + /// The initial length of the filter queue + /// The filter calculate function. Can be one of the predefined + /// calculators or one's own + public TimeSliceFilter(TimeSpan maxAge, Func, T> compute) + { + MaxAge = maxAge; + Compute = compute ?? throw new ArgumentNullException(nameof(compute), "Please provide a filter function"); + } + + /// + /// Computation function + /// + public Func, T> Compute + { + get; + } + + /// + /// The maximum age to use + /// + public TimeSpan MaxAge + { + get + { + return _maxAge; + } + set + { + if (value < TimeSpan.Zero) + { + throw new InvalidOperationException("The filter time must be positive"); + } + + _maxAge = value; + } + } + + /// + /// Default computation function. Can be overriden (or configured via constructor argument) + /// + /// The set of values the calculation needs to operate on + /// The resulting filter value + protected override T FilterAndCompute(IEnumerable<(T, DateTimeOffset)> values) + { + List contents = values.Select(x => x.Item1).ToList(); + + return Compute(contents); + } + + /// + /// Returns true if the element is outdated + /// + /// Element that is checked + /// True if it is older than + protected override bool CanRemove((T, DateTimeOffset) element) + { + var now = DateTimeOffset.UtcNow; + TimeSpan age = now - element.Item2; + return (age > MaxAge); + } + } +} diff --git a/src/devices/Common/Iot/Device/Common/ValueFilter.cs b/src/devices/Common/Iot/Device/Common/ValueFilter.cs new file mode 100644 index 0000000000..eaa0f9bb3a --- /dev/null +++ b/src/devices/Common/Iot/Device/Common/ValueFilter.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Iot.Device.Common +{ + /// + /// An abstract filter class for values. + /// You put in numbers and get out a filtered value of said numbers, based on the concrete implementation + /// + /// Some type that can be filtered + /// The result of the filtering. Can be a single value (e.g. for an averaging filter) + /// or a list of TSource + /// This class is thread safe. Concrete implementations should maintain that. + public abstract class ValueFilter + { + private ConcurrentQueue _valueQueue; + private object _readLock; + + /// + /// Creates a new instance of this class + /// + protected ValueFilter() + { + _valueQueue = new ConcurrentQueue(); + _readLock = new object(); + } + + /// + /// Adds a value to the filter queue + /// + /// A value object + public virtual void AddValue(TSourceElement value) + { + _valueQueue.Enqueue(value); + } + + /// + /// Clears the filter + /// + public virtual void Clear() + { + _valueQueue.Clear(); + } + + /// + /// Tests old elements for removal + /// + private void RemoveOldElements() + { + lock (_readLock) + { + while (_valueQueue.TryPeek(out var element)) + { + if (CanRemove(element)) + { + _valueQueue.TryDequeue(out _); + } + else + { + break; + } + } + } + } + + /// + /// Determine whether the given element can be removed from the queue (e.g. because it is too old) + /// + /// The element to test + /// True to remove, false to not remove. Will not check any further elements when false is returned + protected abstract bool CanRemove(TSourceElement element); + + /// + /// This method gets the current elements. It can filter them if needed, + /// and should return the filtered result (e.g. the average of the remaining elements) + /// + /// List of values + /// The filtered value + /// Filtering is only needed when the value queue is kept longer than the filter + /// size for some reason + protected abstract TResult FilterAndCompute(IEnumerable values); + + /// + /// Computes the current value of the filter + /// + /// + public virtual TResult CurrentValue() + { + RemoveOldElements(); + TSourceElement[] valueArray; + lock (_readLock) + { + valueArray = _valueQueue.ToArray(); + } + + return FilterAndCompute(valueArray); + } + } +} From aa462295c621b0308535fa6eba5d8c8bbdbd90aa Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Mon, 11 May 2026 10:19:20 +0200 Subject: [PATCH 7/8] Designed new filter interface --- src/devices/Arduino/Arduino.sln | 11 ++- .../Iot/Device/Common/TimeSliceFilter.cs | 74 ++++++++++++++++++- .../Common/Iot/Device/Common/ValueFilter.cs | 13 ++-- src/devices/Common/tests/FilterTests.cs | 40 ++++++++++ 4 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 src/devices/Common/tests/FilterTests.cs diff --git a/src/devices/Arduino/Arduino.sln b/src/devices/Arduino/Arduino.sln index cb9737aa3e..79b1dc113b 100644 --- a/src/devices/Arduino/Arduino.sln +++ b/src/devices/Arduino/Arduino.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29905.134 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36804.6 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arduino", "Arduino.csproj", "{0D3AA0FD-4834-4AB1-AD2A-BB12180B61B4}" EndProject @@ -33,6 +33,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Board", "..\Board\Board.csp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Button", "..\Button\Button.csproj", "{8664F653-3DA8-40A6-AD36-80EE13AC2DC2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Tests", "..\Common\tests\Common.Tests.csproj", "{8810B2D1-F55F-4420-A559-387BE41F85C9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -91,6 +93,10 @@ Global {8664F653-3DA8-40A6-AD36-80EE13AC2DC2}.Debug|Any CPU.Build.0 = Debug|Any CPU {8664F653-3DA8-40A6-AD36-80EE13AC2DC2}.Release|Any CPU.ActiveCfg = Release|Any CPU {8664F653-3DA8-40A6-AD36-80EE13AC2DC2}.Release|Any CPU.Build.0 = Release|Any CPU + {8810B2D1-F55F-4420-A559-387BE41F85C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8810B2D1-F55F-4420-A559-387BE41F85C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8810B2D1-F55F-4420-A559-387BE41F85C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8810B2D1-F55F-4420-A559-387BE41F85C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -99,6 +105,7 @@ Global {2670F7BF-A7C8-49EB-9A99-1719A90D0C67} = {9E5A25ED-9839-4C1A-9B27-993437D1CB31} {23B4B60C-9594-42BB-9D25-C54983B0F809} = {9E5A25ED-9839-4C1A-9B27-993437D1CB31} {273C9233-8A7D-4A0B-8DB2-FB7F56FA727D} = {CA26B999-4C0E-4E82-A46E-A68AC1B85C10} + {8810B2D1-F55F-4420-A559-387BE41F85C9} = {CA26B999-4C0E-4E82-A46E-A68AC1B85C10} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {47BF9684-1876-4AC1-8C87-04B8C7FA6C3A} diff --git a/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs b/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs index 6a3e2539e1..860f056b6a 100644 --- a/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs +++ b/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs @@ -16,7 +16,7 @@ namespace Iot.Device.Common /// /// A number type public class TimeSliceFilter : ValueFilter<(T, DateTimeOffset), T> - where T : INumber + where T : struct, INumber { private TimeSpan _maxAge; @@ -60,6 +60,15 @@ public TimeSpan MaxAge } } + /// + /// Adds an element to the front of the filter queue with the current timestamp + /// + /// The value to insert + public void Add(T value) + { + AddElement((value, DateTimeOffset.UtcNow)); + } + /// /// Default computation function. Can be overriden (or configured via constructor argument) /// @@ -83,5 +92,68 @@ protected override bool CanRemove((T, DateTimeOffset) element) TimeSpan age = now - element.Item2; return (age > MaxAge); } + + /// + /// Calculates the arithmetic average of the current filter entries. + /// + /// The values to average over + /// The return value + public static T? AverageFilter(List values) + { + T sum = default(T); + T numElements = default(T); + if (values.Count == 0) + { + return null; + } + + foreach (var value in values) + { + sum += value; + numElements++; + } + + return sum / numElements; + } + + /// + /// Returns the maximum of the current queue + /// + public static T? Maximum(List values) + { + if (values.Count == 0) + { + return null; + } + + T result = values[0]; + + foreach (var value in values) + { + result = T.Max(value, result); + } + + return result; + } + + /// + /// Returns the maximum of the current queue + /// + public static T? Minimum(List values) + { + if (values.Count == 0) + { + return null; + } + + T result = values[0]; + + foreach (var value in values) + { + result = T.Min(value, result); + } + + return result; + } } } diff --git a/src/devices/Common/Iot/Device/Common/ValueFilter.cs b/src/devices/Common/Iot/Device/Common/ValueFilter.cs index eaa0f9bb3a..14a7f16bf3 100644 --- a/src/devices/Common/Iot/Device/Common/ValueFilter.cs +++ b/src/devices/Common/Iot/Device/Common/ValueFilter.cs @@ -29,10 +29,13 @@ protected ValueFilter() } /// - /// Adds a value to the filter queue + /// Adds an element to the filter queue /// - /// A value object - public virtual void AddValue(TSourceElement value) + /// A filter queue element + /// + /// It's typically favorable to use a method of the derived class instead. + /// + public virtual void AddElement(TSourceElement value) { _valueQueue.Enqueue(value); } @@ -81,13 +84,13 @@ private void RemoveOldElements() /// The filtered value /// Filtering is only needed when the value queue is kept longer than the filter /// size for some reason - protected abstract TResult FilterAndCompute(IEnumerable values); + protected abstract TResult? FilterAndCompute(IEnumerable values); /// /// Computes the current value of the filter /// /// - public virtual TResult CurrentValue() + public virtual TResult? CurrentValue() { RemoveOldElements(); TSourceElement[] valueArray; diff --git a/src/devices/Common/tests/FilterTests.cs b/src/devices/Common/tests/FilterTests.cs new file mode 100644 index 0000000000..0ee13f6874 --- /dev/null +++ b/src/devices/Common/tests/FilterTests.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Iot.Device.Common; +using Xunit; + +namespace Common.Tests +{ + public class FilterTests + { + [Fact] + public void SimpleCase() + { + var filter1 = new TimeSliceFilter(TimeSpan.MaxValue, TimeSliceFilter.AverageFilter); + filter1.Add(1); + filter1.Add(2); + double result = filter1.CurrentValue(); + Assert.Equal(1.5, result, 0.01); + } + + [Fact] + public void ElementsAreRemovedAfterTimeout() + { + var filter1 = new TimeSliceFilter(TimeSpan.Zero, TimeSliceFilter.AverageFilter); + filter1.Add(1); + Thread.Sleep(100); + filter1.CurrentValue(); + filter1.MaxAge = TimeSpan.FromMinutes(1); + filter1.Add(2); + double result = filter1.CurrentValue(); + Assert.Equal(2.0, result, 0.01); + } + } +} From ee743d284aa4099be28f3072c9282a6cc7ec59ff Mon Sep 17 00:00:00 2001 From: Patrick Grawehr Date: Thu, 28 May 2026 09:24:12 +0200 Subject: [PATCH 8/8] Start eventually using the new filter type --- .../Arduino/samples/Monitor/SensorHandling.cs | 4 +++ .../Iot/Device/Common/TimeSliceFilter.cs | 26 ++++++++++++------- src/devices/Common/tests/FilterTests.cs | 8 +++--- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/devices/Arduino/samples/Monitor/SensorHandling.cs b/src/devices/Arduino/samples/Monitor/SensorHandling.cs index 1ecba9a556..02c7362741 100644 --- a/src/devices/Arduino/samples/Monitor/SensorHandling.cs +++ b/src/devices/Arduino/samples/Monitor/SensorHandling.cs @@ -10,6 +10,7 @@ using Iot.Device.Bmxx80; using Iot.Device.Bmxx80.PowerMode; using Iot.Device.Board; +using Iot.Device.Common; using Iot.Device.HardwareMonitor; using UnitsNet; @@ -31,6 +32,7 @@ internal sealed class SensorHandling : IDisposable private readonly CancellationTokenSource _cancellationTokenSource; private readonly ConcurrentDictionary _sensorValues; + private TimeSliceFilter _rawPressureFilter; public SensorHandling(ArduinoBoard board, object displayLock) { @@ -41,6 +43,8 @@ public SensorHandling(ArduinoBoard board, object displayLock) _sensorValues.TryAdd(DhtSensor, new SensorValues(DhtSensor)); _sensorValues.TryAdd(Cpu, new SensorValues(Cpu)); _sensorValues.TryAdd(Gpu, new SensorValues(Gpu)); + _rawPressureFilter = + new TimeSliceFilter(TimeSpan.FromSeconds(0.5), TimeSliceFilter.AverageFilter); Console.WriteLine("Scanning for I2C devices..."); _bus = board.CreateOrGetI2cBus(board.GetDefaultI2cBusNumber()); diff --git a/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs b/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs index 860f056b6a..f8997ed797 100644 --- a/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs +++ b/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs @@ -15,8 +15,10 @@ namespace Iot.Device.Common /// A filter that operates on a range of elements based on age /// /// A number type - public class TimeSliceFilter : ValueFilter<(T, DateTimeOffset), T> - where T : struct, INumber + public class TimeSliceFilter : ValueFilter<(T, DateTimeOffset), T?> + where T : struct, IAdditionOperators, + IDivisionOperators, + IComparisonOperators { private TimeSpan _maxAge; @@ -26,7 +28,7 @@ public class TimeSliceFilter : ValueFilter<(T, DateTimeOffset), T> /// The initial length of the filter queue /// The filter calculate function. Can be one of the predefined /// calculators or one's own - public TimeSliceFilter(TimeSpan maxAge, Func, T> compute) + public TimeSliceFilter(TimeSpan maxAge, Func, T?> compute) { MaxAge = maxAge; Compute = compute ?? throw new ArgumentNullException(nameof(compute), "Please provide a filter function"); @@ -35,7 +37,7 @@ public TimeSliceFilter(TimeSpan maxAge, Func, T> compute) /// /// Computation function /// - public Func, T> Compute + public Func, T?> Compute { get; } @@ -74,7 +76,7 @@ public void Add(T value) /// /// The set of values the calculation needs to operate on /// The resulting filter value - protected override T FilterAndCompute(IEnumerable<(T, DateTimeOffset)> values) + protected override T? FilterAndCompute(IEnumerable<(T, DateTimeOffset)> values) { List contents = values.Select(x => x.Item1).ToList(); @@ -101,7 +103,7 @@ protected override bool CanRemove((T, DateTimeOffset) element) public static T? AverageFilter(List values) { T sum = default(T); - T numElements = default(T); + double numElements = 0.0; if (values.Count == 0) { return null; @@ -110,7 +112,7 @@ protected override bool CanRemove((T, DateTimeOffset) element) foreach (var value in values) { sum += value; - numElements++; + numElements += 1; } return sum / numElements; @@ -130,7 +132,10 @@ protected override bool CanRemove((T, DateTimeOffset) element) foreach (var value in values) { - result = T.Max(value, result); + if (value > result) + { + result = value; + } } return result; @@ -150,7 +155,10 @@ protected override bool CanRemove((T, DateTimeOffset) element) foreach (var value in values) { - result = T.Min(value, result); + if (value < result) + { + result = value; + } } return result; diff --git a/src/devices/Common/tests/FilterTests.cs b/src/devices/Common/tests/FilterTests.cs index 0ee13f6874..afbce92591 100644 --- a/src/devices/Common/tests/FilterTests.cs +++ b/src/devices/Common/tests/FilterTests.cs @@ -20,8 +20,8 @@ public void SimpleCase() var filter1 = new TimeSliceFilter(TimeSpan.MaxValue, TimeSliceFilter.AverageFilter); filter1.Add(1); filter1.Add(2); - double result = filter1.CurrentValue(); - Assert.Equal(1.5, result, 0.01); + double? result = filter1.CurrentValue(); + Assert.Equal(1.5, result.GetValueOrDefault(), 0.01); } [Fact] @@ -33,8 +33,8 @@ public void ElementsAreRemovedAfterTimeout() filter1.CurrentValue(); filter1.MaxAge = TimeSpan.FromMinutes(1); filter1.Add(2); - double result = filter1.CurrentValue(); - Assert.Equal(2.0, result, 0.01); + double? result = filter1.CurrentValue(); + Assert.Equal(2.0, result.GetValueOrDefault(), 0.01); } } }