diff --git a/Daqifi.Desktop.DataModel/Daqifi.Desktop.DataModel.csproj b/Daqifi.Desktop.DataModel/Daqifi.Desktop.DataModel.csproj index a699d2b..ccc4ab3 100644 --- a/Daqifi.Desktop.DataModel/Daqifi.Desktop.DataModel.csproj +++ b/Daqifi.Desktop.DataModel/Daqifi.Desktop.DataModel.csproj @@ -11,7 +11,7 @@ - + all diff --git a/Daqifi.Desktop.IO/Daqifi.Desktop.IO.csproj b/Daqifi.Desktop.IO/Daqifi.Desktop.IO.csproj index f1f2abd..a8ca1c3 100644 --- a/Daqifi.Desktop.IO/Daqifi.Desktop.IO.csproj +++ b/Daqifi.Desktop.IO/Daqifi.Desktop.IO.csproj @@ -10,7 +10,7 @@ - + all diff --git a/Daqifi.Desktop/Daqifi.Desktop.csproj b/Daqifi.Desktop/Daqifi.Desktop.csproj index 6e3911d..6a6d329 100644 --- a/Daqifi.Desktop/Daqifi.Desktop.csproj +++ b/Daqifi.Desktop/Daqifi.Desktop.csproj @@ -57,7 +57,7 @@ - + diff --git a/Daqifi.Desktop/Device/SerialDevice/SerialStreamingDevice.cs b/Daqifi.Desktop/Device/SerialDevice/SerialStreamingDevice.cs index ca45db6..89d05d0 100644 --- a/Daqifi.Desktop/Device/SerialDevice/SerialStreamingDevice.cs +++ b/Daqifi.Desktop/Device/SerialDevice/SerialStreamingDevice.cs @@ -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? _initialStatusReceivedSource; + private DaqifiOutMessage? _lastInitialStatusMessage; + private DateTime _lastInitialStatusReceivedAtUtc; public SerialPort? Port { @@ -246,6 +255,11 @@ public override bool Connect() try { + _initialStatusReceivedSource = new TaskCompletionSource( + 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); @@ -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) @@ -280,11 +295,82 @@ public override bool Connect() /// 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, wrap it for Desktop's event args var args = new MessageEventArgs(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."); + } + /// /// Sends a message to the device using Core's DaqifiDevice. /// @@ -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) { @@ -433,6 +523,16 @@ public void ResetLanAfterUpdate() _coreDevice?.Send(ScpiMessageProducer.SaveNetworkLan); } + /// + /// Queries the WiFi module chip information by delegating to the underlying Core device. + /// + public Task GetLanChipInfoAsync(CancellationToken cancellationToken = default) + { + if (_coreDevice is not ILanChipInfoProvider provider) + return Task.FromResult(null); + return provider.GetLanChipInfoAsync(cancellationToken); + } + /// /// Returns the COM port name for this USB device /// diff --git a/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs b/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs index 6bfd0a3..94e40fe 100644 --- a/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs +++ b/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs @@ -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 @@ -537,7 +540,7 @@ await _firmwareUpdateService.UpdateFirmwareAsync( if (!isManualUpload) { - await UpdateWifiModuleAsync(coreDevice, _firmwareUploadCts.Token); + await UpdateWifiModuleAsync(coreDevice, serialStreamingDevice, _firmwareUploadCts.Token); } IsUploadComplete = true; @@ -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(percent => { @@ -623,6 +675,46 @@ await wifiUpdateService.UpdateWifiModuleAsync( cancellationToken); } + private async Task 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>() @@ -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;