Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 1.12.0 #530

Merged
merged 1 commit into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/develop-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ jobs:
with:
dotnet-version:
7.0.x

- name: Install MAUI Workload
run: dotnet workload install maui --ignore-failed-sources

- name: Workload Restore Meadow.Core
run: dotnet workload restore Meadow.Core/source/Meadow.Core.sln
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 8.0.x
dotnet-version: 7.0.x

- name: Workload Restore Meadow.Core
run: dotnet workload restore Meadow.Core/source/Meadow.Core.sln
Expand Down
16 changes: 13 additions & 3 deletions source/Meadow.Core/Cloud/HealthReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ public async Task Start(int interval)
{
Resolver.Log.Info($"Health Metrics enabled with interval: {interval} minute(s).");

if (interval < 0)
{
throw new ArgumentException("HealthReporter interval must be a non-negative integer.");
}

if (interval == 0)
{
// do not set the timer. metrics can be sent manually.
return;
}

System.Timers.Timer timer = new(interval: interval * 60 * 1000);
timer.Elapsed += async (sender, e) => await TimerOnElapsed(sender, e);
timer.AutoReset = true;
Expand All @@ -35,7 +46,7 @@ public async Task Start(int interval)

await Send();
}

Resolver.Device.NetworkAdapters.NetworkConnected += async (sender, args) =>
{
// TODO: what happens if we disconnect and reconnect?
Expand Down Expand Up @@ -73,7 +84,7 @@ public async Task Send()
Resolver.Log.Trace("could not send health metric, connection unavailable.");
return;
}

var service = Resolver.Services.Get<IMeadowCloudService>();
var device = Resolver.Device;

Expand All @@ -87,7 +98,6 @@ public async Task Send()
{ "health.memory_used", GC.GetTotalMemory(false) },
{ "health.disk_space_used", device.PlatformOS.GetPrimaryDiskSpaceInUse().Bytes },
{ "info.os_version", device.Information.OSVersion },

},
Timestamp = DateTimeOffset.UtcNow
};
Expand Down
104 changes: 72 additions & 32 deletions source/Meadow.Core/Cloud/MeadowCloudConnectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ namespace Meadow;

internal class MeadowCloudConnectionService : IMeadowCloudService
{
/// <inheritdoc/>
public event EventHandler<Exception>? ErrorOccurred;

/// <inheritdoc/>
public event EventHandler<CloudConnectionState>? ConnectionStateChanged;

internal event EventHandler<MqttApplicationMessage>? MqttMessageReceived;
Expand Down Expand Up @@ -67,6 +70,7 @@ internal MeadowCloudConnectionService(IMeadowCloudSettings settings)
_storeAndForwardTimer = new Timer(DataForwarderProc, null, TimeSpan.FromMilliseconds(-1), TimeSpan.FromMilliseconds(-1));
}

/// <inheritdoc/>
public CloudConnectionState ConnectionState
{
get => _connectionState;
Expand All @@ -78,17 +82,19 @@ private set
}
}

/// <inheritdoc/>
public int QueueCount { get => _dataQueue.Count; }

private async void DataForwarderProc(object _)
{
CloudDataQueue.DataInfo? info = null;
if (_dataQueue.Count == 0) { return; }

try
{
if (ConnectionState == CloudConnectionState.Connected)
{
// get the head item
info = _dataQueue.Peek();

var info = _dataQueue.Peek();
if (info != null)
{
// failed to send, leave the item in the queue
Expand All @@ -103,7 +109,7 @@ private async void DataForwarderProc(object _)
}
catch (Exception ex)
{
Resolver.Log.Warn($"Unable to forward Meadow Cloud record: {ex.Message}");
LogAndRaiseOnErrorOccurredEvent("Unable to forward Meadow Cloud record", ex);
}

// schedule the next send
Expand All @@ -120,6 +126,30 @@ private async void DataForwarderProc(object _)
_storeAndForwardTimer.Change(nextSend, TimeSpan.FromMilliseconds(-1));
}

private void LogAndRaiseOnErrorOccurredEvent(string message, Exception? ex = null)
{
if (ex != null)
{
message = $"{message}{Environment.NewLine}{ex.Message}";
if (ex.HResult != 0)
{
message = $"{message}({ex.HResult})";
}
if (ex.InnerException != null)
{
message = $"{message}{Environment.NewLine}{ex.InnerException.Message}";
if (ex.InnerException.HResult != 0)
{
message = $"{message}({ex.InnerException.HResult})";
}
}
}

Resolver.Log.Warn(message);
ErrorOccurred?.Invoke(this, new MeadowCloudException(message, ex));
}

/// <inheritdoc/>
public void AddSubscription(string topic)
{
// pause the state machine
Expand Down Expand Up @@ -173,6 +203,7 @@ public void Start()
_storeAndForwardTimer.Change(TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(-1));
}

/// <inheritdoc/>
public void Stop()
{
_stopService = true;
Expand Down Expand Up @@ -298,7 +329,7 @@ private async void ConnectionStateMachine()
Resolver.Log.Error($"Failed to authenticate with Meadow.Cloud: {ae.Message}");
if (ae.InnerException != null)
{
Resolver.Log.Error($" Inner Exception ({ae.InnerException.GetType().Name}): {ae.InnerException.Message}");
Resolver.Log.Error($"Inner Exception ({ae.InnerException.GetType().Name}): {ae.InnerException.Message}");
}
await Task.Delay(TimeSpan.FromSeconds(Settings.ConnectRetrySeconds));
}
Expand Down Expand Up @@ -350,6 +381,10 @@ private async void ConnectionStateMachine()
catch (Exception ex)
{
Resolver.Log.Error($"Error connecting to Meadow.Cloud: {ex.Message}");
if (ex.InnerException != null)
{
Resolver.Log.Error($"Inner Exception ({ex.InnerException.GetType().Name}): {ex.InnerException.Message}");
}
ConnectionState = CloudConnectionState.Disconnected;
// just delay for a while
await Task.Delay(TimeSpan.FromSeconds(Settings.ConnectRetrySeconds));
Expand Down Expand Up @@ -403,7 +438,10 @@ private async void ConnectionStateMachine()
catch (Exception ex)
{
Resolver.Log.Error($"Error subscribing to Meadow.Cloud: {ex.Message}");

if (ex.InnerException != null)
{
Resolver.Log.Error($"Inner Exception ({ex.InnerException.GetType().Name}): {ex.InnerException.Message}");
}
// if subscribing fails, then we need to disconnect from the server
await MqttClient.DisconnectAsync();

Expand All @@ -418,6 +456,10 @@ private async void ConnectionStateMachine()
_firstConection = false;
}
}

// trigger a send
_storeAndForwardTimer.Change(TimeSpan.Zero, TimeSpan.FromMilliseconds(-1));

Thread.Sleep(1000);
break;
}
Expand Down Expand Up @@ -487,6 +529,7 @@ public JsonIdPayload(string id)
public string Id { get; set; } = default!;
}

/// <inheritdoc/>
public async Task<bool> Authenticate()
{
string errorMessage;
Expand Down Expand Up @@ -537,18 +580,14 @@ public async Task<bool> Authenticate()
{
// dev note: bug in pre-0.9.6.3 on F7 will provision with a bad key and end up here
// TODO: add platform and OS checking for this?
errorMessage = $"RSA decrypt failure. This device likely needs to be reprovisioned.";
Resolver.Log.Error(errorMessage);
ErrorOccurred?.Invoke(this, new MeadowCloudException(errorMessage));
LogAndRaiseOnErrorOccurredEvent("RSA decrypt failure. This device likely needs to be reprovisioned.");

_jwt = null;
return false;
}
catch (Exception ex)
{
errorMessage = $"RSA decrypt failure: {ex.Message}";
Resolver.Log.Error(errorMessage);
ErrorOccurred?.Invoke(this, new MeadowCloudException(errorMessage));
LogAndRaiseOnErrorOccurredEvent("RSA decrypt failure", ex);

_jwt = null;
return false;
Expand All @@ -571,47 +610,43 @@ public async Task<bool> Authenticate()
}
catch (Exception ex)
{
errorMessage = $"AES decrypt failure: {ex.Message}";
Resolver.Log.Error(errorMessage);
ErrorOccurred?.Invoke(this, new MeadowCloudException(errorMessage));
LogAndRaiseOnErrorOccurredEvent("AES decrypt failure", ex);

_jwt = null;
return false;
}
}

if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
if (response.StatusCode == HttpStatusCode.NotFound)
{
// device is likely not provisioned?
errorMessage = $"Meadow.Cloud service returned 'Not Found': this device has likely not been provisioned";
}
else
else if (response.StatusCode == HttpStatusCode.InternalServerError)
{
errorMessage = $"Meadow.Cloud service login returned {response.StatusCode}: {responseContent}";
}
else
{
errorMessage = $"Meadow.Cloud service login returned {response.StatusCode}";
}

Resolver.Log.Warn(errorMessage);
ErrorOccurred?.Invoke(this, new MeadowCloudException(errorMessage));
LogAndRaiseOnErrorOccurredEvent(errorMessage);

_jwt = null;
return false;
}
catch (Exception ex)
{
errorMessage = $"Exception authenticating with Meadow.Cloud @{endpoint}: {ex.Message}";
errorMessage = $"Exception authenticating with Meadow.Cloud @{endpoint}";
if (ex.InnerException != null)
{
if (ex.InnerException is IOException && ex.InnerException.HResult == -2146232800 /*0x80131620*/)
{
errorMessage += $"{Environment.NewLine}Is your device clock correct? (UTC date is {DateTime.Now.ToShortDateString()})";
}
else
{
errorMessage += $"{Environment.NewLine}Inner exception: {ex.InnerException.Message}";
}
}
Resolver.Log.Warn(errorMessage);
ErrorOccurred?.Invoke(this, new MeadowCloudException(errorMessage));
LogAndRaiseOnErrorOccurredEvent(errorMessage, ex);

_jwt = null;
return false;
Expand All @@ -637,7 +672,7 @@ private bool SendCrashReports()
}
catch (Exception ex)
{
Resolver.Log.Warn($"Unable to send crash report: {ex.Message}");
LogAndRaiseOnErrorOccurredEvent("Unable to send crash report", ex);
result &= false;
}
}
Expand Down Expand Up @@ -719,9 +754,15 @@ private async Task<bool> Send<T>(T item, string endpoint)
if (!response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
errorMessage = $"cloud request to {endpoint} failed with {response.StatusCode}: '{responseContent}'";
Resolver.Log.Debug(errorMessage);
ErrorOccurred?.Invoke(this, new MeadowCloudException(errorMessage));
if (response.StatusCode == HttpStatusCode.InternalServerError)
{
errorMessage = $"cloud request to {endpoint} failed with {response.StatusCode}: '{responseContent}'";
}
else
{
errorMessage = $"cloud request to {endpoint} failed with {response.StatusCode}";
}
LogAndRaiseOnErrorOccurredEvent(errorMessage);
return false;
}
else
Expand All @@ -732,8 +773,7 @@ private async Task<bool> Send<T>(T item, string endpoint)
}
catch (Exception ex)
{
Resolver.Log.Debug($"exception sending cloud message: {ex.Message}");
ErrorOccurred?.Invoke(this, ex);
LogAndRaiseOnErrorOccurredEvent("Exception sending cloud message", ex);
return false;
}
finally
Expand Down
3 changes: 2 additions & 1 deletion source/Meadow.Core/Cloud/MeadowCloudException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ public class MeadowCloudException : Exception
/// Initializes a new instance of the MeadowCloudException class with a specified error message.
/// </summary>
/// <param name="message"></param>
public MeadowCloudException(string message) : base(message) { }
/// <param name="innerException"></param>
public MeadowCloudException(string message, Exception? innerException) : base(message, innerException) { }
}
2 changes: 1 addition & 1 deletion source/Meadow.Core/Cloud/MeadowCloudUpdateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ private async Task DownloadProc(UpdateMessage message)
{
using (var fileStream = Store.GetUpdateFileStream(message.ID))
{
byte[] buffer = new byte[4096 * 64]; // TODO: make this configurable/platform dependent
byte[] buffer = new byte[1024 * 64]; // TODO: make this configurable/platform dependent
int bytesRead;

while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public CharacteristicBool(string name, string uuid, CharacteristicPermission per
/// <param name="data">The data to be written.</param>
public override void HandleDataWrite(byte[] data)
{
Resolver.Log.Info($"HandleDataWrite in {this.GetType().Name}");
Resolver.Log.Debug($"HandleDataWrite in {this.GetType().Name}");
RaiseValueSet(data[0] != 0);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ public CharacteristicInt32(string name, string uuid, CharacteristicPermission pe
/// <param name="data">The data to be written.</param>
public override void HandleDataWrite(byte[] data)
{
Resolver.Log.Info($"HandleDataWrite in {this.GetType().Name}");
Resolver.Log.Debug($"HandleDataWrite in {this.GetType().Name}");

// TODO: if the written data isn't 4 bytes, then what??
// for now I'll right-pad with zeros
if (data.Length < 4)
{
Resolver.Log.Info($"HandleDataWrite only got {data.Length} bytes - padding");
Resolver.Log.Debug($"HandleDataWrite only got {data.Length} bytes - padding");
var temp = new byte[4];
Array.Copy(data, temp, data.Length);
RaiseValueSet(BitConverter.ToInt32(temp));
Expand All @@ -41,7 +41,7 @@ public override void HandleDataWrite(byte[] data)
{
if (data.Length > 4)
{
Resolver.Log.Info($"HandleDataWrite got {data.Length} bytes - using only the first 4");
Resolver.Log.Debug($"HandleDataWrite got {data.Length} bytes - using only the first 4");
}
RaiseValueSet(BitConverter.ToInt32(data));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public CharacteristicString(string name, string uuid, CharacteristicPermission p
/// <param name="data">The data to be written.</param>
public override void HandleDataWrite(byte[] data)
{
Resolver.Log.Info($"HandleDataWrite in {this.GetType().Name}");
Resolver.Log.Debug($"HandleDataWrite in {this.GetType().Name}");

RaiseValueSet(Encoding.UTF8.GetString(data));
}
Expand Down
Loading
Loading