Skip to content
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
2 changes: 1 addition & 1 deletion Daqifi.Desktop.DataModel/Daqifi.Desktop.DataModel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Daqifi.Core" Version="0.18.2" />
<PackageReference Include="Daqifi.Core" Version="0.18.3" />
<PackageReference Include="Google.Protobuf" Version="3.33.5" />
<PackageReference Include="Roslynator.Analyzers" Version="4.15.0">
<PrivateAssets>all</PrivateAssets>
Expand Down
2 changes: 1 addition & 1 deletion Daqifi.Desktop.IO/Daqifi.Desktop.IO.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="10.0.3" />
<PackageReference Include="Daqifi.Core" Version="0.18.2" />
<PackageReference Include="Daqifi.Core" Version="0.18.3" />
<PackageReference Include="Google.Protobuf" Version="3.33.5" />
<PackageReference Include="Roslynator.Analyzers" Version="4.15.0">
<PrivateAssets>all</PrivateAssets>
Expand Down
2 changes: 1 addition & 1 deletion Daqifi.Desktop/Daqifi.Desktop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Daqifi.Core" Version="0.18.2" />
<PackageReference Include="Daqifi.Core" Version="0.18.3" />
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.2" />
<PackageReference Include="MahApps.Metro" Version="2.4.11" />
<PackageReference Include="MahApps.Metro.IconPacks" Version="6.2.1" />
Expand Down
102 changes: 101 additions & 1 deletion Daqifi.Desktop/Device/SerialDevice/SerialStreamingDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,27 @@
using Daqifi.Core.Communication.Transport;
using Daqifi.Core.Communication.Messages;
using Daqifi.Core.Device.Protocol;
using Daqifi.Core.Firmware;
using Daqifi.Desktop.IO.Messages;
using ScpiMessageProducer = Daqifi.Core.Communication.Producers.ScpiMessageProducer;
using CoreStreamingDevice = Daqifi.Core.Device.DaqifiStreamingDevice;

namespace Daqifi.Desktop.Device.SerialDevice;

public class SerialStreamingDevice : AbstractStreamingDevice
public class SerialStreamingDevice : AbstractStreamingDevice, ILanChipInfoProvider
{
private static readonly TimeSpan InitialStatusTimeout = TimeSpan.FromSeconds(8);
private static readonly TimeSpan InitialStatusPollInterval = TimeSpan.FromMilliseconds(100);
private static readonly TimeSpan InitialStatusRequestInterval = TimeSpan.FromSeconds(1);
private static readonly TimeSpan DuplicateInitialStatusSuppressionWindow = TimeSpan.FromSeconds(2);

#region Properties
private SerialPort? _port;
private CoreStreamingDevice? _coreDevice;
private SerialStreamTransport? _transport;
private TaskCompletionSource<bool>? _initialStatusReceivedSource;
private DaqifiOutMessage? _lastInitialStatusMessage;
private DateTime _lastInitialStatusReceivedAtUtc;

public SerialPort? Port
{
Expand Down Expand Up @@ -246,6 +255,11 @@ public override bool Connect()

try
{
_initialStatusReceivedSource = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously);
_lastInitialStatusMessage = null;
_lastInitialStatusReceivedAtUtc = DateTime.MinValue;

// Use Core's transport for unified message handling (both send and receive)
// Note: Transport manages the actual SerialPort connection internally
_transport = new SerialStreamTransport(Port.PortName, enableDtr: true);
Expand All @@ -265,6 +279,7 @@ public override bool Connect()

// Use Core's async initialization (safe because Connect() is called from Task.Run)
_coreDevice.InitializeAsync().GetAwaiter().GetResult();
WaitForInitialStatusMessage();
return true;
}
catch (Exception ex)
Expand All @@ -280,11 +295,82 @@ public override bool Connect()
/// </summary>
private void OnCoreMessageReceived(object? sender, MessageReceivedEventArgs e)
{
if (e.Message.Data is DaqifiOutMessage protobufMessage &&
ProtobufProtocolHandler.DetectMessageType(protobufMessage) == ProtobufMessageType.Status)
{
if (ShouldSuppressDuplicateInitialStatus(protobufMessage))
{
return;
}

_initialStatusReceivedSource?.TrySetResult(true);
}

// Core's message is already an IInboundMessage<object>, wrap it for Desktop's event args
var args = new MessageEventArgs<object>(e.Message);
HandleInboundMessage(args);
}

private bool ShouldSuppressDuplicateInitialStatus(DaqifiOutMessage statusMessage)
{
var initialStatusSource = _initialStatusReceivedSource;
if (initialStatusSource == null)
{
return false;
}

var now = DateTime.UtcNow;
var shouldSuppress = initialStatusSource.Task.IsCompleted &&
_lastInitialStatusMessage != null &&
now - _lastInitialStatusReceivedAtUtc <= DuplicateInitialStatusSuppressionWindow &&
_lastInitialStatusMessage.Equals(statusMessage);

_lastInitialStatusMessage = statusMessage;
_lastInitialStatusReceivedAtUtc = now;
return shouldSuppress;
}

private void WaitForInitialStatusMessage()
{
if (_coreDevice == null)
{
throw new InvalidOperationException("Core device was not initialized.");
}

var statusReceivedSource = _initialStatusReceivedSource
?? throw new InvalidOperationException("Initial status wait source was not initialized.");

var deadline = DateTime.UtcNow + InitialStatusTimeout;
var nextDeviceInfoRequestAt = DateTime.UtcNow + InitialStatusRequestInterval;

while (DateTime.UtcNow < deadline)
{
if (statusReceivedSource.Task.Wait(InitialStatusPollInterval))
{
return;
}

if (DateTime.UtcNow < nextDeviceInfoRequestAt)
{
continue;
}

try
{
_coreDevice.Send(ScpiMessageProducer.GetDeviceInfo);
}
catch (Exception ex)
{
AppLogger.Warning($"Failed to re-request device info on {PortName}: {ex.Message}");
}

nextDeviceInfoRequestAt = DateTime.UtcNow + InitialStatusRequestInterval;
}

throw new TimeoutException(
$"Device on {PortName} did not report status within {InitialStatusTimeout.TotalSeconds:F0} seconds of connect.");
}

/// <summary>
/// Sends a message to the device using Core's DaqifiDevice.
/// </summary>
Expand Down Expand Up @@ -356,6 +442,10 @@ public override bool Disconnect()

private void CleanupConnection()
{
_initialStatusReceivedSource = null;
_lastInitialStatusMessage = null;
_lastInitialStatusReceivedAtUtc = DateTime.MinValue;

// Unsubscribe from Core device events first
if (_coreDevice != null)
{
Expand Down Expand Up @@ -433,6 +523,16 @@ public void ResetLanAfterUpdate()
_coreDevice?.Send(ScpiMessageProducer.SaveNetworkLan);
}

/// <summary>
/// Queries the WiFi module chip information by delegating to the underlying Core device.
/// </summary>
public Task<LanChipInfo?> GetLanChipInfoAsync(CancellationToken cancellationToken = default)
{
if (_coreDevice is not ILanChipInfoProvider provider)
return Task.FromResult<LanChipInfo?>(null);
return provider.GetLanChipInfoAsync(cancellationToken);
}

/// <summary>
/// Returns the COM port name for this USB device
/// </summary>
Expand Down
103 changes: 101 additions & 2 deletions Daqifi.Desktop/ViewModels/DaqifiViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ namespace Daqifi.Desktop.ViewModels;

public partial class DaqifiViewModel : ObservableObject
{
private const int WifiChipInfoMaxAttempts = 3;
private static readonly TimeSpan WifiChipInfoRetryDelay = TimeSpan.FromSeconds(2);

private readonly AppLogger _appLogger = AppLogger.Instance;

#region Private Variables
Expand Down Expand Up @@ -537,7 +540,7 @@ await _firmwareUpdateService.UpdateFirmwareAsync(

if (!isManualUpload)
{
await UpdateWifiModuleAsync(coreDevice, _firmwareUploadCts.Token);
await UpdateWifiModuleAsync(coreDevice, serialStreamingDevice, _firmwareUploadCts.Token);
}

IsUploadComplete = true;
Expand Down Expand Up @@ -585,8 +588,57 @@ private bool CanCancelFirmwareUpload()
return IsFirmwareUploading;
}

private async Task UpdateWifiModuleAsync(Daqifi.Core.Device.IStreamingDevice coreDevice, CancellationToken cancellationToken)
private async Task UpdateWifiModuleAsync(
Daqifi.Core.Device.IStreamingDevice coreDevice,
SerialStreamingDevice serialStreamingDevice,
CancellationToken cancellationToken)
{
// Core's WiFi updater also performs its own version probe when the passed device
// implements ILanChipInfoProvider. Keep the explicit desktop-side check here, but
// run the actual WiFi update through the same non-provider adapter used on main.
// Check the device's current WiFi version before downloading to avoid unnecessary flashing.
if (serialStreamingDevice is ILanChipInfoProvider lanChipProvider)
{
FirmwareUpdateStatusText = "Checking WiFi firmware version...";
_appLogger.Information("Checking WiFi firmware version before deciding whether to flash the WiFi module.");

var chipInfo = await TryGetLanChipInfoAsync(lanChipProvider, cancellationToken);

if (chipInfo == null)
{
_appLogger.Warning("WiFi chip info unavailable after startup retries; continuing with WiFi update.");
FirmwareUpdateStatusText = "WiFi firmware version unavailable; continuing with update.";
}
else
{
_appLogger.Information(
$"WiFi chip info query succeeded. Device WiFi firmware version: {chipInfo.FwVersion}.");

var latestRelease = await _firmwareDownloadService.GetLatestWifiReleaseAsync(cancellationToken);

if (latestRelease != null)
{
var latestVersion = NormalizeWifiFirmwareVersion(latestRelease.TagName);
if (IsWifiVersionCurrent(chipInfo.FwVersion, latestVersion))
{
FirmwareUpdateStatusText = $"WiFi firmware already up to date ({chipInfo.FwVersion}).";
_appLogger.Information(
$"WiFi firmware is already up to date (device: {chipInfo.FwVersion}, latest: {latestVersion}); skipping WiFi flash.");
UploadWiFiProgress = 100;
return;
}

FirmwareUpdateStatusText = $"WiFi update available ({chipInfo.FwVersion} → {latestVersion}). Downloading...";
_appLogger.Information(
$"WiFi firmware update required (device: {chipInfo.FwVersion}, latest: {latestVersion}); proceeding with WiFi flash.");
}
else
{
_appLogger.Warning("Latest WiFi firmware release metadata was unavailable; continuing with WiFi update.");
}
}
}

FirmwareUpdateStatusText = "Downloading WiFi firmware package...";
var wifiDownloadProgress = new Progress<int>(percent =>
{
Expand Down Expand Up @@ -623,6 +675,46 @@ await wifiUpdateService.UpdateWifiModuleAsync(
cancellationToken);
}

private async Task<LanChipInfo?> TryGetLanChipInfoAsync(
ILanChipInfoProvider lanChipProvider,
CancellationToken cancellationToken)
{
for (var attempt = 1; attempt <= WifiChipInfoMaxAttempts; attempt++)
{
cancellationToken.ThrowIfCancellationRequested();

try
{
var chipInfo = await lanChipProvider.GetLanChipInfoAsync(cancellationToken);
if (chipInfo != null)
{
return chipInfo;
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_appLogger.Warning(
$"WiFi chip info query attempt {attempt}/{WifiChipInfoMaxAttempts} failed: {ex.Message}");
}

if (attempt >= WifiChipInfoMaxAttempts)
{
break;
}

_appLogger.Information(
$"WiFi chip info unavailable on attempt {attempt}/{WifiChipInfoMaxAttempts}; retrying after startup delay.");
FirmwareUpdateStatusText = "Waiting for device to finish starting up before checking WiFi firmware version...";
await Task.Delay(WifiChipInfoRetryDelay, cancellationToken);
}

return null;
}

private FirmwareUpdateService CreateWifiFirmwareUpdateService(string wifiVersion, string portName)
{
var firmwareLogger = App.ServiceProvider?.GetService<ILogger<FirmwareUpdateService>>()
Expand Down Expand Up @@ -710,6 +802,13 @@ private static string NormalizeWifiFirmwareVersion(string rawVersion)
return normalized;
}

private static bool IsWifiVersionCurrent(string deviceVersion, string latestVersion)
{
if (!FirmwareVersion.TryParse(deviceVersion, out var device)) return false;
if (!FirmwareVersion.TryParse(latestVersion, out var latest)) return false;
return device >= latest;
}

private void HandleFirmwareUpdateException(FirmwareUpdateException exception)
{
HasErrorOccured = true;
Expand Down
Loading