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;
}
}
}