common/Hardware/WaterMeter/Genesis/GenesisCore/MeterPortScanner.cs
2026-04-23 17:50:07 +02:00

239 lines
9.9 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Linq;
using Newtonsoft.Json;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.Properties;
using Xylem.Common.Hardware.WaterMeter.WaterMeterCore;
using Xylem.Common.Utils.ProcessExec;
using Xylem.Common.Utils.ProcessExec.EventArguments;
namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
{
/// <summary>
/// Port scanner:
/// using the Windows Device Manager listed ports,
/// tries to open the port with an exception if it cannot be accessed (very time-consuming),
/// tries to use the request protocol to detect a Genesis device.
/// </summary>
public class MeterPortScanner : IProcessState, IDisposable
{
/// <summary>
/// Genesis for test access
/// </summary>
private GenesisMeter _currentGenesis;
private MeterBatch _meterBatch;
private readonly String _portType;
/// <summary>
/// Auto-detected port name
/// </summary>
public String AutoDetectedPortName;
/// <summary>
/// Port scan result event for message dispatcher to caller
/// </summary>
public event EventHandler<ProcessExecEventArgs> OnProcessUpdate;
/// <summary>
/// Stop the port scan
/// </summary>
public Boolean StopPortScan;
/// <summary>
/// Number of ports
/// </summary>
public Int32 NumberOfPorts;
/// <summary>
/// Actual Port Counter
/// </summary>
public Int32 ActualPortCtr;
/// <summary>
/// Returns the port scan state
/// </summary>
/// <returns></returns>
/// <remarks date="2020-Oct-21" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
public String GetPortScanState()
{
return $@"{Resources.StrScanPort} {ActualPortCtr}/{NumberOfPorts}";
}
/// <summary>
/// Ctor
/// </summary>
/// <returns>true if one port has been validated</returns>
/// <remarks date="2020-Dec-11" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <param name="portType">type of communication port to meter like IrDA</param>
public MeterPortScanner(String portType)
{
_portType = portType;
}
/// <inheritdoc />
/// <remarks date="2021-Jan-05" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
/// <remarks date="2021-Mai-11" author="Thomas Wiedebusch">
/// - Remove all meters removed as _meterBatch.Dispose will remove all meters.
/// </remarks>
public void Dispose()
{
// removal from meter batch includes a disposal of meter
_meterBatch?.Dispose();
}
/// <summary>
/// Use initially the port configuration to speed up search,
/// If configuration file contains wrong port setup than scan all serial ports listed in
/// the windows device manager,
/// Read available ports,
/// Try to open port and connect to Genesis meter.
/// </summary>
/// <param name="slot">slot to search for</param>
/// <param name="portConfigFilePathName">configuration of port to speed up search</param>
/// <returns>true if port found</returns>
/// <returns>true if one port has been validated</returns>
/// <remarks date="2020-Oct-14" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2020-Dec-11" author="Thomas Wiedebusch">
/// - Port type from Ctor,
/// - Event message changed:
/// - Overall message: ctr / counts - ongoing scan,
/// - Actual message: Information to log
/// </remarks>
/// <remarks date="2021-Jan-05" author="Thomas Wiedebusch">
/// - Changed exit of function,
/// - Added configuration file handling.
/// </remarks>
/// <remarks date="2021-Mai-11" author="Thomas Wiedebusch">
/// - Remove all meters removed as _meterBatch.Dispose will remove all meters.
/// </remarks>
public Boolean ScanAllSerialPorts(Int32 slot, String portConfigFilePathName)
{
var serialPort = new SerialPort();
var serialPorts = new List<String>();
_meterBatch = new MeterBatch();
serialPorts.AddRange(SerialPort.GetPortNames());
NumberOfPorts = serialPorts.Count;
ActualPortCtr = 0;
// If configuration file exists, read it and check for slot setup. This will speed up the recurrent detection
// process as at the end the detection process the detected slot will be saved.
var slotPortConfigs = new List<SlotConfig>();
if (portConfigFilePathName != null && File.Exists(portConfigFilePathName))
{
using (var tr = new StreamReader(portConfigFilePathName))
{
var fileStream = tr.ReadToEnd();
var slots = JsonConvert.DeserializeObject<SlotConfig[]>(fileStream);
if (slots != null && slots.Length > 0)
slotPortConfigs.AddRange(slots);
}
foreach (var slotPortConfig in slotPortConfigs.Where(slotPortConfig =>
slotPortConfig.Slot == slot && !string.IsNullOrEmpty(slotPortConfig.Request.PortName)))
{
// add port on bottom of port list even if it is doubled that way
NumberOfPorts++;
serialPorts.Add(slotPortConfig.Request.PortName);
break;
}
}
OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs("", 0,
$@"{NumberOfPorts} {Resources.StrPortListDeviceManger}"));
var serialPortDetected = false;
StopPortScan = false;
// start with last port,this might be added at current connection of interface
for (var ctr = serialPorts.Count - 1; ctr >= 0; ctr--)
{
if (StopPortScan) break;
ActualPortCtr++;
serialPort.PortName = serialPorts[ctr];
var overallProcessCtrPercent = 100.0 * ActualPortCtr / (NumberOfPorts > 0 ? NumberOfPorts : 1);
OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs(GetPortScanState(), overallProcessCtrPercent,
$@"{serialPort.PortName}: {Resources.StrPortAccessRequest}"));
// skip all ports which are already opened, this might be another customer
if (serialPort.IsOpen) continue;
PortConfig portConfig;
try
{
// removal from meter batch includes a disposal of meter
_meterBatch.RemoveAllMeters();
portConfig = new PortConfig
{
PortName = serialPort.PortName,
Type = _portType
};
_currentGenesis = new GenesisMeter(slot, portConfig, null);
if (_currentGenesis == null) return false;
// configuration has to be set BEFORE adding meter to batch to avoid e.g. auto update files
// from network and therefore have a long network request timeout before the task starts
_currentGenesis.Configuration.UseRegisterWatchService = false;
_currentGenesis.Configuration.UseMinMaxCheck = false;
_currentGenesis.Configuration.AutoUpdateFiles = false;
_meterBatch.AddMeter(_currentGenesis);
}
catch (Exception)
{
OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs(GetPortScanState(), overallProcessCtrPercent,
$@"{serialPort.PortName}: {Resources.StrPortFailedToOpen}"));
continue;
}
// if port cannot be opened on access to the port is senseless, try the next port
if (_currentGenesis != null && !_currentGenesis.RequestPort.IsOpen()) continue;
OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs(GetPortScanState(), overallProcessCtrPercent,
$@"{serialPort.PortName}: {Resources.StrPortSuccessfullyAccessed}"));
// read PCB ID from meter, this is the indicator, that a Genesis is connected to this port
if (_currentGenesis == null || string.Empty == _currentGenesis.GetPcbId())
{
OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs(GetPortScanState(), overallProcessCtrPercent,
$@"{serialPort.PortName}: {Resources.StrRequestPortDetectionFailed}"));
continue;
}
AutoDetectedPortName = serialPort.PortName;
serialPortDetected = true;
// check if needed to store new configuration if slot and port name is not in file
if (!slotPortConfigs.Any(c => c.Slot == slot && c.Request.PortName == AutoDetectedPortName))
{
slotPortConfigs.Add(new SlotConfig() { Slot = slot, Request = portConfig });
var text = JsonConvert.SerializeObject(slotPortConfigs);
if (portConfigFilePathName != null)
File.WriteAllText(portConfigFilePathName, text);
}
// stop loop if one Genesis has been detected
break;
}
serialPort.Dispose();
// removal from meter batch includes a disposal of meter
_meterBatch?.Dispose();
return serialPortDetected;
}
}
}