951 lines
33 KiB
C#
951 lines
33 KiB
C#
|
|
using MainShell.EventArgsFolder;
|
||
|
|
using Stylet;
|
||
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.IO;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Text;
|
||
|
|
using System.Threading;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
|
||
|
|
namespace MainShell.Hardware
|
||
|
|
{
|
||
|
|
public interface IDeviceIoMonitorService
|
||
|
|
{
|
||
|
|
event EventHandler<DeviceIoChangedEventArgs> IoChanged;
|
||
|
|
IReadOnlyList<DeviceIoPointDefinition> Definitions { get; }
|
||
|
|
IReadOnlyDictionary<string, List<DeviceIoPointDefinition>> DefinitionsByModule { get; }
|
||
|
|
IReadOnlyDictionary<string, DeviceIoPointState> LatestPoints { get; }
|
||
|
|
bool IsOnline { get; }
|
||
|
|
bool TryGetPoint(int id, out DeviceIoPointState point);
|
||
|
|
bool TryGetPoint(string name, out DeviceIoPointState point);
|
||
|
|
bool TryGetPointByPointKey(string pointKey, out DeviceIoPointState point);
|
||
|
|
bool IsPointOn(int id);
|
||
|
|
bool IsPointOn(string name);
|
||
|
|
bool IsPointOnByPointKey(string pointKey);
|
||
|
|
Task<bool> WaitForPointStateAsync(int id, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken));
|
||
|
|
Task<bool> WaitForPointStateAsync(string name, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken));
|
||
|
|
Task<bool> WaitForPointStateByPointKeyAsync(string pointKey, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken));
|
||
|
|
IReadOnlyList<DeviceIoPointState> GetPointsByModule(string module);
|
||
|
|
void Start();
|
||
|
|
void Stop();
|
||
|
|
void RequestRefresh();
|
||
|
|
bool TryRefreshNow(TimeSpan timeout);
|
||
|
|
Task<bool> TryRefreshNowAsync(TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken));
|
||
|
|
bool TrySetOutputState(int id, bool outputOn);
|
||
|
|
bool TrySetOutputState(string name, bool outputOn);
|
||
|
|
bool TrySetOutputStateByPointKey(string pointKey, bool outputOn);
|
||
|
|
bool TrySetOutputStates(IReadOnlyList<DeviceIoOutputWriteRequest> requests, out string failurePointReference);
|
||
|
|
bool TrySetOutputStatesByPointKey(IReadOnlyDictionary<string, bool> outputStates, out string failurePointKey);
|
||
|
|
}
|
||
|
|
|
||
|
|
public class DeviceIoMonitorService : IDeviceIoMonitorService
|
||
|
|
{
|
||
|
|
private readonly HardwareManager _hardwareManager;
|
||
|
|
private readonly AutoResetEvent _refreshSignal = new AutoResetEvent(false);
|
||
|
|
private readonly TimeSpan _pollInterval = TimeSpan.FromMilliseconds(100);
|
||
|
|
private readonly object _syncRoot = new object();
|
||
|
|
private readonly List<DeviceIoPointDefinition> _definitions = new List<DeviceIoPointDefinition>();
|
||
|
|
private readonly Dictionary<string, List<DeviceIoPointDefinition>> _definitionsByModule = new Dictionary<string, List<DeviceIoPointDefinition>>(StringComparer.OrdinalIgnoreCase);
|
||
|
|
private readonly Dictionary<string, DeviceIoPointDefinition> _definitionsByName = new Dictionary<string, DeviceIoPointDefinition>(StringComparer.OrdinalIgnoreCase);
|
||
|
|
private readonly Dictionary<string, DeviceIoPointDefinition> _definitionsByPointKey = new Dictionary<string, DeviceIoPointDefinition>(StringComparer.OrdinalIgnoreCase);
|
||
|
|
private readonly object _refreshWaitersSyncRoot = new object();
|
||
|
|
private readonly List<TaskCompletionSource<bool>> _refreshWaiters = new List<TaskCompletionSource<bool>>();
|
||
|
|
private const string DeviceIoConfigFileName = "DeviceIoPoints.csv";
|
||
|
|
|
||
|
|
private readonly Dictionary<string, DeviceIoPointState> _latestPoints = new Dictionary<string, DeviceIoPointState>(StringComparer.OrdinalIgnoreCase);
|
||
|
|
private CancellationTokenSource _cts;
|
||
|
|
|
||
|
|
public event EventHandler<DeviceIoChangedEventArgs> IoChanged;
|
||
|
|
|
||
|
|
public IReadOnlyList<DeviceIoPointDefinition> Definitions => _definitions;
|
||
|
|
public IReadOnlyDictionary<string, List<DeviceIoPointDefinition>> DefinitionsByModule => _definitionsByModule;
|
||
|
|
public IReadOnlyDictionary<string, DeviceIoPointState> LatestPoints => GetLatestPointsSnapshot();
|
||
|
|
public bool IsOnline => _hardwareManager.TdCard != null || _hardwareManager.AcsCard != null;
|
||
|
|
|
||
|
|
|
||
|
|
public DeviceIoMonitorService(HardwareManager hardwareManager)
|
||
|
|
{
|
||
|
|
_hardwareManager = hardwareManager;
|
||
|
|
LoadIoPointDefinitions();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Start()
|
||
|
|
{
|
||
|
|
if (_cts != null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
LoadIoPointDefinitions();
|
||
|
|
|
||
|
|
_cts = new CancellationTokenSource();
|
||
|
|
var token = _cts.Token;
|
||
|
|
|
||
|
|
Task.Run(async () =>
|
||
|
|
{
|
||
|
|
while (!token.IsCancellationRequested)
|
||
|
|
{
|
||
|
|
_refreshSignal.WaitOne(_pollInterval);
|
||
|
|
if (token.IsCancellationRequested)
|
||
|
|
{
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
IList<DeviceIoPointState> readPoints;
|
||
|
|
if (!TryReadIoPoints(out readPoints) || readPoints == null || readPoints.Count == 0)
|
||
|
|
{
|
||
|
|
CompleteRefreshWaiters(false);
|
||
|
|
await Task.Yield();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var changed = CollectChangedPoints(readPoints);
|
||
|
|
CompleteRefreshWaiters(true);
|
||
|
|
if (changed.Count == 0)
|
||
|
|
{
|
||
|
|
await Task.Yield();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var handler = IoChanged;
|
||
|
|
if (handler != null)
|
||
|
|
{
|
||
|
|
var changedCopy = changed.Select(x => x.Clone()).ToList();
|
||
|
|
var allCopy = GetLatestPointsSnapshot();
|
||
|
|
Execute.OnUIThread(() => handler(this, new DeviceIoChangedEventArgs(changedCopy, allCopy)));
|
||
|
|
}
|
||
|
|
|
||
|
|
await Task.Yield();
|
||
|
|
}
|
||
|
|
}, token);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Stop()
|
||
|
|
{
|
||
|
|
if (_cts == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
_cts.Cancel();
|
||
|
|
_refreshSignal.Set();
|
||
|
|
CompleteRefreshWaiters(false);
|
||
|
|
_cts.Dispose();
|
||
|
|
_cts = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void RequestRefresh()
|
||
|
|
{
|
||
|
|
_refreshSignal.Set();
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool TryRefreshNow(TimeSpan timeout)
|
||
|
|
{
|
||
|
|
return TryRefreshNowAsync(timeout).GetAwaiter().GetResult();
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<bool> TryRefreshNowAsync(TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken))
|
||
|
|
{
|
||
|
|
Start();
|
||
|
|
|
||
|
|
var waiter = new TaskCompletionSource<bool>();
|
||
|
|
CancellationTokenRegistration registration = default(CancellationTokenRegistration);
|
||
|
|
lock (_refreshWaitersSyncRoot)
|
||
|
|
{
|
||
|
|
_refreshWaiters.Add(waiter);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cancellationToken.CanBeCanceled)
|
||
|
|
{
|
||
|
|
registration = cancellationToken.Register(() => waiter.TrySetCanceled());
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
RequestRefresh();
|
||
|
|
|
||
|
|
if (timeout <= TimeSpan.Zero)
|
||
|
|
{
|
||
|
|
timeout = _pollInterval;
|
||
|
|
}
|
||
|
|
|
||
|
|
var completedTask = await Task.WhenAny(waiter.Task, Task.Delay(timeout, cancellationToken));
|
||
|
|
if (completedTask != waiter.Task)
|
||
|
|
{
|
||
|
|
RemoveRefreshWaiter(waiter);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return await waiter.Task;
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
registration.Dispose();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool TrySetOutputState(int id, bool outputOn)
|
||
|
|
{
|
||
|
|
var definition = ResolveUniqueDefinitionById(id, IoPointType.Output);
|
||
|
|
if (definition == null)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
var result = _hardwareManager.TryWriteIoPointValue(definition, outputOn);
|
||
|
|
if (result)
|
||
|
|
{
|
||
|
|
RequestRefresh();
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool TrySetOutputStateByPointKey(string pointKey, bool outputOn)
|
||
|
|
{
|
||
|
|
DeviceIoPointDefinition definition;
|
||
|
|
if (!_definitionsByPointKey.TryGetValue(pointKey ?? string.Empty, out definition) || definition.Type != IoPointType.Output || !definition.Enabled)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
var result = _hardwareManager.TryWriteIoPointValue(definition, outputOn);
|
||
|
|
if (result)
|
||
|
|
{
|
||
|
|
RequestRefresh();
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool TrySetOutputStates(IReadOnlyList<DeviceIoOutputWriteRequest> requests, out string failurePointReference)
|
||
|
|
{
|
||
|
|
failurePointReference = string.Empty;
|
||
|
|
if (requests == null || requests.Count == 0)
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
var definitions = new List<Tuple<DeviceIoPointDefinition, bool, string>>();
|
||
|
|
foreach (var request in requests)
|
||
|
|
{
|
||
|
|
if (request == null || string.IsNullOrWhiteSpace(request.PointReference))
|
||
|
|
{
|
||
|
|
failurePointReference = string.Empty;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
DeviceIoPointDefinition definition;
|
||
|
|
if (!TryResolveOutputDefinition(request.PointReference, out definition))
|
||
|
|
{
|
||
|
|
failurePointReference = request.PointReference;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
definitions.Add(Tuple.Create(definition, request.OutputOn, request.PointReference));
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var item in definitions)
|
||
|
|
{
|
||
|
|
if (!_hardwareManager.TryWriteIoPointValue(item.Item1, item.Item2))
|
||
|
|
{
|
||
|
|
failurePointReference = item.Item3;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
RequestRefresh();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool TrySetOutputStatesByPointKey(IReadOnlyDictionary<string, bool> outputStates, out string failurePointKey)
|
||
|
|
{
|
||
|
|
failurePointKey = string.Empty;
|
||
|
|
if (outputStates == null || outputStates.Count == 0)
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
var definitions = new List<Tuple<DeviceIoPointDefinition, bool, string>>();
|
||
|
|
foreach (var item in outputStates)
|
||
|
|
{
|
||
|
|
DeviceIoPointDefinition definition;
|
||
|
|
if (!_definitionsByPointKey.TryGetValue(item.Key ?? string.Empty, out definition) || definition.Type != IoPointType.Output || !definition.Enabled)
|
||
|
|
{
|
||
|
|
failurePointKey = item.Key;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
definitions.Add(Tuple.Create(definition, item.Value, item.Key));
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var item in definitions)
|
||
|
|
{
|
||
|
|
if (!_hardwareManager.TryWriteIoPointValue(item.Item1, item.Item2))
|
||
|
|
{
|
||
|
|
failurePointKey = item.Item3;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
RequestRefresh();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool TrySetOutputState(string name, bool outputOn)
|
||
|
|
{
|
||
|
|
DeviceIoPointDefinition definition;
|
||
|
|
if (!_definitionsByName.TryGetValue(name ?? string.Empty, out definition) || definition.Type != IoPointType.Output || !definition.Enabled)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
var result = _hardwareManager.TryWriteIoPointValue(definition, outputOn);
|
||
|
|
if (result)
|
||
|
|
{
|
||
|
|
RequestRefresh();
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool TryGetPoint(int id, out DeviceIoPointState point)
|
||
|
|
{
|
||
|
|
var definition = ResolveUniqueDefinitionById(id, null);
|
||
|
|
if (definition == null)
|
||
|
|
{
|
||
|
|
point = null;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
lock (_syncRoot)
|
||
|
|
{
|
||
|
|
if (_latestPoints.TryGetValue(definition.PointKey, out point))
|
||
|
|
{
|
||
|
|
point = point.Clone();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
point = null;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool TryGetPointByPointKey(string pointKey, out DeviceIoPointState point)
|
||
|
|
{
|
||
|
|
lock (_syncRoot)
|
||
|
|
{
|
||
|
|
if (_latestPoints.TryGetValue(pointKey ?? string.Empty, out point))
|
||
|
|
{
|
||
|
|
point = point.Clone();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
point = null;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool IsPointOn(int id)
|
||
|
|
{
|
||
|
|
DeviceIoPointState point;
|
||
|
|
return TryGetPoint(id, out point) && point.Value;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool IsPointOn(string name)
|
||
|
|
{
|
||
|
|
DeviceIoPointState point;
|
||
|
|
return TryGetPoint(name, out point) && point.Value;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool IsPointOnByPointKey(string pointKey)
|
||
|
|
{
|
||
|
|
DeviceIoPointState point;
|
||
|
|
return TryGetPointByPointKey(pointKey, out point) && point.Value;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<bool> WaitForPointStateAsync(int id, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken))
|
||
|
|
{
|
||
|
|
var start = DateTime.UtcNow;
|
||
|
|
while (DateTime.UtcNow - start < timeout)
|
||
|
|
{
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
|
||
|
|
var remaining = timeout - (DateTime.UtcNow - start);
|
||
|
|
var refreshTimeout = remaining < _pollInterval ? remaining : _pollInterval;
|
||
|
|
if (refreshTimeout > TimeSpan.Zero)
|
||
|
|
{
|
||
|
|
await TryRefreshNowAsync(refreshTimeout, cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (IsPointOn(id) == expectedState)
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
await Task.Delay(50, cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<bool> WaitForPointStateByPointKeyAsync(string pointKey, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken))
|
||
|
|
{
|
||
|
|
var start = DateTime.UtcNow;
|
||
|
|
while (DateTime.UtcNow - start < timeout)
|
||
|
|
{
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
|
||
|
|
var remaining = timeout - (DateTime.UtcNow - start);
|
||
|
|
var refreshTimeout = remaining < _pollInterval ? remaining : _pollInterval;
|
||
|
|
if (refreshTimeout > TimeSpan.Zero)
|
||
|
|
{
|
||
|
|
await TryRefreshNowAsync(refreshTimeout, cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (IsPointOnByPointKey(pointKey) == expectedState)
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
await Task.Delay(50, cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<bool> WaitForPointStateAsync(string name, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken))
|
||
|
|
{
|
||
|
|
var start = DateTime.UtcNow;
|
||
|
|
while (DateTime.UtcNow - start < timeout)
|
||
|
|
{
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
|
||
|
|
var remaining = timeout - (DateTime.UtcNow - start);
|
||
|
|
var refreshTimeout = remaining < _pollInterval ? remaining : _pollInterval;
|
||
|
|
if (refreshTimeout > TimeSpan.Zero)
|
||
|
|
{
|
||
|
|
await TryRefreshNowAsync(refreshTimeout, cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (IsPointOn(name) == expectedState)
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
await Task.Delay(50, cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool TryGetPoint(string name, out DeviceIoPointState point)
|
||
|
|
{
|
||
|
|
DeviceIoPointDefinition definition;
|
||
|
|
if (!_definitionsByName.TryGetValue(name ?? string.Empty, out definition) && !_definitionsByPointKey.TryGetValue(name ?? string.Empty, out definition))
|
||
|
|
{
|
||
|
|
point = null;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
lock (_syncRoot)
|
||
|
|
{
|
||
|
|
if (_latestPoints.TryGetValue(definition.PointKey, out point))
|
||
|
|
{
|
||
|
|
point = point.Clone();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
point = null;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public IReadOnlyList<DeviceIoPointState> GetPointsByModule(string module)
|
||
|
|
{
|
||
|
|
lock (_syncRoot)
|
||
|
|
{
|
||
|
|
return _latestPoints.Values
|
||
|
|
.Where(x => string.Equals(x.Module, module, StringComparison.OrdinalIgnoreCase))
|
||
|
|
.Select(x => x.Clone())
|
||
|
|
.ToList();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private bool TryReadIoPoints(out IList<DeviceIoPointState> points)
|
||
|
|
{
|
||
|
|
points = new List<DeviceIoPointState>();
|
||
|
|
|
||
|
|
if (Definitions == null || Definitions.Count == 0)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
Dictionary<string, bool> readValues;
|
||
|
|
bool hasBatchValues = _hardwareManager.TryReadIoPointValues(Definitions, out readValues);
|
||
|
|
|
||
|
|
foreach (DeviceIoPointDefinition definition in Definitions)
|
||
|
|
{
|
||
|
|
bool value;
|
||
|
|
if (!definition.Enabled)
|
||
|
|
{
|
||
|
|
value = definition.DefaultValue;
|
||
|
|
}
|
||
|
|
else if (hasBatchValues && readValues != null && readValues.TryGetValue(definition.PointKey, out value))
|
||
|
|
{
|
||
|
|
}
|
||
|
|
else if (!_hardwareManager.TryReadIoPointValue(definition, out value))
|
||
|
|
{
|
||
|
|
value = definition.DefaultValue;
|
||
|
|
}
|
||
|
|
|
||
|
|
DeviceIoPointState state;
|
||
|
|
if (definition.Type == IoPointType.Output)
|
||
|
|
{
|
||
|
|
state = new DeviceOutputPointState
|
||
|
|
{
|
||
|
|
Id = definition.Id,
|
||
|
|
PointKey = definition.PointKey,
|
||
|
|
Module = definition.Module,
|
||
|
|
Name = definition.Name,
|
||
|
|
Card = definition.Card,
|
||
|
|
StationNo = definition.StationNo,
|
||
|
|
LineNo = definition.LineNo,
|
||
|
|
Value = value,
|
||
|
|
IsInverse = definition.IsInverse,
|
||
|
|
CommandValue = value
|
||
|
|
};
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
state = new DeviceInputPointState
|
||
|
|
{
|
||
|
|
Id = definition.Id,
|
||
|
|
PointKey = definition.PointKey,
|
||
|
|
Module = definition.Module,
|
||
|
|
Name = definition.Name,
|
||
|
|
Card = definition.Card,
|
||
|
|
StationNo = definition.StationNo,
|
||
|
|
LineNo = definition.LineNo,
|
||
|
|
Value = value,
|
||
|
|
IsInverse = definition.IsInverse
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
points.Add(state);
|
||
|
|
}
|
||
|
|
|
||
|
|
return points.Count > 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void LoadIoPointDefinitions()
|
||
|
|
{
|
||
|
|
_definitions.Clear();
|
||
|
|
_definitionsByModule.Clear();
|
||
|
|
_definitionsByName.Clear();
|
||
|
|
_definitionsByPointKey.Clear();
|
||
|
|
|
||
|
|
var pointKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
|
|
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
|
|
|
||
|
|
var path = ResolveConfigPath();
|
||
|
|
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var lines = File.ReadAllLines(path, Encoding.UTF8);
|
||
|
|
for (var i = 0; i < lines.Length; i++)
|
||
|
|
{
|
||
|
|
var raw = lines[i];
|
||
|
|
if (string.IsNullOrWhiteSpace(raw))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var line = raw.Trim();
|
||
|
|
if (line.StartsWith("#"))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (i == 0 && line.IndexOf("Id", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var cols = line.Split(',');
|
||
|
|
if (cols.Length < 11)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
int id;
|
||
|
|
if (!int.TryParse(cols[0].Trim(), out id))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var moduleIndex = 1;
|
||
|
|
var nameIndex = 2;
|
||
|
|
var typeIndex = 3;
|
||
|
|
var cardIndex = 4;
|
||
|
|
var stationIndex = 5;
|
||
|
|
var indexColumnIndex = 6;
|
||
|
|
var subIndexColumnIndex = 7;
|
||
|
|
var lineNoIndex = 8;
|
||
|
|
var enabledIndex = 9;
|
||
|
|
var defaultValueIndex = 10;
|
||
|
|
var isInverseIndex = cols.Length >= 12 ? 11 : -1;
|
||
|
|
var descriptionIndex = isInverseIndex >= 0 ? 12 : 11;
|
||
|
|
|
||
|
|
var name = cols[nameIndex].Trim();
|
||
|
|
if (string.IsNullOrWhiteSpace(name))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var module = cols[moduleIndex].Trim();
|
||
|
|
if (string.IsNullOrWhiteSpace(module))
|
||
|
|
{
|
||
|
|
module = "Default";
|
||
|
|
}
|
||
|
|
|
||
|
|
var type = ParseIoPointType(cols[typeIndex]);
|
||
|
|
var card = ParseIoCardType(cols[cardIndex]);
|
||
|
|
var stationNo = ParseInt(cols[stationIndex]);
|
||
|
|
var index = ParseInt(cols[indexColumnIndex]);
|
||
|
|
var subIndex = ParseInt(cols[subIndexColumnIndex]);
|
||
|
|
var lineNo = cols[lineNoIndex].Trim();
|
||
|
|
var pointKey = DeviceIoPointDefinition.BuildPointKey(type, card, stationNo, index, subIndex, lineNo);
|
||
|
|
|
||
|
|
if (!pointKeys.Add(pointKey) || !names.Add(name))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var definition = new DeviceIoPointDefinition
|
||
|
|
{
|
||
|
|
Id = id,
|
||
|
|
PointKey = pointKey,
|
||
|
|
Module = module,
|
||
|
|
Name = name,
|
||
|
|
Type = type,
|
||
|
|
Card = card,
|
||
|
|
StationNo = stationNo,
|
||
|
|
Index = index,
|
||
|
|
SubIndex = subIndex,
|
||
|
|
LineNo = lineNo,
|
||
|
|
Enabled = ParseBoolean(cols[enabledIndex], true),
|
||
|
|
DefaultValue = ParseBoolean(cols[defaultValueIndex], false),
|
||
|
|
IsInverse = isInverseIndex >= 0 && isInverseIndex < cols.Length && ParseBoolean(cols[isInverseIndex], false),
|
||
|
|
Description = cols.Length > descriptionIndex ? string.Join(",", cols.Skip(descriptionIndex)).Trim() : string.Empty
|
||
|
|
};
|
||
|
|
|
||
|
|
_definitions.Add(definition);
|
||
|
|
_definitionsByName[definition.Name] = definition;
|
||
|
|
_definitionsByPointKey[definition.PointKey] = definition;
|
||
|
|
|
||
|
|
List<DeviceIoPointDefinition> moduleDefinitions;
|
||
|
|
if (!_definitionsByModule.TryGetValue(module, out moduleDefinitions))
|
||
|
|
{
|
||
|
|
moduleDefinitions = new List<DeviceIoPointDefinition>();
|
||
|
|
_definitionsByModule[module] = moduleDefinitions;
|
||
|
|
}
|
||
|
|
|
||
|
|
moduleDefinitions.Add(definition);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static int ParseInt(string raw)
|
||
|
|
{
|
||
|
|
int value;
|
||
|
|
return int.TryParse((raw ?? string.Empty).Trim(), out value) ? value : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static IoPointType ParseIoPointType(string raw)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(raw))
|
||
|
|
{
|
||
|
|
return IoPointType.Input;
|
||
|
|
}
|
||
|
|
|
||
|
|
var text = raw.Trim();
|
||
|
|
if (string.Equals(text, "Output", StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
string.Equals(text, "OUT", StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
string.Equals(text, "DO", StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
string.Equals(text, "O", StringComparison.OrdinalIgnoreCase))
|
||
|
|
{
|
||
|
|
return IoPointType.Output;
|
||
|
|
}
|
||
|
|
|
||
|
|
return IoPointType.Input;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static IoCardType ParseIoCardType(string raw)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(raw))
|
||
|
|
{
|
||
|
|
return IoCardType.Td;
|
||
|
|
}
|
||
|
|
|
||
|
|
var text = raw.Trim();
|
||
|
|
if (string.Equals(text, "Acs", StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
string.Equals(text, "ACS", StringComparison.OrdinalIgnoreCase))
|
||
|
|
{
|
||
|
|
return IoCardType.Acs;
|
||
|
|
}
|
||
|
|
|
||
|
|
return IoCardType.Td;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool ParseBoolean(string raw, bool defaultValue)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(raw))
|
||
|
|
{
|
||
|
|
return defaultValue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var text = raw.Trim();
|
||
|
|
if (string.Equals(text, "1", StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
string.Equals(text, "true", StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
string.Equals(text, "yes", StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
string.Equals(text, "y", StringComparison.OrdinalIgnoreCase))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (string.Equals(text, "0", StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
string.Equals(text, "false", StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
string.Equals(text, "no", StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
string.Equals(text, "n", StringComparison.OrdinalIgnoreCase))
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return defaultValue;
|
||
|
|
}
|
||
|
|
|
||
|
|
private DeviceIoPointDefinition ResolveUniqueDefinitionById(int id, IoPointType? pointType)
|
||
|
|
{
|
||
|
|
var matches = _definitions
|
||
|
|
.Where(x => x.Id == id && (!pointType.HasValue || x.Type == pointType.Value))
|
||
|
|
.Take(2)
|
||
|
|
.ToList();
|
||
|
|
|
||
|
|
return matches.Count == 1 ? matches[0] : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void CompleteRefreshWaiters(bool result)
|
||
|
|
{
|
||
|
|
List<TaskCompletionSource<bool>> waiters;
|
||
|
|
lock (_refreshWaitersSyncRoot)
|
||
|
|
{
|
||
|
|
if (_refreshWaiters.Count == 0)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
waiters = _refreshWaiters.ToList();
|
||
|
|
_refreshWaiters.Clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var waiter in waiters)
|
||
|
|
{
|
||
|
|
waiter.TrySetResult(result);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RemoveRefreshWaiter(TaskCompletionSource<bool> waiter)
|
||
|
|
{
|
||
|
|
lock (_refreshWaitersSyncRoot)
|
||
|
|
{
|
||
|
|
_refreshWaiters.Remove(waiter);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private bool TryResolveOutputDefinition(string pointReference, out DeviceIoPointDefinition definition)
|
||
|
|
{
|
||
|
|
definition = null;
|
||
|
|
if (string.IsNullOrWhiteSpace(pointReference))
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (_definitionsByPointKey.TryGetValue(pointReference, out definition))
|
||
|
|
{
|
||
|
|
return definition.Type == IoPointType.Output && definition.Enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (_definitionsByName.TryGetValue(pointReference, out definition))
|
||
|
|
{
|
||
|
|
return definition.Type == IoPointType.Output && definition.Enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
int id;
|
||
|
|
if (int.TryParse(pointReference, out id))
|
||
|
|
{
|
||
|
|
definition = ResolveUniqueDefinitionById(id, IoPointType.Output);
|
||
|
|
return definition != null && definition.Enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string ResolveConfigPath()
|
||
|
|
{
|
||
|
|
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||
|
|
var localPath = Path.Combine(baseDir, "Configuration", DeviceIoConfigFileName);
|
||
|
|
var sourcePath = ResolveRootConfigPath(baseDir);
|
||
|
|
|
||
|
|
if (TrySyncConfigToLocal(sourcePath, localPath))
|
||
|
|
{
|
||
|
|
return localPath;
|
||
|
|
}
|
||
|
|
|
||
|
|
return File.Exists(localPath) ? localPath : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string ResolveRootConfigPath(string baseDir)
|
||
|
|
{
|
||
|
|
var current = new DirectoryInfo(baseDir);
|
||
|
|
while (current != null)
|
||
|
|
{
|
||
|
|
var rootConfigDir = Path.Combine(current.FullName, "Configuration");
|
||
|
|
if (Directory.Exists(rootConfigDir) && Directory.Exists(Path.Combine(current.FullName, "MainShell")))
|
||
|
|
{
|
||
|
|
var path = Path.Combine(rootConfigDir, DeviceIoConfigFileName);
|
||
|
|
if (File.Exists(path))
|
||
|
|
{
|
||
|
|
return path;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
current = current.Parent;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool TrySyncConfigToLocal(string sourcePath, string localPath)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(sourcePath) || !File.Exists(sourcePath))
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!string.Equals(Path.GetFullPath(sourcePath), Path.GetFullPath(localPath), StringComparison.OrdinalIgnoreCase))
|
||
|
|
{
|
||
|
|
var localDir = Path.GetDirectoryName(localPath);
|
||
|
|
if (!string.IsNullOrWhiteSpace(localDir))
|
||
|
|
{
|
||
|
|
Directory.CreateDirectory(localDir);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!File.Exists(localPath) || File.GetLastWriteTimeUtc(localPath) < File.GetLastWriteTimeUtc(sourcePath))
|
||
|
|
{
|
||
|
|
File.Copy(sourcePath, localPath, true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private List<DeviceIoPointState> CollectChangedPoints(IList<DeviceIoPointState> readPoints)
|
||
|
|
{
|
||
|
|
var changed = new List<DeviceIoPointState>();
|
||
|
|
|
||
|
|
lock (_syncRoot)
|
||
|
|
{
|
||
|
|
if (_latestPoints.Count == 0)
|
||
|
|
{
|
||
|
|
foreach (var point in readPoints)
|
||
|
|
{
|
||
|
|
if (point == null)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
_latestPoints[point.PointKey] = PreparePoint(point.Clone());
|
||
|
|
}
|
||
|
|
|
||
|
|
return changed;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var point in readPoints)
|
||
|
|
{
|
||
|
|
if (point == null)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!_latestPoints.TryGetValue(point.PointKey, out DeviceIoPointState old))
|
||
|
|
{
|
||
|
|
_latestPoints[point.PointKey] = PreparePoint(point.Clone());
|
||
|
|
changed.Add(PreparePoint(point.Clone()));
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (HasPointChanged(old, point))
|
||
|
|
{
|
||
|
|
_latestPoints[point.PointKey] = PreparePoint(point.Clone());
|
||
|
|
changed.Add(PreparePoint(point.Clone()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return changed;
|
||
|
|
}
|
||
|
|
|
||
|
|
private IReadOnlyDictionary<string, DeviceIoPointState> GetLatestPointsSnapshot()
|
||
|
|
{
|
||
|
|
lock (_syncRoot)
|
||
|
|
{
|
||
|
|
return _latestPoints.ToDictionary(x => x.Key, x => PreparePoint(x.Value.Clone()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private DeviceIoPointState PreparePoint(DeviceIoPointState point)
|
||
|
|
{
|
||
|
|
var outputPoint = point as DeviceOutputPointState;
|
||
|
|
DeviceIoPointDefinition definition;
|
||
|
|
_definitionsByPointKey.TryGetValue(point.PointKey ?? string.Empty, out definition);
|
||
|
|
if (outputPoint != null && definition != null && definition.Enabled)
|
||
|
|
{
|
||
|
|
outputPoint.BindWriteAction(outputOn => TrySetOutputState(definition.Name, outputOn));
|
||
|
|
}
|
||
|
|
|
||
|
|
return point;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool HasPointChanged(DeviceIoPointState oldPoint, DeviceIoPointState newPoint)
|
||
|
|
{
|
||
|
|
if (oldPoint == null || newPoint == null)
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (oldPoint.Value != newPoint.Value ||
|
||
|
|
oldPoint.PointType != newPoint.PointType ||
|
||
|
|
!string.Equals(oldPoint.PointKey, newPoint.PointKey, StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
!string.Equals(oldPoint.Name, newPoint.Name, StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
!string.Equals(oldPoint.Module, newPoint.Module, StringComparison.OrdinalIgnoreCase) ||
|
||
|
|
oldPoint.Card != newPoint.Card)
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
var oldOutput = oldPoint as DeviceOutputPointState;
|
||
|
|
var newOutput = newPoint as DeviceOutputPointState;
|
||
|
|
if (oldOutput != null && newOutput != null)
|
||
|
|
{
|
||
|
|
return oldOutput.CommandValue != newOutput.CommandValue;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|