diff --git a/src/Baballonia/Helpers/JsonExtractor.cs b/src/Baballonia/Helpers/JsonExtractor.cs index 889bbef2..fb1f42f7 100644 --- a/src/Baballonia/Helpers/JsonExtractor.cs +++ b/src/Baballonia/Helpers/JsonExtractor.cs @@ -18,12 +18,12 @@ public JsonDocument ReadUntilValidJson(Func readLineFunction, TimeSpan t if (DateTime.Now - startTime > timeout) throw new TimeoutException("Timeout reached"); - string content = _buffer.ToString(); + var content = _buffer.ToString(); - int start = -1; - int braceDepth = 0; + var start = -1; + var braceDepth = 0; - for (int i = _lastScannedIndex; i < content.Length; i++) + for (var i = _lastScannedIndex; i < content.Length; i++) { if (content[i] == '{') { @@ -34,32 +34,28 @@ public JsonDocument ReadUntilValidJson(Func readLineFunction, TimeSpan t else if (content[i] == '}') { braceDepth--; - if (braceDepth == 0 && start != -1) - { - int lenghh = i - start + 1; - string candidatestr = content.Substring(start, lenghh); + if (braceDepth != 0 || start == -1) continue; - var candidate = TryParseJson(candidatestr); - if(candidate != null) - { - _buffer.Remove(0, i + 1); - _lastScannedIndex = 0; - return candidate; - } + var lenghh = i - start + 1; + var candidatestr = content.Substring(start, lenghh); - } + var candidate = TryParseJson(candidatestr); + if (candidate == null) continue; + + _buffer.Remove(0, i + 1); + _lastScannedIndex = 0; + return candidate; } } _lastScannedIndex = Math.Max(0, content.Length - 1); // Only read if buffer was processed and still no JSON - string line = readLineFunction(); - if (!string.IsNullOrWhiteSpace(line)) - { - _buffer.Append(line); - _lastScannedIndex = 0; - } + var line = readLineFunction(); + if (string.IsNullOrWhiteSpace(line)) continue; + + _buffer.Append(line); + _lastScannedIndex = 0; } } diff --git a/src/Baballonia/Helpers/SerialCommandSender.cs b/src/Baballonia/Helpers/SerialCommandSender.cs index 997e0d4b..608fba53 100644 --- a/src/Baballonia/Helpers/SerialCommandSender.cs +++ b/src/Baballonia/Helpers/SerialCommandSender.cs @@ -5,65 +5,78 @@ using System.Threading; using Baballonia.Contracts; -namespace Baballonia.Helpers +namespace Baballonia.Helpers; + +public class SerialCommandSender : ICommandSender { - public class SerialCommandSender : ICommandSender + private const int DefaultBaudRate = 115200; // esptool-rs: Setting baud rate higher than 115,200 can cause issues + private readonly SerialPort _serialPort; + + public SerialCommandSender(string port) { - private const int DefaultBaudRate = 115200; // esptool-rs: Setting baud rate higher than 115,200 can cause issues - private readonly SerialPort _serialPort; + _serialPort = new SerialPort(port, DefaultBaudRate); + + // Set serial port parameters + _serialPort.DataBits = 8; + _serialPort.StopBits = StopBits.One; + _serialPort.Parity = Parity.None; + _serialPort.Handshake = Handshake.None; + + // Set read/write timeouts + _serialPort.ReadTimeout = 30000; + _serialPort.WriteTimeout = 30000; + _serialPort.Encoding = Encoding.UTF8; - public SerialCommandSender(string port) + int maxRetries = 5; + const int sleepTimeInMs = 50; + while (maxRetries > 0) { - _serialPort = new SerialPort(port, DefaultBaudRate); - - // Set serial port parameters - _serialPort.DataBits = 8; - _serialPort.StopBits = StopBits.One; - _serialPort.Parity = Parity.None; - _serialPort.Handshake = Handshake.None; - - // Set read/write timeouts - _serialPort.ReadTimeout = 30000; - _serialPort.WriteTimeout = 30000; - _serialPort.Encoding = Encoding.UTF8; - - int maxRetries = 5; - const int sleepTimeInMs = 50; - while (maxRetries > 0) + try { - try - { - _serialPort.Open(); - _serialPort.DiscardInBuffer(); - _serialPort.DiscardOutBuffer(); - break; - } - catch (IOException) - { - // Timeout - maxRetries = 0; - } - catch (Exception ex) + _serialPort.Open(); + _serialPort.DiscardInBuffer(); + _serialPort.DiscardOutBuffer(); + break; + } + catch (Exception ex) + { + switch (ex) { - if (ex is not FileNotFoundException && ex is not UnauthorizedAccessException) throw; - maxRetries--; - Thread.Sleep(sleepTimeInMs); + case FileNotFoundException: + case UnauthorizedAccessException: + maxRetries--; + Thread.Sleep(sleepTimeInMs); + break; + + case IOException: + case InvalidOperationException: + maxRetries = 0; + break; + + default: + throw; } } } + } - public void Dispose() + public void Dispose() + { + try { if (_serialPort.IsOpen) _serialPort.Close(); _serialPort.Dispose(); } + catch (IOException) { } + } - public string ReadLine(TimeSpan timeout) + public string ReadLine(TimeSpan timeout) + { + string data; + try { - string data; - // Read available data if (_serialPort.BytesToRead > 0) { @@ -74,25 +87,38 @@ public string ReadLine(TimeSpan timeout) { return ""; } + } + catch (Exception ex) + { + switch (ex) + { + case IOException: + case InvalidOperationException: + case OperationCanceledException: + return ""; // Port is closed - return data; + default: + throw; + } } - public void WriteLine(string payload) - { - _serialPort.DiscardInBuffer(); + return data; + } - // Convert the payload to bytes - byte[] payloadBytes = Encoding.UTF8.GetBytes(payload); + public void WriteLine(string payload) + { + _serialPort.DiscardInBuffer(); - // Write the payload to the serial port - const int chunkSize = 256; - for (int i = 0; i < payloadBytes.Length; i += chunkSize) - { - int length = Math.Min(chunkSize, payloadBytes.Length - i); - _serialPort.Write(payloadBytes, i, length); - Thread.Sleep(50); // Small pause between chunks - } + // Convert the payload to bytes + byte[] payloadBytes = Encoding.UTF8.GetBytes(payload); + + // Write the payload to the serial port + const int chunkSize = 256; + for (int i = 0; i < payloadBytes.Length; i += chunkSize) + { + int length = Math.Min(chunkSize, payloadBytes.Length - i); + _serialPort.Write(payloadBytes, i, length); + Thread.Sleep(50); // Small pause between chunks } } } diff --git a/src/Baballonia/Models/FirmwareSession.cs b/src/Baballonia/Models/FirmwareSession.cs index 9f7021f7..2d1e1712 100644 --- a/src/Baballonia/Models/FirmwareSession.cs +++ b/src/Baballonia/Models/FirmwareSession.cs @@ -75,7 +75,7 @@ private void SendCommand(string command) } } - public FirmwareResponses.Heartbeat? WaitForHeartbeat() + private FirmwareResponses.Heartbeat? WaitForHeartbeat() { return WaitForHeartbeat(new TimeSpan(5000)); } @@ -121,11 +121,21 @@ public FirmwareResponse SendCommand(IFirmwareRequest request, Time if (response == null) return FirmwareResponse.Failure("Wtf?"); - var result = JsonSerializer.Deserialize(response.results.First()); - - return FirmwareResponse.Success(JsonSerializer.Deserialize(result!.result)!); + try + { + // Attempt to extract inner content + var result = JsonSerializer.Deserialize(response.results.First()); + return FirmwareResponse.Success( + JsonSerializer.Deserialize(result!.result)!); + } + catch (JsonException) + { + // Attempt to extract outer content + return FirmwareResponse.Success( + JsonSerializer.Deserialize(response.results.First())!); + } } - catch (TimeoutException ex) + catch (TimeoutException) { return FirmwareResponse.Failure("Timeout reached"); } diff --git a/src/Baballonia/Models/FirmwareSessionFactory.cs b/src/Baballonia/Models/FirmwareSessionFactory.cs index bc1f4ee8..eff208fd 100644 --- a/src/Baballonia/Models/FirmwareSessionFactory.cs +++ b/src/Baballonia/Models/FirmwareSessionFactory.cs @@ -9,18 +9,9 @@ namespace Baballonia.Models; -public class FirmwareSessionFactory +public class FirmwareSessionFactory(ILoggerFactory loggerFactory, ICommandSenderFactory commandSenderFactory) { - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - private readonly ICommandSenderFactory _commandSenderFactory; - - public FirmwareSessionFactory(ILoggerFactory loggerFactory, ICommandSenderFactory commandSenderFactory) - { - _loggerFactory = loggerFactory; - _commandSenderFactory = commandSenderFactory; - _logger = loggerFactory.CreateLogger(); - } + private readonly ILogger _logger = loggerFactory.CreateLogger(); private string[] FindAvailableSerialPorts() { @@ -86,8 +77,8 @@ public async Task> TryOpenAllSessionsAsync() private FirmwareSessionV2? TryOpenV2Session(string port) { _logger.LogInformation($"Attempting to open V2 session for {port}"); - var sender = _commandSenderFactory.Create(CommandSenderType.Serial, port); - var sessionV2 = new FirmwareSessionV2(sender, _loggerFactory.CreateLogger()); + var sender = commandSenderFactory.Create(CommandSenderType.Serial, port); + var sessionV2 = new FirmwareSessionV2(sender, loggerFactory.CreateLogger()); var response = sessionV2.SendCommand(new FirmwareRequests.GetWhoAmIRequest(), TimeSpan.FromSeconds(3)); if (!response.IsSuccess) { @@ -111,8 +102,8 @@ public async Task> TryOpenAllSessionsAsync() private FirmwareSessionV1? TryOpenV1Session(string port) { _logger.LogInformation($"Attempting to open V1 session for {port}"); - var sender = _commandSenderFactory.Create(CommandSenderType.Serial, port); - var sessionV1 = new FirmwareSessionV1(sender, _loggerFactory.CreateLogger()); + var sender = commandSenderFactory.Create(CommandSenderType.Serial, port); + var sessionV1 = new FirmwareSessionV1(sender, loggerFactory.CreateLogger()); var heartbeat = sessionV1.WaitForHeartbeat(TimeSpan.FromSeconds(3)); if (heartbeat == null) { diff --git a/src/Baballonia/ViewModels/SplitViewPane/FirmwareViewModel.cs b/src/Baballonia/ViewModels/SplitViewPane/FirmwareViewModel.cs index 59c7d5cd..cc21f2e2 100644 --- a/src/Baballonia/ViewModels/SplitViewPane/FirmwareViewModel.cs +++ b/src/Baballonia/ViewModels/SplitViewPane/FirmwareViewModel.cs @@ -178,7 +178,8 @@ partial void OnSelectedSerialPortChanged(string? oldValue, string? newValue) [RelayCommand] private async Task SelectSerialPort() { - if (IsDeviceSelectionPresent) + // No, 'openiristracker' is not a valid COM object or device path + if (IsDeviceSelectionPresent && CanConnect(SelectedSerialPort!)) { // If we haven't already refreshed, create the new firmware session for the // Manually typed in tracker @@ -190,27 +191,51 @@ private async Task SelectSerialPort() _firmwareSessions.Add(SelectedSerialPort!, s); } - var session = _firmwareSessions[SelectedSerialPort!]; - // for legacy only - if (session.Version < new Version(0, 0, 1)) + if (_firmwareSessions.TryGetValue(SelectedSerialPort!, out var session)) { - var res = await TrySendCommandAsync(new FirmwareRequests.SetPausedRequest(true), - TimeSpan.FromSeconds(5)); - IsValidDeviceSelected = res.IsSuccess; + if (session == null) + { + IsValidDeviceSelected = false; + } + else if (session.Version < new Version(0, 0, 1)) // open v1 device. v1 devices do not report version, but they respond to commands + { + var res = await TrySendCommandAsync(new FirmwareRequests.SetPausedRequest(true), + TimeSpan.FromSeconds(5)); + IsValidDeviceSelected = res.IsSuccess; + } + else if (session.Version > new Version(0, 2, 0)) // open v2 device. v2 devices report version and respond to commands + { + IsValidDeviceSelected = true; + } + else + { + IsValidDeviceSelected = false; // wtf? + } } else - IsValidDeviceSelected = true; + { + IsValidDeviceSelected = false; // open legacy device. legacy devices do not have versions or commands + } - if (IsValidDeviceSelected) - SelectTracker = Resources.Firmware_SelectTracker_Connected; - else - SelectTracker = Resources.Firmware_SelectTracker_NoResponse; + SelectTracker = IsValidDeviceSelected ? + Resources.Firmware_SelectTracker_Connected : + Resources.Firmware_SelectTracker_NoResponse; await Task.Delay(3000); SelectTracker = Resources.Firmware_SelectTracker_Default; } } + // Plucked from SerialCameraCaptureFactory.cs + private bool CanConnect(string address) + { + var lowered = address.ToLower(); + return lowered.StartsWith("com") || + lowered.StartsWith("/dev/tty") || + lowered.StartsWith("/dev/cu") || + lowered.StartsWith("/dev/ttyacm"); + } + [RelayCommand] private async Task RefreshSerialPorts() { @@ -305,6 +330,7 @@ private async Task SendDeviceWifiCredentials() await Task.Delay(2000); WifiSetButton = Resources.Firmware_WifiSetButton_Default; + // By this point we should have a valid serial port, no need to do any error wrapping here //if (!string.IsNullOrEmpty(Mdns)) //{ // _firmwareSessions[SelectedSerialPort!].SendCommand(new FirmwareRequests.SetMdns(Mdns), TimeSpan.FromSeconds(30)); @@ -314,8 +340,10 @@ private async Task SendDeviceWifiCredentials() [RelayCommand] private async Task FlashFirmware() { - if (_firmwareSessions.TryGetValue(SelectedSerialPort!, out IFirmwareSession? value)) + if (_firmwareSessions.TryGetValue(SelectedSerialPort!, out var value)) { + if (value == null) return; + // True, this is a multimodal device that needs to be released prior to flashing await TrySendCommandAsync(new FirmwareRequests.SetPausedRequest(false), TimeSpan.FromSeconds(5)); value.Dispose(); @@ -349,6 +377,7 @@ private async Task FlashFirmware() IsFlashing = false; IsFinished = true; + // No need to check if this is a valid Babble tracker - treat it like a normal device _firmwareSessions[SelectedSerialPort!] = _firmwareService.StartSession(CommandSenderType.Serial, SelectedSerialPort!); await Task.Delay(5000); @@ -370,13 +399,17 @@ private async Task> TrySendCommandAsync(IFirmware { try { - return await _firmwareSessions[SelectedSerialPort!].SendCommandAsync(request, timeSpan); + if (_firmwareSessions.TryGetValue(SelectedSerialPort!, out var session)) + { + return await session.SendCommandAsync(request, timeSpan); + } } catch (Exception e) { _logger.LogError("Error while sending command {Exception}", e); - return await Task.FromResult(FirmwareResponse.Failure($"Error while sending command: {e}")); } + + return await Task.FromResult(FirmwareResponse.Failure("Error while sending command.")); } public void Dispose() @@ -389,7 +422,8 @@ public void Dispose() foreach (var sessions in _firmwareSessions.Values) { - sessions.SendCommand(new FirmwareRequests.SetPausedRequest(false), TimeSpan.FromSeconds(5)); + if (sessions != null) + sessions.SendCommand(new FirmwareRequests.SetPausedRequest(false), TimeSpan.FromSeconds(5)); } } }