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 IoChanged; IReadOnlyList Definitions { get; } IReadOnlyDictionary> DefinitionsByModule { get; } IReadOnlyDictionary 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 WaitForPointStateAsync(int id, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken)); Task WaitForPointStateAsync(string name, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken)); Task WaitForPointStateByPointKeyAsync(string pointKey, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken)); IReadOnlyList GetPointsByModule(string module); void Start(); void Stop(); void RequestRefresh(); bool TryRefreshNow(TimeSpan timeout); Task 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 requests, out string failurePointReference); bool TrySetOutputStatesByPointKey(IReadOnlyDictionary 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 _definitions = new List(); private readonly Dictionary> _definitionsByModule = new Dictionary>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _definitionsByName = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _definitionsByPointKey = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly object _refreshWaitersSyncRoot = new object(); private readonly List> _refreshWaiters = new List>(); private const string DeviceIoConfigFileName = "DeviceIoPoints.csv"; private readonly Dictionary _latestPoints = new Dictionary(StringComparer.OrdinalIgnoreCase); private CancellationTokenSource _cts; public event EventHandler IoChanged; public IReadOnlyList Definitions => _definitions; public IReadOnlyDictionary> DefinitionsByModule => _definitionsByModule; public IReadOnlyDictionary 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 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 TryRefreshNowAsync(TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken)) { Start(); var waiter = new TaskCompletionSource(); 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 requests, out string failurePointReference) { failurePointReference = string.Empty; if (requests == null || requests.Count == 0) { return true; } var definitions = new List>(); 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 outputStates, out string failurePointKey) { failurePointKey = string.Empty; if (outputStates == null || outputStates.Count == 0) { return true; } var definitions = new List>(); 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 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 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 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 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 points) { points = new List(); if (Definitions == null || Definitions.Count == 0) { return false; } Dictionary 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(StringComparer.OrdinalIgnoreCase); var names = new HashSet(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 moduleDefinitions; if (!_definitionsByModule.TryGetValue(module, out moduleDefinitions)) { moduleDefinitions = new List(); _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> 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 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 CollectChangedPoints(IList readPoints) { var changed = new List(); 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 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; } } }