openbve-fcmb-train-plugin/src/Devices/AtcDimetronic.cs

630 lines
28 KiB
C#

using System.Collections.Generic;
using System.Globalization;
using OpenBveApi.Runtime;
namespace OpenbveFcmbTrainPlugin
{
/// <summary>A device simulating ATC by Dimetronic.</summary>
internal class AtcDimetronic : Device
{
/// <summary>Represents the state of the device.</summary>
private enum DeviceStates
{
/// <summary>The device is being overriden.</summary>
Override,
/// <summary>The device has been initialized and no driving mode is selected.</summary>
NoMode,
/// <summary>The device is in YARD (M+25) driving mode.</summary>
YARD,
/// <summary>The device is in ATP (M+ATP) driving mode.</summary>
ATP,
/// <summary>The device is in ATO mode.</summary>
ATO,
}
/// <summary>The current state of the device.</summary>
private DeviceStates DeviceState;
/// <summary>Represents the state of the ATC control.</summary>
private enum AtcControlStates
{
/// <summary>The brakes are released.</summary>
Released,
/// <summary>The power is cut.</summary>
NoPower,
/// <summary>The power is cut and an alarm plays.</summary>
NoPowerAlarm,
/// <summary>The emergency brake is applied.</summary>
BrakeEmergency,
}
/// <summary>The current state of the ATC control.</summary>
private AtcControlStates AtcControlState;
/// <summary>Represents the state of the track.</summary>
private enum TrackStates
{
Unprotected,
Enable,
ATC,
Disable
}
/// <summary>The current state of the track.</summary>
private TrackStates TrackState;
/// <summary>The current position of the train.</summary>
private double TrainLocation;
/// <summary>The position of the train used to detect runback.</summary>
private double RunbackLocation;
/// <summary>The position of the train used to detect rollforward.</summary>
private double RollforwardLocation;
/// <summary>Whether rollforward should be detected or not.</summary>
private bool RollforwardTrigger;
/// <summary>The length of the train.</summary>
private readonly double TrainLength;
/// <summary>The maximum speed in YARD mode.</summary>
private readonly Speed YardMaximumSpeed;
/// <summary>The distance after which the runback protection is triggered.</summary>
private readonly double RunbackDistance;
/// <summary>The distance after which the rollforward protection is triggered.</summary>
private readonly double RollforwardDistance;
/// <summary>Whether ATO is available or not on the train.</summary>
private readonly bool AtoAvailable;
/// <summary>The index of the alarm sound.</summary>
private readonly int AlarmSoundIndex;
/// <summary>Represents an ATC speed code.</summary>
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)
{
}
/// <summary>Updates the ATC speed code.</summary>
/// <param name="distance">The distance to the end of the signalling block.</param>
internal void Update(double distance)
{
TargetDistance = distance;
}
}
/// <summary>The current speed code received by the train.</summary>
private SpeedCode CurrentSpeedCode;
/// <summary>Represents a signal code.</summary>
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);
}
}
}
}
}
}
}
/// <summary>The list of signal codes recognised by the device.</summary>
private readonly List<SignalCode> SignalCodes;
/// <summary>The aspect of the current signal in the route.</summary>
private int CurrentSignalAspect;
/// <summary>The start position of the current signal.</summary>
private double CurrentSignalStartLocation;
/// <summary>The end position of the current signal.</summary>
private double CurrentSignalEndLocation;
/// <summary>The ideal power notch for the AI.</summary>
private int AiPowerNotch;
/// <summary>The ideal brake notch for the AI.</summary>
private int AiBrakeNotch;
/// <summary>Creates an instance of the Bombardier ATC device.</summary>
/// <param name="trainLength">The length of the train, in meters.</param>
/// <param name="yardMaxSpeed">The maximum speed in YARD mode, in km/h.</param>
/// <param name="runbackDistance">The distance after which the runback protection is triggered, in meters.</param>
/// <param name="rollforwardDistance">The distance after which the rollforward protection is triggered, in meters.</param>
/// <param name="signalCodes">The list of signal codes recognised by the device.</param>
/// <param name="alarmSoundIndex">The index of the alarm sound.</param>
/// <param name="atoAvailable">Whether ATO is available or not.</param>
internal AtcDimetronic(double trainLength, Speed yardMaxSpeed, double runbackDistance, double rollforwardDistance, List<SignalCode> signalCodes, int alarmSoundIndex, bool atoAvailable)
{
TrainLength = trainLength;
YardMaximumSpeed = yardMaxSpeed;
RunbackDistance = runbackDistance;
RollforwardDistance = rollforwardDistance;
SignalCodes = signalCodes;
AlarmSoundIndex = alarmSoundIndex;
AtoAvailable = atoAvailable;
}
/// <summary>Is called when the device state should be updated.</summary>
/// <param name="train">The current train.</param>
/// <param name="route">The current route.</param>
/// <param name="init">Whether the device should initialize.</param>
/// <param name="elapsedTime">The time elapsed since the previous call.</param>
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);
}
}
/// <summary>Is called when the state of a key changes.</summary>
/// <param name="key">The key.</param>
/// <param name="pressed">Whether the key is pressed or released.</param>
/// <param name="train">The current train.</param>
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;
}
}
}
/// <summary>Is called to inform about signals.</summary>
/// <param name="signal">The signal data.</param>
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;
}
/// <summary>Is called when a beacon is passed.</summary>
/// <param name="beacon">The beacon data.</param>
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;
}
}
}
/// <summary>Is called when the device should perform the AI.</summary>
/// <param name="data">The AI data.</param>
/// <param name="train">The current train.</param>
/// <param name="route">The current route.</param>
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;
}
}
/// <summary>Calculates the ideal notches for the AI driver.</summary>
/// <param name="data">The AI data.</param>
/// <param name="train">The current train.</param>
/// <param name="route">The current route.</param>
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;
}
}
}
/// <summary>Generates the corresponding speed code from a signal aspect.</summary>
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));
}
}
}