using DTS.Common.Interface.Channels; using DTS.Common.Interface.DASFactory; using DTS.Common.Interface.DASFactory.Config; using DTS.Common.Interface.Sensors; using DTS.Common.Interface.StatusAndProgressBar; using DTS.Common.Utilities.Logging; using DTS.DASLib.Service.StateMachine.StatusAndParameters.Configure; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace DTS.DASLib.Service.StateMachine { public class ConfigureStatusInformation : IStatusInfo { /// /// all possible status that can be relayed to consumers /// public enum StatusValues { ApplyingConfiguration, AutoResolvingChannels, ManuallyResolvedChannels, Completed, Cancelling, Cancelled, ChannelOutOfPosition, NoChannelsAssigned, AllChannelsResolved, EIDNotFound, ApplyConfigFailed, AppliedConfiguration, PrepareForDiagnostics, PrepareForDiagnosticsFailed, PrepareForDiagnosticsSuccess, LowPower, LowPowerSuccess, LowPowerFailure } /// /// no channels are currently assigned /// public bool NoChannelsAssigned { get; internal set; } = true; /// /// all channels to be resolved are resolved /// public bool AllChannelsResolved { get; internal set; } = false; /// /// one or more channels are out of position with respect to how /// they requested to be positioned (they are on a different channel than they were specified to be on) /// public bool ChannelsOutOfPosition { get; internal set; } = false; /// /// all units that were to be configured were configured /// public bool HaveAppliedConfigAllUnits { get; set; } = false; /// /// signals a cancel request /// public ManualResetEvent CancelEvent = new ManualResetEvent(false); /// /// signals when work is finished /// public ManualResetEvent DoneEvent = new ManualResetEvent(false); /// /// action to take on completion of work /// public ActionCompleteDelegate CompleteAction { get; set; } /// /// action to take on progress notification /// public SetProgressValueDelegate ProgressAction { get; set; } /// /// action to take on a status notification /// public StatusIntDelegate StatusAction { get; set; } /// /// action to take on extended status notification /// public StatusExIntDelegate StatusExAction { get; set; } /// /// holds references to all units which were successfully configured /// public IDASCommunication[] UnitsConfigured { get; set; } = new IDASCommunication[0]; /// /// task for turning excitation on/off, used for monitoring task or interrupting /// private Task _ExcitationTask = null; /// /// task for applying configuration, used for monitoring task or interrupting /// private Task _ApplyConfigTask = null; /// /// used to canel running task /// returns when task is cancelled or completed /// /// public async Task Cancel() { CancelEvent.Set(); StatusAction?.Invoke((int)StatusValues.Cancelling); //simulate time spent waiting for cancel to be acknowledged Thread.Sleep(100); DoneEvent.WaitOne(); StatusAction?.Invoke((int)StatusValues.Cancelled); } /// /// turns on power (if requested and appropriate) /// private void PrepareForDiagnostics() { var param = States.Instance.Configure.Status.ConfigureParameters; var status = States.Instance.Configure.Status.ConfigureStatus; var global = States.Instance.Configure.Status.GlobalStatusInformation; var dasFactory = States.Instance.Configure.DASFactory; //make sure we have some units to turn on, if not fail task if (!status.UnitsConfigured.Any()) { StatusAction?.Invoke((int)StatusValues.PrepareForDiagnosticsFailed); return; } ProgressAction?.Invoke(0D); StatusAction?.Invoke((int)StatusValues.PrepareForDiagnostics); //record if any units fail to turn on excitation var bPassed = true; var mre = new ManualResetEvent(false); try { using (var diagnosticsService = new DiagnosticsService()) { var units = status.UnitsConfigured.ToList(); diagnosticsService.PrepareForDiagnostics(units, PrePostResults.PreEventDiagnosticsResult, param.SampleRateLookup, param.AAFRateLookup, (ServiceBase.CallbackData data) => { switch (data.Status) { case ServiceBase.CallbackData.CallbackStatus.Success: StatusExAction?.Invoke((int)StatusValues.PrepareForDiagnosticsSuccess, data.Target); global.AddUnitAtHighPower(data.Target); break; case ServiceBase.CallbackData.CallbackStatus.Failure: bPassed = false; StatusExAction?.Invoke((int)StatusValues.PrepareForDiagnosticsFailed, data.Target); break; case ServiceBase.CallbackData.CallbackStatus.AllFinished: mre.Set(); break; } }, units); } } catch (Exception ex) { bPassed = false; throw ex; } var timeWaited = 0; while (!mre.WaitOne(PREPARE_SPIN_TIME, false)) { timeWaited += PREPARE_SPIN_TIME; ProgressAction?.Invoke(100D * timeWaited / EXPECTED_PREPARE_TIME); } if (bPassed) { StatusAction?.Invoke((int)StatusValues.PrepareForDiagnosticsSuccess); States.Instance.ConfigureStart.Status.GlobalStatusInformation.ExcitationOn = true; } else { StatusAction?.Invoke((int)StatusValues.PrepareForDiagnosticsFailed); } } private const int PREPARE_SPIN_TIME = 200; private const int EXPECTED_PREPARE_TIME = 8000; /// /// lock used to control thread access to shared resources /// [like UnitsConfigured] /// private static readonly object MyLock = new object(); private void AddConfiguredDevice(IDASCommunication device) { lock (MyLock) { if (UnitsConfigured.Contains(device)) { return; } var list = new List(); list.AddRange(UnitsConfigured); list.Add(device); UnitsConfigured = list.ToArray(); } StatusExAction?.Invoke((int)StatusValues.AppliedConfiguration, device); } /// /// Turns off excitation, returns immediately /// public void TurnOffExcitation() { var param = States.Instance.ConfigureStart.Status.ConfigureParameters; //reset the flag that got us here param.TurnOffExcitation = false; var status = States.Instance.ConfigureStart.Status.ConfigureStatus; var dasFactory = States.Instance.ConfigureStart.DASFactory; _ExcitationTask = Task.Run(() => { StatusAction?.Invoke((int)StatusValues.LowPower); var units = dasFactory.GetDASList(); var anyFailed = false; try { var mre = new ManualResetEvent(false); using (var service = new ArmingService()) { service.EnterLowPowerMode(units, (ServiceBase.CallbackData data) => { switch (data.Status) { case ServiceBase.CallbackData.CallbackStatus.Success: StatusExAction?.Invoke((int)StatusValues.LowPowerSuccess, data.Target); break; case ServiceBase.CallbackData.CallbackStatus.AllFinished: mre.Set(); break; case ServiceBase.CallbackData.CallbackStatus.Failure: anyFailed = true; StatusExAction?.Invoke((int)StatusValues.LowPowerFailure, data.Target); break; } }, units); } mre.WaitOne(); } catch (Exception ex) { APILogger.Log(ex); } //if any failed to turn off record the error //if all turned off, then set the global status param that excitation is now known off if (anyFailed) { StatusAction?.Invoke((int)StatusValues.LowPowerFailure); } else { States.Instance.ConfigureStart.Status.GlobalStatusInformation.ExcitationOn = false; StatusAction?.Invoke((int)StatusValues.LowPowerSuccess); } StatusAction?.Invoke((int)StatusValues.Completed); CompleteAction?.Invoke(); DoneEvent.Set(); }); } /// /// starts ApplyConfig process, returns immediately /// public void ApplyConfig() { DoneEvent.Reset(); var param = States.Instance.ConfigureStart.Status.ConfigureParameters; if (param.TurnOffExcitation) { TurnOffExcitation(); return; } var unitsToConfigure = param.UnitsToConfigure.ToList(); var dasFactory = States.Instance.ConfigureStart.DASFactory; _ApplyConfigTask = Task.Run(() => { //the user could just be turning on power, in which case the configuration is already set //if the flag for set configuration is set, then set the configuration, otherwise don't if (param.SetConfiguration) { HaveAppliedConfigAllUnits = false; StatusAction?.Invoke((int)StatusValues.ApplyingConfiguration); //check if we are supposed to configure a unit, but it's not available //mark it failed if the unit is not available var units = dasFactory.GetDASList(); foreach (var unit in unitsToConfigure) { if (!units.Contains(unit)) { unitsToConfigure.Remove(unit); StatusExAction?.Invoke((int)StatusValues.ApplyConfigFailed, unit); } } var mre = new ManualResetEvent(false); if (unitsToConfigure.Any()) { using (var configService = new ConfigurationService()) { configService.SetConfiguration(unitsToConfigure, param.DoStrictCheck, param.EventConfig, (data) => { switch (data.Status) { case ServiceBase.CallbackData.CallbackStatus.AllFinished: mre.Set(); break; case ServiceBase.CallbackData.CallbackStatus.Progress: ProgressAction?.Invoke(data.ProgressValue); break; case ServiceBase.CallbackData.CallbackStatus.Failure: StatusExAction?.Invoke((int)StatusValues.ApplyConfigFailed, data.Target, data.ErrorMessage, data.ErrorException); break; case ServiceBase.CallbackData.CallbackStatus.Success: AddConfiguredDevice(data.Target); break; } }, unitsToConfigure, param.ErrorRequiringActionAction, param.DummyConfig, param.MaxAAF, param.ConfigureDigitalOutputs, param.TurnOffAAFRealtime, param.DSPFilterType, param.DiscardDiagnostics); } } mre.WaitOne(); //check that all the units that should have been configured were var allConfigured = Array.TrueForAll(param.UnitsToConfigure, d => UnitsConfigured.Contains(d)); HaveAppliedConfigAllUnits = allConfigured; } //finally turn on power UNLESS the user has specified not to (most likely by pressing "Low Power" button) //but also potentially by a property setting if (!CancelEvent.WaitOne(1, false)) { if (param.PrepareForDiagnostics && !param.SkipTurnOnPower) { PrepareForDiagnostics(); } CompleteAction?.Invoke(); } StatusAction?.Invoke((int)StatusValues.Completed); DoneEvent.Set(); }); } /// /// holds channels that are not resolved for one reason or another /// private readonly List _unresolvedChannels = new List(); /// /// holds channels that are now resolved/assigned /// private readonly List _resolvedChannels = new List(); private bool IsUnresolved(IGroupChannel ch) { return _unresolvedChannels.Exists(channel => channel.Channel == ch); } private bool IsResolved(IGroupChannel ch) { return _resolvedChannels.Exists(channel => channel.Channel == ch); } private void AddResolvedChannel(IGroupChannel channel, IDASChannel hwChannel, bool eidOutOfPlace, ConfigureStatusParameters param, IDictionary lookupChannelToDAS, ISensorData sd, bool missingEID) { //remove any possible duplicates _resolvedChannels.RemoveAll(ch => ch.Channel == channel); _unresolvedChannels.RemoveAll(ch => ch.Channel == channel); var meta = new GroupChannelWithMeta() { Channel = channel, ChannelConflict = false, ConflictingChannel = null, DASChannel = hwChannel, EIDOutOfPlace = eidOutOfPlace, HWChannelIncompatible = false, MissingID = missingEID, MissingSensor = false }; _resolvedChannels.Add(meta); //set the hardware in the group if needed var includedHardware = channel.Group.IncludedHardware.ToList(); var hwId = param.GetDatabaseIdAction(lookupChannelToDAS[hwChannel]); if (!includedHardware.Contains(hwId)) { includedHardware.Add(hwId); } channel.Group.SetIncludedHardware(includedHardware.ToArray()); //set the calibration if (hwChannel is AnalogInputDASChannel aic) { foreach (var e in sd.SupportedExcitation) { if (!aic.IsSupported(e)) { continue; } var sc = param.GetCalibrationAction(sd, e); if (null == sc) { continue; } param.SetSensorCalibrationAction(sd, sc); break; } } } private void AddUnresolvedChannel(IGroupChannel ch, bool bChannelConflict, IGroupChannel conflictingChannel, bool EIDOutOfPlace, bool hwChannelIncompatible, bool hwNotFound, bool missingId, bool missingSensor) { //remove any possible duplicates _resolvedChannels.RemoveAll(channel => channel.Channel == ch); _unresolvedChannels.RemoveAll(channel => channel.Channel == ch); var meta = new GroupChannelWithMeta() { Channel = ch, ChannelConflict = bChannelConflict, ConflictingChannel = conflictingChannel, DASChannel = null, EIDOutOfPlace = EIDOutOfPlace, HWChannelIncompatible = hwChannelIncompatible, HWNotFound = hwNotFound, MissingID = missingId, MissingSensor = missingSensor }; _unresolvedChannels.Add(meta); } private bool IsHWChannelResolved(IDASChannel channel) { return _resolvedChannels.Exists(ch => ch.DASChannel == channel); } public void ManuallyUnresolveChannel(IGroupChannel channel) { if (!IsResolved(channel)) { throw new InvalidAssignmentException(channel, InvalidAssignmentException.Reasons.ChannelNotAssigned); } var resolvedChannel = _resolvedChannels.First(ch => ch.Channel == channel); if (resolvedChannel.AssignedByEID) { throw new InvalidAssignmentException(channel, InvalidAssignmentException.Reasons.EID_Locked); } AddUnresolvedChannel(channel, false, null, false, false, resolvedChannel.HWNotFound, resolvedChannel.MissingID, false); } public void ManuallyResolveChannel(IGroupChannel channel, IDASChannel hardwareChannel) { var param = States.Instance.ConfigureStart.Status.ConfigureParameters; GetIdToHardwareLookup(param, out var lookupIDToHardware, out var lookupChannelToDAS, out var lookupDAStoChannel); if (channel.IsBlank()) { throw new InvalidAssignmentException(channel, InvalidAssignmentException.Reasons.BlankChannel); } if (channel.IsDisabled) { throw new InvalidAssignmentException(channel, InvalidAssignmentException.Reasons.ChannelDisabled); } if (channel.SensorId < 0) { throw new InvalidAssignmentException(channel, InvalidAssignmentException.Reasons.NoSensor); } var sd = param.GetSensorAction(channel); if (null == sd) { throw new InvalidAssignmentException(channel, InvalidAssignmentException.Reasons.SensorNotFound); } var compatible = IsSensorHwCompatible(hardwareChannel, sd); if (SensorCompatiblilityResponse.Compatible != compatible) { throw new InvalidAssignmentException(channel, InvalidAssignmentException.Reasons.IncompatibleHardware); } //alright, looks ok ... unless there's something on that channel already ... if (IsHWChannelResolved(hardwareChannel)) { throw new InvalidAssignmentException(channel, InvalidAssignmentException.Reasons.ChannelAlreadyAssigned); } //ok, well make sure it's we didn't find the EID for the channel already on a channel, if so ... it's locked on //that channel bool bFoundEID = false; if (IsResolved(channel)) { var resolvedChannel = _resolvedChannels.First(ch => ch.Channel == channel); if (resolvedChannel.AssignedByEID) { if (resolvedChannel.DASChannel != hardwareChannel) { throw new InvalidAssignmentException(channel, InvalidAssignmentException.Reasons.EID_Locked); } //nothing to do ... bFoundEID = true; } } //determine if we should have found an EID and didn't var bMissingEID = !string.IsNullOrWhiteSpace(sd.EID) && !bFoundEID; //if we are missing an ID, and aren't allowed to assign to a channel without an id, then don't allow it if (bMissingEID && !param.AllowSensorIdToBlankChannel) { throw new InvalidAssignmentException(channel, InvalidAssignmentException.Reasons.EIDRequiredAndMissing); } //add as a resolved channel AddResolvedChannel(channel, hardwareChannel, false, param, lookupChannelToDAS, sd, bMissingEID); AllChannelsResolved = _unresolvedChannels.Any(); } /// /// thrown by manually resolve/unresolve functions /// public class InvalidAssignmentException : Exception { public enum Reasons { BlankChannel, NoSensor, ChannelDisabled, SensorNotFound, IncompatibleHardware, ChannelAlreadyAssigned, EID_Locked, //sensor is already locked by EID to a different channel EIDRequiredAndMissing, ChannelNotAssigned } public IGroupChannel GroupChannel { get; private set; } public Reasons Reason { get; private set; } public InvalidAssignmentException(IGroupChannel channel, Reasons reason) : base($"{reason.ToString()} - {channel}") { Reason = reason; GroupChannel = channel; } } /// /// sensor is not compatible with hardware, but maybe in the future we'll want to know why /// even though right now it's just "hw incompatible" /// internal enum SensorCompatiblilityResponse { Compatible, DigitalInputNotSupportedOnHWChannel, DigitalInputModeNotSupportedOnHWChannel, BridgeModeNotSupportedOnHWChannel, SquibFireModeNotSupportedOnHWChannel, NoSupportedExcitationOnHWChannel } /// /// check if sensor can run on the channel in question /// checks excitation, bridge, sensor type, etc /// /// /// /// private SensorCompatiblilityResponse IsSensorHwCompatible(IDASChannel hwChannel, ISensorData sd) { switch (hwChannel) { case AnalogInputDASChannel aic: if (aic.SupportedBridges.Contains(sd.Bridge)) { if (aic.DigitalInputChannel) { if (!sd.IsDigitalInput()) { //hardware is digital, sensor is not, it's not compatible return SensorCompatiblilityResponse.DigitalInputNotSupportedOnHWChannel; } else if (!aic.SupportedDigitalInputModes.Contains(sd.InputMode)) { //sensor and hardware are digital, but the input mode is not supported return SensorCompatiblilityResponse.DigitalInputModeNotSupportedOnHWChannel; } } else { var param = States.Instance.ConfigureStart.Status.ConfigureParameters; var excitationPassed = (from e in sd.SupportedExcitation where aic.SupportedExcitation.Contains(e) select param.GetCalibrationAction(sd, e)).Any(sc => null != sc); if (!excitationPassed) { //there's no supported excitation match return SensorCompatiblilityResponse.NoSupportedExcitationOnHWChannel; } } } else { //the hardware does not support the bridge mode of the sensor ... return SensorCompatiblilityResponse.BridgeModeNotSupportedOnHWChannel; } break; case OutputSquibChannel _: var squibChannel = (OutputSquibChannel)hwChannel; if (!sd.IsSquib()) { //hardware channel is squib, sensor is not return SensorCompatiblilityResponse.BridgeModeNotSupportedOnHWChannel; } else if (!squibChannel.SupportedSquibFireModes.Contains(sd.SquibFireMode)) { //hardware and sensor are squib, but fire mode not supported by hardware return SensorCompatiblilityResponse.SquibFireModeNotSupportedOnHWChannel; } break; case OutputTOMDigitalChannel _: if (!sd.IsDigitalOutput()) { //hardware channel is digital output, sensor is not return SensorCompatiblilityResponse.BridgeModeNotSupportedOnHWChannel; } break; } return SensorCompatiblilityResponse.Compatible; } /// /// returns sensor EID to hardware channel mapping as determined from connected units /// /// /// hardware channel to IDASCommunication /// eid to hardware channel /// private void GetIdToHardwareLookup(ConfigureStatusParameters param, out IDictionary lookupIDToHardware, out IDictionary lookupChannelToDAS, out IDictionary> lookupDASToChannel ) { lookupIDToHardware = new Dictionary(); lookupChannelToDAS = new Dictionary(); lookupDASToChannel = new Dictionary>(); foreach (var unit in param.UnitsToConfigure) { //why is there no config data? if (null == unit.ConfigData) { continue; } var dasid = param.GetDatabaseIdAction(unit); if (!lookupDASToChannel.ContainsKey(dasid)) { lookupDASToChannel.Add(dasid, new Dictionary()); } foreach (var module in unit.ConfigData.Modules) { foreach (var channel in module.Channels) { lookupChannelToDAS[channel] = unit; lookupDASToChannel[dasid][channel.Number] = channel; if (null != channel.IDs && channel.IDs.Length > 0) { var id = channel.IDs[0].ID; if (!string.IsNullOrWhiteSpace(id)) { lookupIDToHardware[id] = channel; } } } } } } /// /// resets all status to defaults /// public void Reset() { NoChannelsAssigned = true; AllChannelsResolved = false; ChannelsOutOfPosition = false; HaveAppliedConfigAllUnits = false; StatusAction = null; CompleteAction = null; ProgressAction = null; UnitsConfigured = new IDASCommunication[0]; _unresolvedChannels.Clear(); _resolvedChannels.Clear(); _ExcitationTask = null; _ApplyConfigTask = null; } } }