diff --git a/Source/Clima_Demo/MeadowApp.cs b/Source/Clima_Demo/MeadowApp.cs index 599a554..d3a6b35 100644 --- a/Source/Clima_Demo/MeadowApp.cs +++ b/Source/Clima_Demo/MeadowApp.cs @@ -1,7 +1,7 @@ using Meadow; using Meadow.Devices; +using Meadow.Devices.Esp32.MessagePayloads; using Meadow.Hardware; -using System.Collections.Generic; using System.Threading.Tasks; namespace Clima_Demo; @@ -15,16 +15,63 @@ public MeadowApp() mainController = new MainController(); } - public override void OnBootFromCrash(IEnumerable crashReports) - { - mainController.LogAppStartupAfterCrash(crashReports); - } - public override Task Initialize() { + var reliabilityService = Resolver.Services.Get(); + reliabilityService!.MeadowSystemError += OnMeadowSystemError; + if (reliabilityService.LastBootWasFromCrash) + { + mainController.LogAppStartupAfterCrash(reliabilityService.GetCrashData()); + reliabilityService.ClearCrashData(); + } + var wifi = Device.NetworkAdapters.Primary(); mainController.Initialize(Clima.Create(), wifi); return Task.CompletedTask; } + + private void OnMeadowSystemError(MeadowSystemErrorInfo error, bool recommendReset, out bool forceReset) + { + if (error is Esp32SystemErrorInfo espError) + { + Resolver.Log.Warn($"The ESP32 has had an error ({espError.StatusCode})."); + } + else + { + Resolver.Log.Info($"We've had a system error: {error}"); + } + + if (recommendReset) + { + Resolver.Log.Warn($"Meadow is recommending a device reset"); + } + + forceReset = recommendReset; + + // override the reset recommendation + //forceReset = false; + } + + private void OnMeadowSystemError(object sender, MeadowSystemErrorInfo e) + { + Resolver.Log.Error($"App has detected a system error: {e.Message}"); + if (e is Esp32SystemErrorInfo esp) + { + Resolver.Log.Error($"ESP function: {esp.Function}"); + Resolver.Log.Error($"ESP status code: {esp.StatusCode}"); + } + if (e.Exception != null) + { + Resolver.Log.Error($"Exception: {e.Exception.Message}"); + Resolver.Log.Error($"ErrorNumber: {e.ErrorNumber}"); + Resolver.Log.Error($"HResult: {e.Exception.HResult}"); + + if (e.Exception.InnerException != null) + { + Resolver.Log.Error($"InnerException: {e.Exception.InnerException.Message}"); + Resolver.Log.Error($"HResult: {e.Exception.InnerException.HResult}"); + } + } + } } \ No newline at end of file diff --git a/Source/Clima_Demo/meadow.config.yaml b/Source/Clima_Demo/meadow.config.yaml index ae6dfe8..8b632ab 100644 --- a/Source/Clima_Demo/meadow.config.yaml +++ b/Source/Clima_Demo/meadow.config.yaml @@ -2,9 +2,9 @@ Name: Clima Coprocessor: - AutomaticallyStartNetwork: true - AutomaticallyReconnect: true - MaximumRetryCount: 7 + AutomaticallyStartNetwork: false + AutomaticallyReconnect: false + MaximumRetryCount: 0 Network: DefaultInterface: WiFi diff --git a/Source/Clima_Demo/readme.md b/Source/Clima_Demo/readme.md new file mode 100644 index 0000000..f9c48f5 --- /dev/null +++ b/Source/Clima_Demo/readme.md @@ -0,0 +1,27 @@ +```mermaid +flowchart TD +%% Nodes + A("Boot") + B{"Connect to Cloud"} + C("Deliver Data") + D("Shutdown network") + E("Device Sleep") + F("Device Wake") + G("Collect Telemetry") + H{{"`tick++ % pubcount`"}} + + ne_0("== 0") + eq_0("!= 0") + +%% Edge connections between nodes + A --> B + B --> yes --> C + B --> no --> E + C --> D + D --> E + E -.-> F + F --> G + G --> H + H --> eq_0 --> E + H --> ne_0 --> B +``` \ No newline at end of file diff --git a/Source/Meadow.Clima.sln b/Source/Meadow.Clima.sln index 3201975..902740e 100644 --- a/Source/Meadow.Clima.sln +++ b/Source/Meadow.Clima.sln @@ -60,6 +60,8 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommonContracts", "Addition EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serialization.MicroJson", "..\..\Meadow.Foundation\Source\Meadow.Foundation.Libraries_and_Frameworks\Serialization.MicroJson\Driver\Serialization.MicroJson.csproj", "{6300EAB4-806F-4C18-8FE0-57C45A2C0C58}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sensors.Light.Veml7700", "..\..\Meadow.Foundation\Source\Meadow.Foundation.Peripherals\Sensors.Light.Veml7700\Driver\Sensors.Light.Veml7700.csproj", "{C5925D96-F9F4-4F42-AC8D-97E464252A4D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -502,6 +504,18 @@ Global {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Release|iPhone.Build.0 = Release|Any CPU {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Debug|iPhone.Build.0 = Debug|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Release|Any CPU.Build.0 = Release|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Release|iPhone.ActiveCfg = Release|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Release|iPhone.Build.0 = Release|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {C5925D96-F9F4-4F42-AC8D-97E464252A4D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -532,6 +546,7 @@ Global {494082D7-2C48-45A6-8FF7-DD553D27BC4A} = {4AB0FC09-05D2-4F55-9C2D-13C133456E2F} {567267B3-ED96-4FEA-B555-2EE203372EA4} = {4AB0FC09-05D2-4F55-9C2D-13C133456E2F} {6300EAB4-806F-4C18-8FE0-57C45A2C0C58} = {2889A476-F914-49E8-9F97-4CC6CA34A901} + {C5925D96-F9F4-4F42-AC8D-97E464252A4D} = {2889A476-F914-49E8-9F97-4CC6CA34A901} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CA61E123-F783-4CB3-8EB2-099EE930ADD4} diff --git a/Source/Meadow.Clima/Clima.cs b/Source/Meadow.Clima/Clima.cs index eb35032..f7b7a26 100644 --- a/Source/Meadow.Clima/Clima.cs +++ b/Source/Meadow.Clima/Clima.cs @@ -63,7 +63,9 @@ public static IClimaHardware Create() logger?.Info("Failed to instantiate version MCP23008"); } - if (version > 4) + logger?.Info($"MCP Version: {version}"); + + if (version >= 4) { logger?.Info("Instantiating Clima v4 specific hardware"); hardware = new ClimaHardwareV4(ccm, i2cBus, mcpVersion!); diff --git a/Source/Meadow.Clima/Controllers/CloudController.cs b/Source/Meadow.Clima/Controllers/CloudController.cs index 2c25e32..eed99bc 100644 --- a/Source/Meadow.Clima/Controllers/CloudController.cs +++ b/Source/Meadow.Clima/Controllers/CloudController.cs @@ -2,11 +2,23 @@ using Meadow.Cloud; using System; using System.Linq; +using System.Threading.Tasks; namespace Clima_Demo; public class CloudController { + public async Task WaitForDataToSend() + { + // TODO: add a timeout here + while (Resolver.MeadowCloudService.QueueCount > 0) + { + // Resolver.Log.Info($"Waiting for {Resolver.MeadowCloudService.QueueCount} items to be delivered..."); + await Task.Delay(1000); + } + Resolver.Log.Info($"All cloud data has been sent"); + } + public void LogAppStartupAfterCrash() { SendEvent(CloudEventIds.DeviceStarted, $"Device restarted after crash"); diff --git a/Source/Meadow.Clima/Controllers/NetworkController.cs b/Source/Meadow.Clima/Controllers/NetworkController.cs index 89616d2..3e1b74f 100644 --- a/Source/Meadow.Clima/Controllers/NetworkController.cs +++ b/Source/Meadow.Clima/Controllers/NetworkController.cs @@ -1,6 +1,7 @@ using Meadow.Hardware; using System; using System.Threading; +using System.Threading.Tasks; namespace Meadow.Devices; @@ -19,6 +20,16 @@ public class NetworkController public NetworkController(INetworkAdapter networkAdapter) { + if (networkAdapter is IWiFiNetworkAdapter wifi) + { + if (wifi.IsConnected) + { + _ = ReportWiFiScan(wifi); + } + + // TODO: make this configurable + wifi.SetAntenna(AntennaType.External); + } this.networkAdapter = networkAdapter; networkAdapter.NetworkConnected += OnNetworkConnected; @@ -27,6 +38,39 @@ public NetworkController(INetworkAdapter networkAdapter) downEventTimer = new Timer(DownEventTimerProc, null, -1, -1); } + public async Task ConnectToCloud() + { + if (networkAdapter is IWiFiNetworkAdapter wifi) + { + if (!wifi.IsConnected) + { + Resolver.Log.Info("Connecting to network..."); + await wifi.Connect("interwebs", "1234567890"); + } + } + + Resolver.Log.Info($"Connecting to network {(networkAdapter.IsConnected ? "succeeded" : "FAILED")}"); + + return networkAdapter.IsConnected; + } + + public async Task ShutdownNetwork() + { + if (networkAdapter is IWiFiNetworkAdapter wifi) + { + Resolver.Log.Info("Disconnecting network..."); + try + { + await wifi.Disconnect(true); + Resolver.Log.Info("Network disconnected"); + } + catch (Exception ex) + { + Resolver.Log.Info($"Network disconnect failed: {ex.Message}"); + } + } + } + private void DownEventTimerProc(object _) { if (networkAdapter.IsConnected) @@ -46,8 +90,38 @@ private void OnNetworkDisconnected(INetworkAdapter sender, NetworkDisconnectionE ConnectionStateChanged?.Invoke(this, false); } + private async Task ReportWiFiScan(IWiFiNetworkAdapter wifi) + { + var networks = await wifi.Scan(); + + Resolver.Log.Info("WiFi Scan Results"); + if (networks.Count == 0) + { + Resolver.Log.Info("No networks found"); + } + else + { + foreach (var network in networks) + { + if (string.IsNullOrEmpty(network.Ssid)) + { + Resolver.Log.Info($"[no ssid]: {network.SignalDbStrength}dB"); + } + else + { + Resolver.Log.Info($"{network.Ssid}: {network.SignalDbStrength}dB"); + } + } + } + } + private void OnNetworkConnected(INetworkAdapter sender, NetworkConnectionEventArgs args) { + if (sender is IWiFiNetworkAdapter wifi) + { + _ = ReportWiFiScan(wifi); + } + lastDown = null; ConnectionStateChanged?.Invoke(this, true); } diff --git a/Source/Meadow.Clima/Controllers/NotificationController.cs b/Source/Meadow.Clima/Controllers/NotificationController.cs index a958daa..242b646 100644 --- a/Source/Meadow.Clima/Controllers/NotificationController.cs +++ b/Source/Meadow.Clima/Controllers/NotificationController.cs @@ -14,6 +14,17 @@ public enum Warnings BatteryLow = 1 << 2, } + public enum SystemStatus + { + LowPower, + Starting, + SearchingForNetwork, + NetworkConnected, + ConnectingToCloud, + Connected, + + } + private readonly IRgbPwmLed? rgbLed; private Warnings activeWarnings = Warnings.None; @@ -22,14 +33,36 @@ public NotificationController(IRgbPwmLed? rgbLed) this.rgbLed = rgbLed; } - public void SystemStarting() + public void SetSystemStatus(SystemStatus status) { - rgbLed?.SetColor(RgbLedColors.Red); - } + switch (status) + { + case SystemStatus.LowPower: + if (rgbLed != null) + { + rgbLed.StopAnimation(); + rgbLed.IsOn = false; + } + break; + case SystemStatus.Starting: + rgbLed?.SetColor(RgbLedColors.Red); + break; + case SystemStatus.SearchingForNetwork: + rgbLed?.StartBlink(RgbLedColors.Red); + break; + case SystemStatus.NetworkConnected: + rgbLed?.StopAnimation(); + rgbLed?.SetColor(RgbLedColors.Magenta); + break; + case SystemStatus.ConnectingToCloud: + rgbLed?.StartBlink(RgbLedColors.Cyan); + break; + case SystemStatus.Connected: + rgbLed?.StopAnimation(); + rgbLed?.SetColor(RgbLedColors.Green); + break; - public void SystemUp() - { - ReportWarnings(); + } } public void SetWarning(Warnings warning) diff --git a/Source/Meadow.Clima/Controllers/PowerController.cs b/Source/Meadow.Clima/Controllers/PowerController.cs index 04e7550..8e3d5f7 100644 --- a/Source/Meadow.Clima/Controllers/PowerController.cs +++ b/Source/Meadow.Clima/Controllers/PowerController.cs @@ -54,6 +54,15 @@ public async Task GetPowerData() }; } + public void TimedSleep(TimeSpan duration) + { + Resolver.Log.Info("Going to sleep..."); + + Resolver.Device.PlatformOS.Sleep(duration); + + Resolver.Log.Info("PowerController completed sleep"); + } + private void SolarVoltageUpdated(object sender, IChangeResult e) { Resolver.Log.InfoIf(LogPowerData, $"Solar Voltage: {e.New.Volts:0.#} volts"); diff --git a/Source/Meadow.Clima/Controllers/SensorController.cs b/Source/Meadow.Clima/Controllers/SensorController.cs index ecb1454..a94cfc2 100644 --- a/Source/Meadow.Clima/Controllers/SensorController.cs +++ b/Source/Meadow.Clima/Controllers/SensorController.cs @@ -9,48 +9,63 @@ namespace Clima_Demo; public class SensorController { private IClimaHardware hardware; + private CircularBuffer windVaneBuffer = new CircularBuffer(12); + private Length? startupRainValue; + private SensorData latestData; - public bool LogSensorData { get; set; } = false; + private bool LogSensorData { get; set; } = false; public TimeSpan UpdateInterval { get; } = TimeSpan.FromSeconds(5); public SensorController(IClimaHardware clima) { + latestData = new SensorData(); hardware = clima; if (clima.TemperatureSensor is { } temperatureSensor) { temperatureSensor.Updated += TemperatureUpdated; + // atmospheric temp is slow to change + temperatureSensor.StartUpdating(TimeSpan.FromSeconds(15)); temperatureSensor.StartUpdating(UpdateInterval); } if (clima.BarometricPressureSensor is { } pressureSensor) { pressureSensor.Updated += PressureUpdated; - pressureSensor.StartUpdating(UpdateInterval); + // barometric pressure is slow to change + pressureSensor.StartUpdating(TimeSpan.FromMinutes(1)); } if (clima.HumiditySensor is { } humiditySensor) { humiditySensor.Updated += HumidityUpdated; - humiditySensor.StartUpdating(UpdateInterval); + // humidity is slow to change + humiditySensor.StartUpdating(TimeSpan.FromMinutes(1)); } if (clima.CO2ConcentrationSensor is { } co2Sensor) { co2Sensor.Updated += Co2Updated; - co2Sensor.StartUpdating(UpdateInterval); + // CO2 levels are slow to change + co2Sensor.StartUpdating(TimeSpan.FromMinutes(5)); } if (clima.WindVane is { } windVane) { windVane.Updated += WindvaneUpdated; - windVane.StartUpdating(UpdateInterval); + windVane.StartUpdating(TimeSpan.FromSeconds(1)); } if (clima.RainGauge is { } rainGuage) { - rainGuage.Updated += RainGuageUpdated; - rainGuage.StartUpdating(UpdateInterval); + rainGuage.Updated += RainGaugeUpdated; + + // TODO: if we're restarting, we need to rehydrate today's totals already collected + // startupRainValue = rainGuage.Read().Result; + // Resolver.Log.Info($"Startup rain value: {startupRainValue}"); + + // rain does not change frequently + rainGuage.StartUpdating(TimeSpan.FromMinutes(5)); } if (clima.Anemometer is { } anemometer) @@ -58,54 +73,93 @@ public SensorController(IClimaHardware clima) anemometer.Updated += AnemometerUpdated; anemometer.StartUpdating(UpdateInterval); } + + if (clima.LightSensor is { } lightSensor) + { + lightSensor.StartUpdating(UpdateInterval); + } } - public async Task GetSensorData() + public Task GetSensorData() { - return new SensorData - { - Temperature = hardware.TemperatureSensor?.Temperature ?? null, - Pressure = hardware.BarometricPressureSensor?.Pressure ?? null, - Humidity = hardware.HumiditySensor?.Humidity ?? null, - Co2Level = hardware.CO2ConcentrationSensor?.CO2Concentration ?? null, - WindSpeed = hardware.Anemometer?.WindSpeed ?? null, - WindDirection = hardware.WindVane?.WindAzimuth ?? null, - Rain = hardware.RainGauge?.RainDepth ?? null, - }; + lock (latestData) + { + var data = latestData.Copy(); + + latestData.Clear(); + + return Task.FromResult(data); + } } private void TemperatureUpdated(object sender, IChangeResult e) { + lock (latestData) + { + latestData.Temperature = e.New; + } + Resolver.Log.InfoIf(LogSensorData, $"Temperature: {e.New.Celsius:0.#}C"); } private void PressureUpdated(object sender, IChangeResult e) { + lock (latestData) + { + latestData.Pressure = e.New; + } + Resolver.Log.InfoIf(LogSensorData, $"Pressure: {e.New.Millibar:0.#}mbar"); } private void HumidityUpdated(object sender, IChangeResult e) { + lock (latestData) + { + latestData.Humidity = e.New; + } + Resolver.Log.InfoIf(LogSensorData, $"Humidity: {e.New.Percent:0.#}%"); } private void Co2Updated(object sender, IChangeResult e) { + lock (latestData) + { + latestData.Co2Level = e.New; + } Resolver.Log.InfoIf(LogSensorData, $"CO2: {e.New.PartsPerMillion:0.#}ppm"); } private void AnemometerUpdated(object sender, IChangeResult e) { + lock (latestData) + { + latestData.WindSpeed = e.New; + } + Resolver.Log.InfoIf(LogSensorData, $"Anemometer: {e.New.MetersPerSecond:0.#} m/s"); } - private void RainGuageUpdated(object sender, IChangeResult e) + private void RainGaugeUpdated(object sender, IChangeResult e) { + lock (latestData) + { + latestData.Rain = e.New; + } + Resolver.Log.InfoIf(LogSensorData, $"Rain Gauge: {e.New.Millimeters:0.#} mm"); } private void WindvaneUpdated(object sender, IChangeResult e) { - Resolver.Log.InfoIf(LogSensorData, $"Wind Vane: {e.New.Compass16PointCardinalName} ({e.New.Radians:0.#} radians)"); + windVaneBuffer.Append(e.New); + + lock (latestData) + { + latestData.WindDirection = windVaneBuffer.Mean(); + } + + Resolver.Log.InfoIf(LogSensorData, $"Wind Vane: {e.New.DecimalDegrees} (mean: {windVaneBuffer.Mean().DecimalDegrees})"); } } diff --git a/Source/Meadow.Clima/Hardware/ClimaHardwareBase.cs b/Source/Meadow.Clima/Hardware/ClimaHardwareBase.cs index 6105568..24d5fcf 100644 --- a/Source/Meadow.Clima/Hardware/ClimaHardwareBase.cs +++ b/Source/Meadow.Clima/Hardware/ClimaHardwareBase.cs @@ -6,6 +6,7 @@ using Meadow.Peripherals.Sensors; using Meadow.Peripherals.Sensors.Atmospheric; using Meadow.Peripherals.Sensors.Environmental; +using Meadow.Peripherals.Sensors.Light; using Meadow.Peripherals.Sensors.Weather; using System; @@ -63,6 +64,9 @@ public abstract class ClimaHardwareBase : IClimaHardware /// public IAnemometer? Anemometer => GetAnemometer(); + /// + public ILightSensor? LightSensor => GetLightSensor(); + /// public IAnalogInputPort? SolarVoltageInput { get; protected set; } @@ -155,6 +159,8 @@ public IConnector?[] Connectors return _gasResistanceSensor; } + protected virtual ILightSensor? GetLightSensor() => null; + /// /// Get the Wind Vane on the Clima board /// diff --git a/Source/Meadow.Clima/Hardware/ClimaHardwareV4.cs b/Source/Meadow.Clima/Hardware/ClimaHardwareV4.cs index a68165f..685aeff 100644 --- a/Source/Meadow.Clima/Hardware/ClimaHardwareV4.cs +++ b/Source/Meadow.Clima/Hardware/ClimaHardwareV4.cs @@ -1,5 +1,7 @@ using Meadow.Foundation.ICs.IOExpanders; +using Meadow.Foundation.Sensors.Light; using Meadow.Hardware; +using Meadow.Peripherals.Sensors.Light; namespace Meadow.Devices; @@ -8,6 +10,9 @@ namespace Meadow.Devices; /// public class ClimaHardwareV4 : ClimaHardwareV3 { + private ILightSensor? _lightSensor; + private bool _firstLightQuery = true; + /// public override string RevisionString => "v4.x"; @@ -18,7 +23,7 @@ public class ClimaHardwareV4 : ClimaHardwareV3 /// The I2C bus /// The Mcp23008 used to read version information public ClimaHardwareV4(IF7CoreComputeMeadowDevice device, II2cBus i2cBus, Mcp23008 mcpVersion) - : base(device, i2cBus, mcpVersion) + : base(device, i2cBus, mcpVersion) { } @@ -27,12 +32,32 @@ public ClimaHardwareV4(IF7CoreComputeMeadowDevice device, II2cBus i2cBus, Mcp230 Logger?.Trace("Creating Qwiic I2C connector"); return new I2cConnector( - nameof(Qwiic), + nameof(Qwiic), new PinMapping { - new PinMapping.PinAlias(I2cConnector.PinNames.SCL, _device.Pins.I2C1_SCL), - new PinMapping.PinAlias(I2cConnector.PinNames.SDA, _device.Pins.I2C1_SDA), + new PinMapping.PinAlias(I2cConnector.PinNames.SCL, _device.Pins.I2C1_SCL), + new PinMapping.PinAlias(I2cConnector.PinNames.SDA, _device.Pins.I2C1_SDA), }, new I2cBusMapping(_device, 1)); } + + protected override ILightSensor? GetLightSensor() + { + if (_lightSensor == null && _firstLightQuery) + { + try + { + Logger?.Trace("Creating Light sensor"); + _lightSensor = new Veml7700(_device.CreateI2cBus()); + } + catch + { + Logger?.Warn("Light sensor not found on I2C bus"); + } + } + + _firstLightQuery = false; + + return _lightSensor; + } } \ No newline at end of file diff --git a/Source/Meadow.Clima/Hardware/IClimaHardware.cs b/Source/Meadow.Clima/Hardware/IClimaHardware.cs index 2d9d42c..c0c6a71 100644 --- a/Source/Meadow.Clima/Hardware/IClimaHardware.cs +++ b/Source/Meadow.Clima/Hardware/IClimaHardware.cs @@ -4,6 +4,7 @@ using Meadow.Peripherals.Sensors; using Meadow.Peripherals.Sensors.Atmospheric; using Meadow.Peripherals.Sensors.Environmental; +using Meadow.Peripherals.Sensors.Light; using Meadow.Peripherals.Sensors.Weather; namespace Meadow.Devices; @@ -18,6 +19,8 @@ public interface IClimaHardware /// public II2cBus I2cBus { get; } + public ILightSensor? LightSensor { get; } + /// /// Gets the ITemperatureSensor on the Clima board /// diff --git a/Source/Meadow.Clima/MainController.cs b/Source/Meadow.Clima/MainController.cs index a8dc7ab..0a0a08d 100644 --- a/Source/Meadow.Clima/MainController.cs +++ b/Source/Meadow.Clima/MainController.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using static Clima_Demo.NotificationController; namespace Meadow.Devices; @@ -16,7 +17,11 @@ public class MainController private LocationController locationController; private NetworkController? networkController; private CloudController cloudController; - private Timer TelemetryTimer; + private int tick; + private const int SensorReadPeriodSeconds = 10; + private const int PublicationPeriodMinutes = 1; + private bool lowPowerMode = false; + private Timer sleepSimulationTimer; public TimeSpan TelemetryPublicationPeriod { get; } = TimeSpan.FromMinutes(1); @@ -29,7 +34,7 @@ public Task Initialize(IClimaHardware hardware, INetworkAdapter? networkAdapter) notificationController = new NotificationController(hardware.RgbLed); Resolver.Services.Add(notificationController); - notificationController.SystemStarting(); + notificationController.SetSystemStatus(NotificationController.SystemStatus.Starting); cloudController = new CloudController(); @@ -56,25 +61,117 @@ public Task Initialize(IClimaHardware hardware, INetworkAdapter? networkAdapter) if (!networkController.IsConnected) { + notificationController.SetSystemStatus(NotificationController.SystemStatus.SearchingForNetwork); Resolver.Log.Info("Network is down"); - notificationController.SetWarning(NotificationController.Warnings.NetworkDisconnected); + } + else + { + notificationController.SetSystemStatus(NotificationController.SystemStatus.NetworkConnected); + if (Resolver.MeadowCloudService.ConnectionState == CloudConnectionState.Connecting) + { + notificationController.SetSystemStatus(NotificationController.SystemStatus.ConnectingToCloud); + } } } - notificationController.SystemUp(); + Resolver.MeadowCloudService.ConnectionStateChanged += OnMeadowCloudServiceConnectionStateChanged; cloudController.LogAppStartup(hardware.RevisionString); - TelemetryTimer = new Timer(TelemetryTimerProc, null, 0, -1); + Resolver.Device.PlatformOS.AfterWake += PlatformOS_AfterWake; + + if (!lowPowerMode) + { + sleepSimulationTimer = new Timer((_) => PlatformOS_AfterWake(null, WakeSource.Unknown), null, -1, -1); + } + + _ = SystemPreSleepStateProc(); return Task.CompletedTask; } - private async void TelemetryTimerProc(object _) + private void PlatformOS_AfterWake(object sender, WakeSource e) + { + Resolver.Log.Info("PlatformOS_AfterWake"); + SystemPostWakeStateProc(); + } + + private async Task SystemPreSleepStateProc() + { + await CollectTelemetry(); + + // connect to cloud + if (networkController != null) + { + notificationController.SetSystemStatus(SystemStatus.SearchingForNetwork); + var connected = await networkController.ConnectToCloud(); + if (connected) + { + if (cloudController != null) + { + await cloudController.WaitForDataToSend(); + } + + if (lowPowerMode) + { + await networkController.ShutdownNetwork(); + } + } + } + + notificationController.SetSystemStatus(SystemStatus.LowPower); + if (lowPowerMode) + { + powerController.TimedSleep(TimeSpan.FromSeconds(SensorReadPeriodSeconds)); + } + else + { + Resolver.Log.Info("Simulating sleep"); + sleepSimulationTimer.Change(TimeSpan.FromSeconds(SensorReadPeriodSeconds), TimeSpan.FromMilliseconds(-1)); + } + } + + private void SystemPostWakeStateProc() + { + // collect data + + if (++tick % PublicationPeriodMinutes * 60 / SensorReadPeriodSeconds == 0) + { + _ = SystemPreSleepStateProc(); + } + else + { + if (lowPowerMode) + { + powerController.TimedSleep(TimeSpan.FromSeconds(SensorReadPeriodSeconds)); + } + else + { + sleepSimulationTimer.Change(TimeSpan.FromSeconds(SensorReadPeriodSeconds), TimeSpan.FromMilliseconds(-1)); + } + } + } + + private void OnMeadowCloudServiceConnectionStateChanged(object sender, CloudConnectionState e) { + switch (e) + { + case CloudConnectionState.Connected: + notificationController.SetSystemStatus(NotificationController.SystemStatus.Connected); + break; + default: + notificationController.SetSystemStatus(NotificationController.SystemStatus.ConnectingToCloud); + break; + } + } + + private async Task CollectTelemetry() + { + // collect telemetry every tick Resolver.Log.Info($"Collecting telemetry"); try { + // publish telemetry to the cloud every N ticks cloudController.LogTelemetry( await sensorController.GetSensorData(), await powerController.GetPowerData()); @@ -83,8 +180,6 @@ await sensorController.GetSensorData(), { Resolver.Log.Warn($"Failed to log telemetry: {ex.Message}"); } - - TelemetryTimer.Change(TelemetryPublicationPeriod, TimeSpan.FromMilliseconds(-1)); } private void OnNetworkStillDown(object sender, System.TimeSpan e) @@ -92,6 +187,11 @@ private void OnNetworkStillDown(object sender, System.TimeSpan e) Resolver.Log.Info($"Network has been down for {e.TotalSeconds:N0} seconds"); // TODO: after some period, should we force-restart the device? + if (e.TotalMinutes > 5) + { + Resolver.Log.Info($"Network Connection timeout. Resetting the device."); + Resolver.Device.PlatformOS.Reset(); + } } private void OnNetworkConnectionStateChanged(object sender, bool e) @@ -100,6 +200,7 @@ private void OnNetworkConnectionStateChanged(object sender, bool e) { Resolver.Log.Info($"Network connected"); notificationController.ClearWarning(NotificationController.Warnings.NetworkDisconnected); + notificationController.SetSystemStatus(NotificationController.SystemStatus.NetworkConnected); } else { @@ -115,7 +216,7 @@ private void OnBatteryVoltageWarning(object sender, bool e) var message = $"Battery voltage dropped below {powerController.LowBatteryWarningLevel.Volts:N1}"; Resolver.Log.Warn(message); - notificationController.SetWarning(NotificationController.Warnings.BatteryLow); + //notificationController.SetWarning(NotificationController.Warnings.BatteryLow); cloudController.LogWarning(message); } else @@ -135,7 +236,7 @@ private void OnSolarVoltageWarning(object sender, bool e) var message = $"Solar voltage dropped below {powerController.LowSolarWarningLevel.Volts:N1}"; Resolver.Log.Warn(message); - notificationController.SetWarning(NotificationController.Warnings.SolarLoadLow); + //notificationController.SetWarning(NotificationController.Warnings.SolarLoadLow); cloudController.LogWarning(message); } else diff --git a/Source/Meadow.Clima/Meadow.Clima.csproj b/Source/Meadow.Clima/Meadow.Clima.csproj index 5946c38..a3f28e2 100644 --- a/Source/Meadow.Clima/Meadow.Clima.csproj +++ b/Source/Meadow.Clima/Meadow.Clima.csproj @@ -1,4 +1,4 @@ - + 1.11.0 Wilderness Labs, Inc @@ -27,6 +27,7 @@ + diff --git a/Source/Meadow.Clima/Models/SensorData.cs b/Source/Meadow.Clima/Models/SensorData.cs index 03c36dd..15378e3 100644 --- a/Source/Meadow.Clima/Models/SensorData.cs +++ b/Source/Meadow.Clima/Models/SensorData.cs @@ -3,7 +3,7 @@ namespace Clima_Demo; -public record SensorData +public class SensorData { public Temperature? Temperature { get; set; } public Pressure? Pressure { get; set; } @@ -12,6 +12,32 @@ public record SensorData public Speed? WindSpeed { get; set; } public Azimuth? WindDirection { get; set; } public Length? Rain { get; set; } + public Illuminance? Light { get; set; } + + public void Clear() + { + Co2Level = null; + Temperature = null; + Pressure = null; + WindSpeed = null; + WindDirection = null; + Rain = null; + Light = null; + } + + public SensorData Copy() + { + return new SensorData + { + Co2Level = Co2Level, + Temperature = Temperature, + Pressure = Pressure, + WindSpeed = WindSpeed, + WindDirection = WindDirection, + Rain = Rain, + Light = Light, + }; + } public Dictionary AsTelemetryDictionary() { @@ -44,6 +70,10 @@ public Dictionary AsTelemetryDictionary() { d.Add(nameof(Rain), Rain.Value.Centimeters); } + if (Light != null) + { + d.Add(nameof(Light), Light.Value.Lux); + } return d; }