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/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..de8066a9ac 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,20 +36,40 @@ 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;
+ }
+
+ CommandLineOptions options = parsed.Value;
+
+ if (options.ListPorts)
+ {
+ Console.WriteLine("COM ports available:");
+ Console.WriteLine(string.Join(", ", SerialPort.GetPortNames()));
}
- using (var port = new SerialPort(portName, 115200))
+ 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)
+ 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;
@@ -56,7 +79,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,22 +102,31 @@ 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 = 13;
+ Length stationAltitude = Length.FromMeters(options.Altitude);
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(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");
Console.WriteLine("Press x to exit");
disp.Output.ScrollUpDelay = TimeSpan.FromMilliseconds(500);
+ disp.Output.ReplaceLine(0, "Initializing...");
+
AutoResetEvent buttonClicked = new AutoResetEvent(false);
+ Pressure? qnhValue = null;
+
+ bool buttonWasHolding = false;
+
void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs)
{
mode++;
@@ -104,39 +136,34 @@ void ChangeMode(object? sender, EventArgs pinValueChangedEventArgs)
mode = 1;
}
+ Console.WriteLine($"Mode changed to {mode}");
buttonClicked.Set();
}
- button.Press += ChangeMode;
-
- var device = board.CreateI2cDevice(new I2cConnectionSettings(0, Bmp280.DefaultI2cAddress));
- Bmp280? bmp;
- try
+ void ButtonHolding(object? sender, ButtonHoldingEventArgs e)
{
- bmp = new Bmp280(device);
- bmp.StandbyTime = StandbyTime.Ms250;
- bmp.SetPowerMode(Bmx280PowerMode.Normal);
- }
- catch (IOException)
- {
- bmp = null;
- Console.WriteLine("BMP280 not available");
+ if (e.HoldingState == ButtonHoldingState.Completed)
+ {
+ buttonWasHolding = true;
+ }
}
- 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;
- }
+ button.Press += ChangeMode;
+ button.Holding += ButtonHolding;
- OpenHardwareMonitor hardwareMonitor = new OpenHardwareMonitor();
- hardwareMonitor.EnableDerivedSensors();
- TimeSpan sleeptime = TimeSpan.FromMilliseconds(500);
string modeName = string.Empty;
+ string modeData = string.Empty;
string previousModeName = string.Empty;
int firstCharInText = 0;
+ 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')
@@ -144,20 +171,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;
}
@@ -165,106 +189,181 @@ 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 / Barometric Pressure";
- if (bmp != null && bmp.TryReadTemperature(out Temperature temp) && bmp.TryReadPressure(out Pressure p2))
+ modeName = "Temperature / Reduced Pressure V1";
+ values = sensors.GetSensor(SensorHandling.BmpSensor);
+ if (values.Temperature.HasValue && values.Pressure.HasValue)
{
- Pressure p3 = WeatherHelper.CalculateBarometricPressure(p2, 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);
+ if (buttonWasHolding)
+ {
+ qnhValue = p3;
+ modeName = "QNH Value stored";
+ buttonWasHolding = false;
+ }
}
else
{
- disp.Output.ReplaceLine(1, "N/A");
+ modeData = "N/A";
}
break;
+
case 4:
- modeName = "Temperature / Humidity";
- if (dht.TryReadDht(3, 11, out temp, out var humidity))
+ modeName = "Raw Pressure";
+ values = sensors.GetSensor(SensorHandling.BmpSensor);
+ if (values.Pressure.HasValue)
{
- disp.Output.ReplaceLine(1, string.Format(CultureInfo.CurrentCulture, "{0:s1} {1:s0}", temp, humidity));
+ 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 = "Dew point";
- if (bmp != null && bmp.TryReadPressure(out p2) && dht.TryReadDht(3, 11, out temp, out humidity))
+ modeName = "Reduced Pressure V2";
+ values = sensors.GetSensor(SensorHandling.BmpSensor);
+ if (values.Temperature.HasValue && values.Pressure.HasValue)
{
- Temperature dewPoint = WeatherHelper.CalculateDewPoint(temp, humidity);
- disp.Output.ReplaceLine(1, dewPoint.ToString("s1", CultureInfo.CurrentCulture));
+ 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
{
- disp.Output.ReplaceLine(1, "N/A");
+ modeData = "N/A";
}
break;
+
case 6:
- modeName = "CPU Temperature";
- if (hardwareMonitor.TryGetAverageCpuTemperature(out temp))
+ modeName = "Temperature / Humidity";
+ values = sensors.GetSensor(SensorHandling.DhtSensor);
+ if (values.Temperature.HasValue && values.Humidity.HasValue)
{
- disp.Output.ReplaceLine(1, temp.ToString("s1", CultureInfo.CurrentCulture));
+ 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 = "GPU Temperature";
- if (hardwareMonitor.TryGetAverageGpuTemperature(out temp))
+ modeName = "Dew point";
+ values = sensors.GetSensor(SensorHandling.DhtSensor);
+ if (values.Temperature.HasValue && values.Humidity.HasValue)
{
- disp.Output.ReplaceLine(1, temp.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";
+ values = sensors.GetSensor(SensorHandling.Cpu);
+ if (values.Temperature.HasValue)
+ {
+ modeData = values.Temperature.Value.ToString("s1", CultureInfo.CurrentCulture);
+ }
+ else
+ {
+ modeData = "N/A";
+ }
+
+ break;
+ case 9:
+ modeName = "GPU Temperature";
+ values = sensors.GetSensor(SensorHandling.Gpu);
+ if (values.Temperature.HasValue)
+ {
+ modeData = values.Temperature.Value.ToString("s1", CultureInfo.CurrentCulture);
+ }
+ else
+ {
+ modeData = "N/A";
+ }
+
+ break;
+ case 10:
modeName = "CPU Load";
- disp.Output.ReplaceLine(1, hardwareMonitor.GetCpuLoad().ToString("s1", CultureInfo.CurrentCulture));
+ values = sensors.GetSensor(SensorHandling.Cpu);
+ if (values.Load.HasValue)
+ {
+ modeData = values.Load.Value.ToString("s1", CultureInfo.CurrentCulture);
+ }
+ else
+ {
+ modeData = "N/A";
+ }
+
break;
- case 9:
+ 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 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
- 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";
+ }
+
+ 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";
}
- disp.Output.ReplaceLine(1, totalEnergy.ToString("s1", CultureInfo.CurrentCulture));
break;
}
@@ -281,25 +380,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 1308bd127d..26c462e784 100644
--- a/src/devices/Arduino/samples/Monitor/Arduino.Monitor.csproj
+++ b/src/devices/Arduino/samples/Monitor/Arduino.Monitor.csproj
@@ -8,10 +8,14 @@
+
+
+
+
diff --git a/src/devices/Arduino/samples/Monitor/CommandLineOptions.cs b/src/devices/Arduino/samples/Monitor/CommandLineOptions.cs
new file mode 100644
index 0000000000..8d9e004413
--- /dev/null
+++ b/src/devices/Arduino/samples/Monitor/CommandLineOptions.cs
@@ -0,0 +1,31 @@
+// 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;
+
+ [Option("listports", Default = false, HelpText = "Print out the list of known COM ports")]
+ public bool ListPorts { get; set; }
+ }
+}
diff --git a/src/devices/Arduino/samples/Monitor/SensorHandling.cs b/src/devices/Arduino/samples/Monitor/SensorHandling.cs
new file mode 100644
index 0000000000..02c7362741
--- /dev/null
+++ b/src/devices/Arduino/samples/Monitor/SensorHandling.cs
@@ -0,0 +1,174 @@
+// 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.Common;
+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;
+ private TimeSliceFilter _rawPressureFilter;
+
+ 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));
+ _rawPressureFilter =
+ new TimeSliceFilter(TimeSpan.FromSeconds(0.5), TimeSliceFilter.AverageFilter);
+
+ 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(100);
+ }
+ }
+
+ 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;
+ }
+ }
+}
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/TimeSliceFilter.cs b/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs
new file mode 100644
index 0000000000..f8997ed797
--- /dev/null
+++ b/src/devices/Common/Iot/Device/Common/TimeSliceFilter.cs
@@ -0,0 +1,167 @@
+// 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 : struct, IAdditionOperators,
+ IDivisionOperators,
+ IComparisonOperators
+ {
+ 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;
+ }
+ }
+
+ ///
+ /// 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)
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ double numElements = 0.0;
+ if (values.Count == 0)
+ {
+ return null;
+ }
+
+ foreach (var value in values)
+ {
+ sum += value;
+ numElements += 1;
+ }
+
+ 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)
+ {
+ if (value > result)
+ {
+ result = value;
+ }
+ }
+
+ 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)
+ {
+ if (value < result)
+ {
+ result = value;
+ }
+ }
+
+ return result;
+ }
+ }
+}
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..14a7f16bf3
--- /dev/null
+++ b/src/devices/Common/Iot/Device/Common/ValueFilter.cs
@@ -0,0 +1,105 @@
+// 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 an element to the filter queue
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ }
+ }
+}
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)
{
diff --git a/src/devices/Common/tests/FilterTests.cs b/src/devices/Common/tests/FilterTests.cs
new file mode 100644
index 0000000000..afbce92591
--- /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.GetValueOrDefault(), 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.GetValueOrDefault(), 0.01);
+ }
+ }
+}