using System.Collections.Generic; using System.Globalization; using OpenBveApi.Runtime; namespace OpenbveFcmbTrainPlugin { /// A device simulating ATC by Dimetronic. internal class AtcDimetronic : Device { /// Represents the state of the device. private enum DeviceStates { /// The device is being overriden. Override, /// The device has been initialized and no driving mode is selected. NoMode, /// The device is in YARD (M+25) driving mode. YARD, /// The device is in ATP (M+ATP) driving mode. ATP, /// The device is in ATO mode. ATO, } /// The current state of the device. private DeviceStates DeviceState; /// Represents the state of the ATC control. private enum AtcControlStates { /// The brakes are released. Released, /// The power is cut. NoPower, /// The power is cut and an alarm plays. NoPowerAlarm, /// The emergency brake is applied. BrakeEmergency, } /// The current state of the ATC control. private AtcControlStates AtcControlState; /// Represents the state of the track. private enum TrackStates { Unprotected, Enable, ATC, Disable } /// The current state of the track. private TrackStates TrackState; /// The current position of the train. private double TrainLocation; /// The position of the train used to detect runback. private double RunbackLocation; /// The position of the train used to detect rollforward. private double RollforwardLocation; /// Whether rollforward should be detected or not. private bool RollforwardTrigger; /// The length of the train. private readonly double TrainLength; /// The maximum speed in YARD mode. private readonly Speed YardMaximumSpeed; /// The distance after which the runback protection is triggered. private readonly double RunbackDistance; /// The distance after which the rollforward protection is triggered. private readonly double RollforwardDistance; /// Whether ATO is available or not on the train. private readonly bool AtoAvailable; /// The index of the alarm sound. private readonly int AlarmSoundIndex; /// Represents an ATC speed code. private class SpeedCode { internal Speed CurrentLimit { get; private set; } internal Speed WarningOn { get; private set; } internal Speed WarningOff { get; private set; } internal Speed TargetLimit { get; private set; } internal double TargetDistance { get; private set; } internal SpeedCode(Speed limit, Speed target, double distance) { CurrentLimit = limit; TargetLimit = target; TargetDistance = distance; if (limit == target) { // Constant speed WarningOn = new Speed((limit.KilometersPerHour - 3) / 3.6); WarningOff = new Speed((limit.KilometersPerHour - 5) / 3.6); } else { // Reduce speed WarningOn = target; WarningOff = new Speed((target.KilometersPerHour - 2) / 3.6); } } internal SpeedCode(Speed limit, Speed target) : this(limit, target, 0) { } /// Updates the ATC speed code. /// The distance to the end of the signalling block. internal void Update(double distance) { TargetDistance = distance; } } /// The current speed code received by the train. private SpeedCode CurrentSpeedCode; /// Represents a signal code. internal class SignalCode { internal int Aspect; internal Speed Limit; internal Speed Target; internal Speed EntryTarget; internal SignalCode(int aspect, string data) { Aspect = aspect; Limit = new Speed(0); Target = new Speed(0); EntryTarget = new Speed(0); if (data.Contains("/")) { string[] speeds = data.Split('/'); if (speeds.Length > 1) { string a = speeds[0]; string b = speeds[1]; if (double.TryParse(a, NumberStyles.Float, CultureInfo.InvariantCulture, out double an) && double.TryParse(b, NumberStyles.Float, CultureInfo.InvariantCulture, out double bn)) { if (an >= bn && an >= 0) { Limit = new Speed(an / 3.6); } if (bn <= an && bn >= 0) { Target = new Speed(bn / 3.6); } if (speeds.Length > 2) { if (double.TryParse(speeds[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double cn)) { EntryTarget = new Speed(cn / 3.6); } } } } } } } /// The list of signal codes recognised by the device. private readonly List SignalCodes; /// The aspect of the current signal in the route. private int CurrentSignalAspect; /// The start position of the current signal. private double CurrentSignalStartLocation; /// The end position of the current signal. private double CurrentSignalEndLocation; /// The ideal power notch for the AI. private int AiPowerNotch; /// The ideal brake notch for the AI. private int AiBrakeNotch; /// Creates an instance of the Bombardier ATC device. /// The length of the train, in meters. /// The maximum speed in YARD mode, in km/h. /// The distance after which the runback protection is triggered, in meters. /// The distance after which the rollforward protection is triggered, in meters. /// The list of signal codes recognised by the device. /// The index of the alarm sound. /// Whether ATO is available or not. internal AtcDimetronic(double trainLength, Speed yardMaxSpeed, double runbackDistance, double rollforwardDistance, List signalCodes, int alarmSoundIndex, bool atoAvailable) { TrainLength = trainLength; YardMaximumSpeed = yardMaxSpeed; RunbackDistance = runbackDistance; RollforwardDistance = rollforwardDistance; SignalCodes = signalCodes; AlarmSoundIndex = alarmSoundIndex; AtoAvailable = atoAvailable; } /// Is called when the device state should be updated. /// The current train. /// The current route. /// Whether the device should initialize. /// The time elapsed since the previous call. internal override void Update(Train train, Route route, bool init, Time elapsedTime) { if (init) { // Initialize device on start DeviceState = DeviceStates.NoMode; // Reset runback and rollforward detection RunbackLocation = train.State.Location; RollforwardLocation = train.State.Location; } // Update train location TrainLocation = train.State.Location; double speed = train.State.Speed.KilometersPerHour; bool stopped = speed < 0.05; if (DeviceState == DeviceStates.YARD || DeviceState == DeviceStates.ATP || DeviceState == DeviceStates.ATO) { // Speed limit enforcement if (AtcControlState == AtcControlStates.Released && speed > CurrentSpeedCode.WarningOn.KilometersPerHour) { // Cut power above warning on threshold and play alarm AtcControlState = AtcControlStates.NoPowerAlarm; } if ((AtcControlState == AtcControlStates.NoPower || AtcControlState == AtcControlStates.NoPowerAlarm) && speed < CurrentSpeedCode.WarningOff.KilometersPerHour) { // Stop cutting power below warning off threshold AtcControlState = AtcControlStates.Released; } if (speed > CurrentSpeedCode.CurrentLimit.KilometersPerHour) { // Unselect driving mode to apply emergency brake if overspeeding DeviceState = DeviceStates.NoMode; } // Runback protection if ((train.PhysicalHandles.Reverser == 1 && TrainLocation > RunbackLocation) || (train.PhysicalHandles.Reverser == -1 && TrainLocation < RunbackLocation)) { // The train is moving in the correct direction, update location RunbackLocation = TrainLocation; } if ((train.PhysicalHandles.Reverser >= 0 && TrainLocation < RunbackLocation - RunbackDistance) || (train.PhysicalHandles.Reverser <= 0 && TrainLocation > RunbackLocation + RunbackDistance)) { // The train is moving in the incorrect direction, reset driving mode DeviceState = DeviceStates.NoMode; } // Rollforward protection if (train.PhysicalHandles.BrakeNotch > 0 || train.PhysicalHandles.PowerNotch > 0) { // The train has a power/brake notch applied, movement is expected RollforwardTrigger = false; RollforwardLocation = TrainLocation; } else if (stopped) { // The train is stopped but without power or brakes, start checking for unintended rolling RollforwardTrigger = true; } if (RollforwardTrigger && (TrainLocation > RollforwardLocation + RollforwardDistance || TrainLocation < RollforwardLocation - RollforwardDistance)) { // The train is rolling unintendedly, reset driving mode DeviceState = DeviceStates.NoMode; } } // Brake application switch (AtcControlState) { case AtcControlStates.Released: RequestedBrakeNotch = -1; RequestedPowerNotch = -1; break; case AtcControlStates.NoPower: case AtcControlStates.NoPowerAlarm: RequestedBrakeNotch = -1; RequestedPowerNotch = 0; break; case AtcControlStates.BrakeEmergency: RequestedBrakeNotch = train.Specs.BrakeNotches + 1; RequestedPowerNotch = 0; break; } switch (DeviceState) { // ATC device is disabled (also called "Special Mode") case DeviceStates.Override: train.ContinuousProtection = false; train.VigilanceOverride = false; // Release control of the brakes and power AtcControlState = AtcControlStates.Released; break; // ATC device is initialized and a driving mode is required case DeviceStates.NoMode: train.ContinuousProtection = false; train.VigilanceOverride = false; // Apply service/emergency brake while no driving mode is selected AtcControlState = AtcControlStates.BrakeEmergency; // Update ATC speed code CurrentSpeedCode = new SpeedCode(new Speed(0), new Speed(0)); break; // ATC device is in YARD (M+25) driving mode case DeviceStates.YARD: train.ContinuousProtection = false; train.VigilanceOverride = false; // Apply brake if any door opens if (train.DoorState != DoorStates.None) { RequestedBrakeNotch = stopped ? train.Specs.BrakeNotches + 1 : train.ServiceBrakeNotch; } // If the train is not moving, brakes are released if (stopped) { AtcControlState = AtcControlStates.Released; } break; // ATC device is in ATP (M+ATP) driving mode case DeviceStates.ATP: train.ContinuousProtection = true; train.VigilanceOverride = false; CurrentSpeedCode = GetSpeedCodeFromAspect(); break; // ATC device is in ATO driving mode case DeviceStates.ATO: train.ContinuousProtection = true; train.VigilanceOverride = true; CurrentSpeedCode = GetSpeedCodeFromAspect(); break; } // Panel indicators train.Panel[11] = TrackState > TrackStates.Unprotected && train.PhysicalHandles.Reverser == 1 ? 1 : 0; train.Panel[12] = AtcControlState == AtcControlStates.NoPower || AtcControlState == AtcControlStates.NoPowerAlarm ? 1 : 0; train.Panel[13] = (int)DeviceState; train.Panel[14] = (int)CurrentSpeedCode.CurrentLimit.KilometersPerHour * 1000; train.Panel[15] = (int)CurrentSpeedCode.TargetLimit.KilometersPerHour * 1000; train.Panel[16] = (int)DeviceState > 1 ? 1 : 0; // Play alarm sound if (AtcControlState == AtcControlStates.NoPowerAlarm) { SoundManager.Play(AlarmSoundIndex, 1.0, 1.0, true); } // Stop alarm sound if (AtcControlState != AtcControlStates.NoPowerAlarm && SoundManager.Playing(AlarmSoundIndex)) { SoundManager.Stop(AlarmSoundIndex); } } /// Is called when the state of a key changes. /// The key. /// Whether the key is pressed or released. /// The current train. internal override void KeyChange(VirtualKeys key, bool pressed, Train train) { double speed = train.State.Speed.KilometersPerHour; bool stopped = speed < 0.05; if (pressed) { switch (key) { case VirtualKeys.I: // YARD (M+25) mode selection button if (!OpenbveFcmbTrainPlugin.KeysPressed[(int)key]) { // Allow change anytime the train is stopped or from ATP mode below YARD speed limit if ((DeviceState >= DeviceStates.NoMode && stopped) || (DeviceState == DeviceStates.ATP && train.State.Speed.KilometersPerHour < YardMaximumSpeed.KilometersPerHour)) { if (train.PhysicalHandles.Reverser == 1 || train.PhysicalHandles.Reverser == -1) { DeviceState = DeviceStates.YARD; // Update ATC speed code CurrentSpeedCode = new SpeedCode(YardMaximumSpeed, YardMaximumSpeed); } } } break; case VirtualKeys.J: // ATP (M+ATP) mode selection button if (!OpenbveFcmbTrainPlugin.KeysPressed[(int)key]) { // Allow change anytime the train is stopped or from ATO mode when running if ((DeviceState >= DeviceStates.NoMode && DeviceState <= DeviceStates.ATO && stopped) || DeviceState == DeviceStates.ATO) { if (train.PhysicalHandles.Reverser == 1 && TrackState > TrackStates.Unprotected) { DeviceState = DeviceStates.ATP; } } } break; case VirtualKeys.K: // ATO mode selection button if (!OpenbveFcmbTrainPlugin.KeysPressed[(int)key]) { // Allow change when the train is stopped in ATP mode if (DeviceState == DeviceStates.ATP && stopped && AtoAvailable) { if (train.PhysicalHandles.Reverser == 1 && TrackState > TrackStates.Unprotected) { DeviceState = DeviceStates.ATO; } } } break; case VirtualKeys.L: // ATO start button if (!OpenbveFcmbTrainPlugin.KeysPressed[(int)key]) { } break; case VirtualKeys.B1: // ATC alarm acknowledge if (!OpenbveFcmbTrainPlugin.KeysPressed[(int)key]) { if (AtcControlState == AtcControlStates.NoPowerAlarm) { AtcControlState = AtcControlStates.NoPower; } } break; case VirtualKeys.C1: // Override toggle if (!OpenbveFcmbTrainPlugin.KeysPressed[(int)key]) { switch (DeviceState) { case DeviceStates.NoMode: case DeviceStates.YARD: // Override device if possible if (stopped && train.PhysicalHandles.PowerNotch == 0) { CurrentSpeedCode = new SpeedCode(new Speed(0), new Speed(0)); DeviceState = DeviceStates.Override; } break; case DeviceStates.Override: // Disable override if possible if (stopped && train.PhysicalHandles.PowerNotch == 0) { DeviceState = DeviceStates.NoMode; } break; } } break; } } } /// Is called to inform about signals. /// The signal data. internal override void SetSignal(SignalData[] signal) { CurrentSignalAspect = signal[0].Aspect; CurrentSignalStartLocation = signal[0].Distance + TrainLocation; CurrentSignalEndLocation = signal.Length > 1 ? signal[1].Distance + TrainLocation : double.MaxValue; } /// Is called when a beacon is passed. /// The beacon data. internal override void SetBeacon(BeaconData beacon) { int type = beacon.Type; switch (type) { case -16777215: { if (!(beacon.Optional >= 0 & beacon.Optional <= 3)) { break; } TrackState = (TrackStates)beacon.Optional; return; } } } /// Is called when the device should perform the AI. /// The AI data. /// The current train. /// The current route. internal override void PerformAI(AIData data, Train train, Route route) { double speed = train.State.Speed.KilometersPerHour; bool stopped = speed < 0.05; // Set reverser to forward if (data.Handles.Reverser != 1) { data.Handles.Reverser = 1; data.Response = AIResponse.Short; } switch (DeviceState) { case DeviceStates.NoMode: // Apply brake data.Handles.BrakeNotch = train.Specs.BrakeNotches; data.Response = AIResponse.Short; // If the train is stopped, select YARD (M+25) mode if (stopped) { KeyChange(VirtualKeys.I, true, train); data.Response = AIResponse.Short; KeyChange(VirtualKeys.I, false, train); } break; case DeviceStates.YARD: // Select ATP (M+ATP) mode if the train is stopped and receiving ATC codes from the track if (stopped) { switch (TrackState) { case TrackStates.Enable: case TrackStates.ATC: KeyChange(VirtualKeys.J, true, train); data.Response = AIResponse.Short; KeyChange(VirtualKeys.J, false, train); break; } } CalculateAiNotches(data, train, route); break; case DeviceStates.ATP: // Select YARD (M+25) mode if the train is stopped and not receiving ATC codes from the track or a mode change is required if (stopped) { switch (TrackState) { case TrackStates.Disable: case TrackStates.Unprotected: KeyChange(VirtualKeys.I, true, train); data.Response = AIResponse.Short; KeyChange(VirtualKeys.I, false, train); break; } } CalculateAiNotches(data, train, route); break; } } /// Calculates the ideal notches for the AI driver. /// The AI data. /// The current train. /// The current route. private void CalculateAiNotches(AIData data, Train train, Route route) { double speed = train.State.Speed.KilometersPerHour; bool stopped = speed < 0.05; // Set AI notches to what the AI is doing right now AiBrakeNotch = data.Handles.BrakeNotch; AiPowerNotch = data.Handles.PowerNotch; // Calculate ideal notch for AI when approaching the speed limit if (speed > CurrentSpeedCode.WarningOn.KilometersPerHour - 2 && train.Acceleration > 0.1) { if (data.Handles.PowerNotch > 0) { AiPowerNotch -= 1; } else { AiBrakeNotch += 1; } } else if (speed < CurrentSpeedCode.WarningOff.KilometersPerHour - 2 && train.Acceleration < 0.25) { if (data.Handles.BrakeNotch > 0) { AiBrakeNotch -= 1; } else { AiPowerNotch += 1; } } // If still far from next station, take control of AI handles if (speed / (route.CurrentStation.StopPosition - train.State.Location) < 0.2) { if (speed > CurrentSpeedCode.CurrentLimit.KilometersPerHour - 5) { data.Handles.PowerNotch = AiPowerNotch; data.Handles.BrakeNotch = AiBrakeNotch; data.Response = AIResponse.Medium; } } } /// Generates the corresponding speed code from a signal aspect. private SpeedCode GetSpeedCodeFromAspect() { foreach (SignalCode signal in SignalCodes) { if (signal.Aspect == CurrentSignalAspect) { if (TrainLocation - TrainLength < CurrentSignalStartLocation && signal.EntryTarget.MetersPerSecond > 0) { // The train has not fully entered the signal section, use the entry target speed return new SpeedCode(signal.Limit, signal.EntryTarget, CurrentSignalEndLocation); } return new SpeedCode(signal.Limit, signal.Target, CurrentSignalEndLocation); } } return new SpeedCode(new Speed(0), new Speed(0)); } } }