APM provides industrial-grade serial scale integration for capturing weight data from USB and RS-232 connected scales. The system features automatic port detection, robust error recovery, and real-time data streaming to web clients.
public class Scale{ public string Id { get; set; } // Unique identifier public string Name { get; set; } // Display name public string PortName { get; set; } // e.g., "COM3" public int BaudRate { get; set; } = 9600; // Serial speed public int DataBits { get; set; } = 8; public ScaleParity Parity { get; set; } // None, Even, Odd public ScaleStopBits StopBits { get; set; } // One, Two public bool IsActive { get; set; } = true;}
public interface IScaleService{ // Real-time weight change events event EventHandler<ScaleDataEventArgs> OnWeightChanged; // Initialize scale monitoring Task InitializeAsync(); // Get connection status for a specific scale ScaleStatus GetStatus(string scaleId); // Get status of all configured scales List<ScaleStatusInfo> GetAllStatuses(); // Enable data transmission to clients void StartListening(string scaleId); void StopListening(string scaleId); // Reload scale configurations from storage Task ReloadScalesAsync();}
// 1. Intentar puerto configurado (Solo si existe en SO)bool found = false;if (osPorts.Any(p => p.Equals(scale.PortName, StringComparison.OrdinalIgnoreCase))){ if (TryOpenPort(scale, scale.PortName)) found = true;}// 2. Si falló, buscar en otros puertos libresif (!found){ var usedPorts = _activePorts.Values.Where(p => p.IsOpen).Select(p => p.PortName).ToList(); var candidatePorts = osPorts.Except(usedPorts, StringComparer.OrdinalIgnoreCase); foreach (var candPort in candidatePorts) { if (TryOpenPort(scale, candPort)) { _logger.LogInfo($"Auto-Detect: Báscula {scale.Id} encontrada en {candPort}"); // PERSISTENCIA AUTOMÁTICA: Guardar el nuevo puerto en configuración if (!string.Equals(scale.PortName, candPort, StringComparison.OrdinalIgnoreCase)) { try { scale.PortName = candPort; await _scaleRepository.UpdateAsync(scale); _logger.LogInfo($"Configuración actualizada: Báscula {scale.Id} movida a {candPort}"); await RefreshCacheAsync(); } catch (Exception saveEx) { _logger.LogError($"Error guardando nuevo puerto {candPort}: {saveEx.Message}"); } } break; } }}
When a scale is plugged into a different USB port, APM automatically detects the new COM port and updates the configuration.
// This method is called by the OS automatically when bytes arrive at the serial portprivate void Port_DataReceived(object sender, SerialDataReceivedEventArgs e, string scaleId){ try { SerialPort sp = (SerialPort)sender; // Safety check: verify port is still open if (!sp.IsOpen) return; // Performance optimization: only process if someone is listening if (!_listenersCount.TryGetValue(scaleId, out int count) || count <= 0) { // Clear buffer to prevent overflow if (sp.IsOpen) sp.ReadExisting(); return; } // Read one complete line from the scale (until \n) string line = sp.ReadLine(); if (string.IsNullOrWhiteSpace(line)) return; _logger.LogInfo($"Data recibida ({scaleId}): {line}"); // Extract weight using regex: (\d+[\.,]?\d*) // Matches patterns like: "15.00", "15,00", "15" var match = Regex.Match(line, @"(\d+[\.,]?\d*)"); if (match.Success) { // Normalize: replace comma with period for C# decimal parsing string weightStr = match.Groups[1].Value.Replace(',', '.'); if (decimal.TryParse(weightStr, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out decimal weight)) { var data = new ScaleData { ScaleId = scaleId, StationId = "Local", Weight = weight, Unit = "kg", Stable = true, Timestamp = DateTime.Now, Type = "SCALE_READING" }; // Broadcast weight change event OnWeightChanged?.Invoke(this, new ScaleDataEventArgs(scaleId, data)); } } } catch (IOException ex) { _logger.LogError($"Error procesando datos serial ({scaleId}): {ex.Message}"); } catch (UnauthorizedAccessException ex) { _logger.LogError($"Puerto bloqueado ({scaleId}): {ex.Message}"); } catch (TimeoutException ex) { _logger.LogError($"Timeout de lectura ({scaleId}): {ex.Message}"); }}
private void SafeClose(SerialPort? port){ if (port == null) return; // Run Close on separate thread to prevent service freeze if driver fails Task.Run(() => { try { if (port.IsOpen) { try { port.DtrEnable = false; port.RtsEnable = false; } catch { } try { port.DiscardInBuffer(); port.DiscardOutBuffer(); } catch { } port.Close(); } } catch (Exception ex) { _logger.LogError($"{ex.Message}"); // Ignore errors when closing (dead port or hung driver) } finally { try { port.Dispose(); } catch { } } });}
USB-Serial drivers (CH340, PL2303) can hang when closing ports after physical disconnection. Running cleanup on a background thread prevents application freeze.