diff --git a/Eu07.BlackBox.Views/BlackBoxStrings.cs b/Eu07.BlackBox.Views/BlackBoxStrings.cs
index c0474d4..f653599 100644
--- a/Eu07.BlackBox.Views/BlackBoxStrings.cs
+++ b/Eu07.BlackBox.Views/BlackBoxStrings.cs
@@ -111,8 +111,8 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings
["DevHeating"] = "Train heating",
["DevRadio"] = "Radio",
["DevShuntMode"] = "Shunting mode",
- ["DevDoorsLeft"] = "Left doors",
- ["DevDoorsRight"] = "Right doors",
+ ["DevDoorsLeft"] = "Left doors open",
+ ["DevDoorsRight"] = "Right doors open",
["DevSanding"] = "Sanding",
["DevHorn"] = "Horn",
["DevSpringBrake"] = "Spring brake",
@@ -201,6 +201,50 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings
["CabGroupDriving"] = "Driving",
["CabGroupSafety"] = "Safety",
["CabLightsWant"] = "{0}, should be {1}",
+ ["CabLightsNone"] = "no lamps",
+ ["CabBrakeSetting"] = "setting {0}",
+ ["CabEqualizing"] = "Equalizing reservoir",
+ ["CabFeedPipe"] = "Main reservoir pipe",
+ ["CabConsistCylinder"] = "Cylinders, train max",
+ ["CabPower"] = "Power",
+ ["CabSpeedControl"] = "Cruise control",
+ ["CabOilPressure"] = "Oil pressure",
+ ["CabWater"] = "Water",
+ ["CabOil"] = "Oil",
+ ["CabEngine"] = "Engine",
+ ["CabNoDrive"] = "no drive of its own",
+ ["CabClutch"] = "Clutch",
+ ["CabConverterFill"] = "Torque converter",
+ ["CabIntegrated"] = "Driving handle",
+ ["CabGroupDoors"] = "Doors",
+ ["DevDoorPermitLeft"] = "Left doors permitted",
+ ["DevDoorPermitRight"] = "Right doors permitted",
+ ["DevDepartureSignal"] = "Departure signal",
+ ["DevDoorStep"] = "Steps",
+ ["DevDoorLock"] = "Door lock",
+ ["DevDoorsLocked"] = "Doors locked",
+ ["DevBrakeReleaser"] = "Brake releaser",
+ ["DevFuelPump"] = "Fuel pump",
+ ["DevOilPump"] = "Oil pump",
+ ["DevMotorBlowers"] = "Motor fans",
+ ["DevEngineHot"] = "Engine overheating",
+ ["DevSpeedControl"] = "Cruise control",
+ ["DevRadioChannel"] = "Radio, channel {0}",
+ ["DevHandBrake"] = "Hand brake",
+ ["DevHornTone"] = "Horn ({0})",
+ ["HornLow"] = "low",
+ ["HornHigh"] = "high",
+ ["HornBoth"] = "both",
+ ["EventDeviceOn"] = "{0}: on",
+ ["EventDeviceOff"] = "{0}: off",
+ ["EventDoorsLeftClosed"] = "Left doors closed",
+ ["EventDoorsRightClosed"] = "Right doors closed",
+ ["EventMovingDoorsOpen"] = "Moving with the doors open ({0:0} km/h)",
+ ["EventHandBrakeOn"] = "Hand brake applied",
+ ["EventHandBrakeOff"] = "Hand brake released",
+ ["EventBrakeSetting"] = "Brake setting: {0}",
+ ["EventRadioChannel"] = "Radio: channel {0}",
+ ["EventSpeedControlSet"] = "Cruise control: {0:0} km/h",
["DevVigilance"] = "CA",
["DevCabSignal"] = "SHP",
["DevSecurityBraking"] = "CA/SHP braking",
@@ -338,8 +382,8 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings
["DevHeating"] = "Ogrzewanie",
["DevRadio"] = "Radiotelefon",
["DevShuntMode"] = "Jazda manewrowa",
- ["DevDoorsLeft"] = "Drzwi lewe",
- ["DevDoorsRight"] = "Drzwi prawe",
+ ["DevDoorsLeft"] = "Drzwi lewe otwarte",
+ ["DevDoorsRight"] = "Drzwi prawe otwarte",
["DevSanding"] = "Piasecznica",
["DevHorn"] = "Syrena",
["DevSpringBrake"] = "Hamulec sprężynowy",
@@ -428,6 +472,50 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings
["CabGroupDriving"] = "Jazda",
["CabGroupSafety"] = "Bezpieczeństwo",
["CabLightsWant"] = "{0}, a ma być {1}",
+ ["CabLightsNone"] = "brak lamp",
+ ["CabBrakeSetting"] = "nastawa {0}",
+ ["CabEqualizing"] = "Zbiornik czasowy",
+ ["CabFeedPipe"] = "Przewód zasilający",
+ ["CabConsistCylinder"] = "Cylindry, skład max",
+ ["CabPower"] = "Moc",
+ ["CabSpeedControl"] = "Tempomat",
+ ["CabOilPressure"] = "Ciśn. oleju",
+ ["CabWater"] = "Woda",
+ ["CabOil"] = "Olej",
+ ["CabEngine"] = "Silnik",
+ ["CabNoDrive"] = "brak własnego napędu",
+ ["CabClutch"] = "Sprzęgło",
+ ["CabConverterFill"] = "Przekładnia hydr.",
+ ["CabIntegrated"] = "Zadajnik",
+ ["CabGroupDoors"] = "Drzwi",
+ ["DevDoorPermitLeft"] = "Zezwolenie lewe",
+ ["DevDoorPermitRight"] = "Zezwolenie prawe",
+ ["DevDepartureSignal"] = "Sygnał odjazdu",
+ ["DevDoorStep"] = "Stopień",
+ ["DevDoorLock"] = "Blokada drzwi",
+ ["DevDoorsLocked"] = "Drzwi zablokowane",
+ ["DevBrakeReleaser"] = "Odluźniacz",
+ ["DevFuelPump"] = "Pompa paliwa",
+ ["DevOilPump"] = "Pompa oleju",
+ ["DevMotorBlowers"] = "Wentylatory silników",
+ ["DevEngineHot"] = "Przegrzanie silnika",
+ ["DevSpeedControl"] = "Tempomat",
+ ["DevRadioChannel"] = "Radiotelefon, kanał {0}",
+ ["DevHandBrake"] = "Hamulec ręczny",
+ ["DevHornTone"] = "Syrena ({0})",
+ ["HornLow"] = "niski",
+ ["HornHigh"] = "wysoki",
+ ["HornBoth"] = "oba",
+ ["EventDeviceOn"] = "{0}: wł.",
+ ["EventDeviceOff"] = "{0}: wył.",
+ ["EventDoorsLeftClosed"] = "Drzwi lewe zamknięte",
+ ["EventDoorsRightClosed"] = "Drzwi prawe zamknięte",
+ ["EventMovingDoorsOpen"] = "Jazda z otwartymi drzwiami ({0:0} km/h)",
+ ["EventHandBrakeOn"] = "Hamulec ręczny zaciągnięty",
+ ["EventHandBrakeOff"] = "Hamulec ręczny zwolniony",
+ ["EventBrakeSetting"] = "Nastawa hamulca: {0}",
+ ["EventRadioChannel"] = "Radiotelefon: kanał {0}",
+ ["EventSpeedControlSet"] = "Tempomat: {0:0} km/h",
["DevVigilance"] = "CA",
["DevCabSignal"] = "SHP",
["DevSecurityBraking"] = "Hamowanie CA/SHP",
diff --git a/Eu07.BlackBox.Views/BlackBoxWindow.cs b/Eu07.BlackBox.Views/BlackBoxWindow.cs
index 1ba8362..2f09c9b 100644
--- a/Eu07.BlackBox.Views/BlackBoxWindow.cs
+++ b/Eu07.BlackBox.Views/BlackBoxWindow.cs
@@ -178,7 +178,7 @@ public sealed class BlackBoxWindow : Window
cabCard.Margin = new Thickness(0, UiKit.Gap, 0, 0);
var sideColumn = new DockPanel();
// On a short window the cab scrolls, so the list above keeps room for a few rows.
- sideColumn.SizeChanged += (_, e) => cabCard.MaxHeight = Math.Max(160, e.NewSize.Height - 200);
+ sideColumn.SizeChanged += (_, e) => cabCard.MaxHeight = Math.Max(160, e.NewSize.Height - 160);
DockPanel.SetDock(cabCard, Dock.Bottom);
sideColumn.Children.Add(cabCard);
sideColumn.Children.Add(UiKit.Frame(side));
diff --git a/Eu07.BlackBox.Views/CabPanel.cs b/Eu07.BlackBox.Views/CabPanel.cs
index 26c365c..205a14a 100644
--- a/Eu07.BlackBox.Views/CabPanel.cs
+++ b/Eu07.BlackBox.Views/CabPanel.cs
@@ -9,26 +9,32 @@ using Avalonia.Media;
namespace Eu07.BlackBox.Views;
///
-/// The cab of the user's train at the moment played: the speed against the AI's, the
-/// accelerations, the controllers, the pressures of the brakes as gauges, the traction,
-/// the lights at the head and the tail of the train with what they are to show, and a
-/// lamp for every device in three groups (power, driving, safety), the alerter (CA) and
-/// the cab signalling (SHP) apart.
+/// The cab of the user's train at the moment played, for checking what the driver did: the
+/// speed against the AI's, the accelerations, the controllers, the brakes (the pressures as
+/// gauges, the setting), the traction as fits what drives the vehicle (the voltage and the
+/// current of an electric one, the oil and the temperatures of a diesel), the lights at the
+/// head and the tail of the train with what they are to show, and a lamp for every device in
+/// groups (power, driving, safety, doors), the alerter (CA) and the cab signalling (SHP) apart.
///
public sealed class CabPanel : Control
{
private const double Pad = 14;
- private const double LampRow = 15;
+ private const double LampRow = 14;
+ private const int DoorColumns = 3;
private Recording? _recording;
private double _time;
private int _measured = -1;
private int _controllerTop = 1;
+ private readonly HashSet _powered = new();
+ private readonly HashSet _feedPipe = new();
+ private readonly HashSet _integrated = new();
+ private readonly HashSet _hydraulic = new();
public CabPanel()
{
ClipToBounds = true;
- Height = 506;
+ Height = 650;
}
public IBlackBoxStrings Strings { get; set; } = new DefaultBlackBoxStrings();
@@ -67,6 +73,7 @@ public sealed class CabPanel : Control
}
Measure(recording);
var culture = CultureInfo.CurrentCulture;
+ var extra = s.Extra;
// The vehicle at the left; who drives, from which cab, at the right.
double chipX = right;
@@ -105,37 +112,64 @@ public sealed class CabPanel : Control
double column = width / 4;
Value(context, Strings["CabController"], s.MainController.ToString(culture), Pad, top);
Steps(context, s.MainController, _controllerTop, Pad, top + 34, column - 14);
- Value(context, Strings["CabShunt"], s.ScndController.ToString(culture), Pad + column, top);
+ if (extra is { } handle && _integrated.Contains(s.Vehicle))
+ Value(context, Strings["CabIntegrated"], (handle.IntegratedController * 100).ToString("+0;−0;0", culture) + " %", Pad + column, top);
+ else
+ Value(context, Strings["CabShunt"], s.ScndController.ToString(culture), Pad + column, top);
Value(context, Strings["CabReverser"], Strings[s.Reverser > 0 ? "CabForward" : s.Reverser < 0 ? "CabBackward" : "CabNeutral"], Pad + 2 * column, top);
Value(context, Strings["CabTrainBrake"], s.TrainBrake.ToString("0.0", culture), Pad + 3 * column, top);
- // The brakes, as gauges.
- top = Section(context, Strings["CabSectionBrakes"], Pad, 180, right);
+ // The brakes, as gauges, the setting (G, P, R) at the right of the title.
+ string? setting = extra is { } e && (e.BrakeDelay | e.BrakeMode) != 0
+ ? string.Format(culture, Strings["CabBrakeSetting"], string.Join(" · ", new[] { BrakeSetting(e.BrakeDelay), BrakeMode(e.BrakeMode) }.Where(t => t.Length > 0)))
+ : null;
+ top = Section(context, Strings["CabSectionBrakes"], Pad, 180, right, setting);
+ const double gauge = 17;
Gauge(context, Strings["CabPipe"], s.BrakePipe, 6, 5, $"{s.BrakePipe:0.00} bar", Palette.Station, Pad, top, width);
- Gauge(context, Strings["CabCylinder"], s.BrakeCylinder, 6, null, $"{s.BrakeCylinder:0.0} bar", Palette.Brakes, Pad, top + 18, width);
- Gauge(context, Strings["CabReservoir"], s.MainReservoir, 10, null, $"{s.MainReservoir:0.0} bar", Palette.Controller, Pad, top + 36, width);
- Gauge(context, Strings["CabLocalBrake"], s.LocalBrake, 1, null, $"{s.LocalBrake * 100:0} %", Palette.Brakes, Pad, top + 54, width);
+ double y = top + gauge;
+ if (extra is { } x1)
+ {
+ // where the driver's brake valve takes the pipe
+ Gauge(context, Strings["CabEqualizing"], x1.EqualizingReservoir, 6, null, $"{x1.EqualizingReservoir:0.00} bar", Palette.Target, Pad, y, width);
+ y += gauge;
+ }
+ Gauge(context, Strings["CabCylinder"], s.BrakeCylinder, 6, null, $"{s.BrakeCylinder:0.0} bar", Palette.Brakes, Pad, y, width);
+ y += gauge;
+ if (extra is { } x3)
+ {
+ // the most braked vehicle of the train
+ Gauge(context, Strings["CabConsistCylinder"], x3.ConsistBrakeCylinder, 6, null, $"{x3.ConsistBrakeCylinder:0.0} bar", Palette.Brakes, Pad, y, width);
+ y += gauge;
+ }
+ Gauge(context, Strings["CabReservoir"], s.MainReservoir, 10, null, $"{s.MainReservoir:0.0} bar", Palette.Controller, Pad, y, width);
+ y += gauge;
+ if (extra is { } x2)
+ {
+ bool has = _feedPipe.Contains(s.Vehicle);
+ Gauge(context, Strings["CabFeedPipe"], has ? x2.FeedPipe : 0, 10, null, has ? $"{x2.FeedPipe:0.0} bar" : "—", Palette.Controller, Pad, y, width);
+ y += gauge;
+ }
+ Gauge(context, Strings["CabLocalBrake"], s.LocalBrake, 1, null, $"{s.LocalBrake * 100:0} %", Palette.Brakes, Pad, y, width);
- // The traction at the left, the lights of both ends of the train at the right, one under the other.
+ // The traction at the left, as fits what drives the vehicle; the lights of both ends of the train at the right.
double half = Pad + width / 2;
- top = Section(context, Strings["CabSectionDrive"], Pad, 272, half - 10);
- Section(context, Strings["CabLights"], half + 4, 272, right);
- double third = (half - 10 - Pad) / 3;
- Value(context, Strings["CabVoltage"], $"{s.Voltage:0} V", Pad, top);
- Value(context, Strings["CabCurrent"], $"{s.MotorCurrent:0} A", Pad + third, top);
- Value(context, Strings["CabRpm"], $"{s.EngineRpm:0}", Pad + 2 * third, top);
+ double driveTop = y + gauge + 4;
+ top = Section(context, Strings["CabSectionDrive"], Pad, driveTop, half - 10);
+ Section(context, Strings["CabLights"], half + 4, driveTop, right);
+ DrawDrive(context, s, top, (half - 10 - Pad) / 3, culture);
double row = top;
- foreach (var (key, lights, required, isRight) in new[]
+ foreach (var (key, lights, equipped, required, isRight) in new[]
{
- ("CabLightsHead", s.LightsHead, HeadCodes.RequiredHead(s), HeadCodes.HeadRight(s)),
- ("CabLightsTail", s.LightsTail, HeadCodes.RequiredTail(s), HeadCodes.TailRight(s))
+ ("CabLightsHead", s.LightsHead, s.EquippedHead, HeadCodes.RequiredHead(s), HeadCodes.HeadRight(s)),
+ ("CabLightsTail", s.LightsTail, s.EquippedTail, HeadCodes.RequiredTail(s), HeadCodes.TailRight(s))
})
{
DrawFace(context, lights, new Point(half + 4 + 17, row + 15));
// the end, and under it the signal it shows: green as it is to be (with the lamps the vehicle has),
// red otherwise with the one it is to show
var shown = HeadCodes.Of(lights);
- var text = isRight == false && required is { } want
+ var text = HeadCodes.NoLamps(equipped) ? Strings["CabLightsNone"]
+ : isRight == false && required is { } want
? string.Format(culture, Strings["CabLightsWant"], HeadCodes.Name(shown), HeadCodes.Name(want))
: isRight == true && required is { } met ? HeadCodes.Name(met) : HeadCodes.Name(shown);
var brush = isRight is null ? Palette.DimText : isRight.Value ? Palette.Good : Palette.Bad;
@@ -148,63 +182,187 @@ public sealed class CabPanel : Control
}
// The devices, in three groups side by side: lit as their lamps are in the cab, the ones off faint.
- top = Section(context, Strings["CabSectionDevices"], Pad, 350, right);
- var groups = Devices(s);
+ top = Section(context, Strings["CabSectionDevices"], Pad, top + 76, right);
+ var groups = Devices(s, culture);
var widths = new double[groups.Length];
for (int g = 0; g < groups.Length; g++)
{
widths[g] = MapView.Text(Strings[groups[g].Title], Palette.DimText, 10, FontWeight.SemiBold).Width;
- foreach (var (key, _) in groups[g].Lamps)
- widths[g] = Math.Max(widths[g], 14 + MapView.Text(Strings[key], Palette.Text, 10.5).Width);
+ foreach (var (text, _) in groups[g].Lamps)
+ widths[g] = Math.Max(widths[g], 14 + MapView.Text(text, Palette.Text, 10.5).Width);
}
// each group as wide as its longest name, what is left shared between them
double spare = (width - widths.Sum()) / groups.Length;
- double x = Pad;
+ double x = Pad, bottom = top;
for (int g = 0; g < groups.Length; g++)
{
double groupWidth = Math.Max(20, widths[g] + spare);
using (context.PushClip(new Rect(x, top, groupWidth, size.Height - top)))
{
context.DrawText(MapView.Text(Strings[groups[g].Title], Palette.DimText, 10, FontWeight.SemiBold), new Point(x, top));
- double y = top + 16;
- foreach (var (key, brush) in groups[g].Lamps)
+ double lampY = top + 16;
+ foreach (var (text, brush) in groups[g].Lamps)
{
- Led(context, new Point(x + 4, y + 7), brush);
- context.DrawText(MapView.Text(Strings[key], brush is null ? Palette.FaintText : Palette.Text, 10.5), new Point(x + 14, y));
- y += LampRow;
+ Lamp(context, text, brush, x, lampY);
+ lampY += LampRow;
}
+ bottom = Math.Max(bottom, lampY);
}
x += groupWidth;
}
+
+ // The doors under them, across: open, permitted, the departure signal, the steps and the lock.
+ top = Math.Max(bottom, top + 16 + 9 * LampRow) + 6;
+ context.DrawText(MapView.Text(Strings["CabGroupDoors"], Palette.DimText, 10, FontWeight.SemiBold), new Point(Pad, top));
+ var doors = Doors(s);
+ double doorColumn = width / DoorColumns;
+ for (int i = 0; i < doors.Length; i++)
+ {
+ double doorX = Pad + i % DoorColumns * doorColumn;
+ using (context.PushClip(new Rect(doorX, top, doorColumn, size.Height - top)))
+ Lamp(context, doors[i].Text, doors[i].Lit, doorX, top + 16 + i / DoorColumns * LampRow);
+ }
}
- /// The title of a part of the panel, with a line from it to the right; the top of the part returned.
- private static double Section(DrawingContext context, string title, double x, double y, double right)
+ ///
+ /// The traction, in two rows of three, as fits what drives the vehicle: the voltage, the current and the
+ /// revolutions of an electric one, the power and the cruise control under; the revolutions and the oil
+ /// pressure of a diesel, its temperatures under. A recording made before the engine was written: the first
+ /// row of an electric one, no voltage for a vehicle which never had any.
+ ///
+ private void DrawDrive(DrawingContext context, CabSample s, double top, double third, CultureInfo culture)
+ {
+ void At(int column, int row, string label, string value, IBrush? brush = null) =>
+ Value(context, label, value, Pad + column * third, top + row * 36, brush);
+
+ string voltage = _powered.Contains(s.Vehicle) ? $"{s.Voltage:0} V" : "—";
+ if (s.Extra is not { } e)
+ {
+ At(0, 0, Strings["CabVoltage"], voltage);
+ At(1, 0, Strings["CabCurrent"], $"{s.MotorCurrent:0} A");
+ At(2, 0, Strings["CabRpm"], $"{s.EngineRpm:0}");
+ return;
+ }
+ string oil = string.Format(culture, "{0:0.00} MPa", e.OilPressure);
+ switch (e.Engine)
+ {
+ case EngineKind.Electric:
+ case EngineKind.ElectricInduction:
+ At(0, 0, Strings["CabVoltage"], voltage);
+ At(1, 0, Strings["CabCurrent"], $"{s.MotorCurrent:0} A");
+ At(2, 0, Strings["CabRpm"], $"{s.EngineRpm:0}");
+ At(0, 1, Strings["CabPower"], $"{e.EnginePower:0} kW");
+ At(1, 1, Strings["CabSpeedControl"], s.Has(CabDevices.SpeedControl) ? $"{e.SpeedControlKmh:0} km/h" : "—");
+ break;
+ case EngineKind.DieselElectric:
+ case EngineKind.Diesel:
+ At(0, 0, Strings["CabRpm"], $"{s.EngineRpm:0}");
+ if (e.Engine == EngineKind.DieselElectric)
+ At(1, 0, Strings["CabCurrent"], $"{s.MotorCurrent:0} A");
+ else if (_hydraulic.Contains(s.Vehicle))
+ At(1, 0, Strings["CabConverterFill"], $"{e.ConverterFill * 100:0} %");
+ else
+ At(1, 0, Strings["CabClutch"], $"{e.Clutch * 100:0} %");
+ At(2, 0, Strings["CabOilPressure"], oil);
+ // the temperatures, warm when the engine is too hot
+ var heat = s.Has(CabDevices.EngineHot) ? Palette.Bad : null;
+ At(0, 1, Strings["CabWater"], $"{e.WaterTemperature:0} °C", heat);
+ At(1, 1, Strings["CabOil"], $"{e.OilTemperature:0} °C", heat);
+ At(2, 1, Strings["CabEngine"], $"{e.EngineTemperature:0} °C", heat);
+ break;
+ case EngineKind.Steam:
+ At(0, 0, Strings["CabRpm"], $"{s.EngineRpm:0}");
+ break;
+ default:
+ context.DrawText(MapView.Text(Strings["CabNoDrive"], Palette.DimText, 11), new Point(Pad, top + 2));
+ break;
+ }
+ }
+
+ /// The setting of the brakes from its bits: G, P, R, R+Mg.
+ public static string BrakeSetting(byte bits)
+ {
+ var parts = new List();
+ if ((bits & 1) != 0)
+ parts.Add("G");
+ if ((bits & 2) != 0)
+ parts.Add("P");
+ if ((bits & 4) != 0)
+ parts.Add("R");
+ if ((bits & 8) != 0)
+ parts.Add("Mg");
+ return string.Join("+", parts);
+ }
+
+ /// The mode of the brakes from its bits: PS, PN, EP, MED.
+ public static string BrakeMode(byte bits)
+ {
+ var parts = new List();
+ if ((bits & 1) != 0)
+ parts.Add("PS");
+ if ((bits & 2) != 0)
+ parts.Add("PN");
+ if ((bits & 4) != 0)
+ parts.Add("EP");
+ if ((bits & 8) != 0)
+ parts.Add("MED");
+ return string.Join("+", parts);
+ }
+
+ /// The title of a part of the panel, with a line from it to the right (and a note at its end); the top of the part returned.
+ private static double Section(DrawingContext context, string title, double x, double y, double right, string? note = null)
{
var text = MapView.Text(title.ToUpper(CultureInfo.CurrentCulture), Palette.DimText, 9, FontWeight.SemiBold);
context.DrawText(text, new Point(x, y));
+ double end = right;
+ if (note is not null)
+ {
+ var noted = MapView.Text(note, Palette.Text, 10, FontWeight.SemiBold);
+ end = right - noted.Width - 8;
+ context.DrawText(noted, new Point(right - noted.Width, y + (text.Height - noted.Height) / 2));
+ }
double line = Math.Round(y + text.Height / 2) + 0.5;
- if (x + text.Width + 8 < right)
- context.DrawLine(new Pen(Palette.Grid, 1), new Point(x + text.Width + 8, line), new Point(right, line));
+ if (x + text.Width + 8 < end)
+ context.DrawLine(new Pen(Palette.Grid, 1), new Point(x + text.Width + 8, line), new Point(end, line));
return y + 17;
}
- /// The highest step of the controller used in the recording, for its bar.
+ ///
+ /// What is the same all the recording long for each vehicle: the highest step of the controller used, for
+ /// its bar, and which vehicles ever had a voltage, a main reservoir pipe, an integrated controller moved off
+ /// nought and a torque converter filled, to show the field shunting, the clutch or none for the others.
+ ///
private void Measure(Recording recording)
{
if (_measured == recording.Revision)
return;
int top = 1;
+ _powered.Clear();
+ _feedPipe.Clear();
+ _integrated.Clear();
+ _hydraulic.Clear();
foreach (var sample in recording.Cab)
+ {
top = Math.Max(top, sample.MainController);
+ if (sample.Voltage > 1)
+ _powered.Add(sample.Vehicle);
+ if (sample.Extra is not { } e)
+ continue;
+ if (e.FeedPipe > 0.1f)
+ _feedPipe.Add(sample.Vehicle);
+ if (Math.Abs(e.IntegratedController) > 0.01f)
+ _integrated.Add(sample.Vehicle);
+ if (e.ConverterFill > 0.01f)
+ _hydraulic.Add(sample.Vehicle);
+ }
_controllerTop = top;
_measured = recording.Revision;
}
- private static void Value(DrawingContext context, string label, string value, double x, double y)
+ private static void Value(DrawingContext context, string label, string value, double x, double y, IBrush? brush = null)
{
context.DrawText(MapView.Text(label, Palette.DimText, 10), new Point(x, y));
- context.DrawText(MapView.Text(value, Palette.Text, 14, FontWeight.SemiBold), new Point(x, y + 13));
+ context.DrawText(MapView.Text(value, brush ?? Palette.Text, 14, FontWeight.SemiBold), new Point(x, y + 13));
}
/// The steps of the controller as a row of cells, lit up to the one it stands at.
@@ -269,14 +427,14 @@ public sealed class CabPanel : Control
{
context.DrawRectangle(null, new Pen(Palette.Grid, 1), new Rect(center.X - 17, center.Y - 15, 34, 22), 5, 5);
bool rearEnd = lights.HasFlag(Lights.RearEndSignals);
- Lamp(context, center + new Vector(0, -8), lights.HasFlag(Lights.HeadlightUpper), false, false);
- Lamp(context, center + new Vector(-10, 1), lights.HasFlag(Lights.HeadlightLeft) || lights.HasFlag(Lights.AuxiliaryLeft),
- lights.HasFlag(Lights.RedMarkerLeft) || rearEnd, lights.HasFlag(Lights.HighBeamLeft));
- Lamp(context, center + new Vector(10, 1), lights.HasFlag(Lights.HeadlightRight) || lights.HasFlag(Lights.AuxiliaryRight),
- lights.HasFlag(Lights.RedMarkerRight) || rearEnd, lights.HasFlag(Lights.HighBeamRight));
+ Light(context, center + new Vector(0, -8), lights.HasFlag(Lights.HeadlightUpper), false, false);
+ Light(context, center + new Vector(-10, 1), lights.HasFlag(Lights.HeadlightLeft) || lights.HasFlag(Lights.AuxiliaryLeft),
+ lights.HasFlag(Lights.RedMarkerLeft) || rearEnd, lights.HasFlag(Lights.HighBeamLeft));
+ Light(context, center + new Vector(10, 1), lights.HasFlag(Lights.HeadlightRight) || lights.HasFlag(Lights.AuxiliaryRight),
+ lights.HasFlag(Lights.RedMarkerRight) || rearEnd, lights.HasFlag(Lights.HighBeamRight));
}
- private static void Lamp(DrawingContext context, Point center, bool white, bool red, bool high)
+ private static void Light(DrawingContext context, Point center, bool white, bool red, bool high)
{
var fill = white ? Palette.LightWhite : red ? Palette.LightRed : Palette.LampOff;
double radius = high ? 4.5 : 3.5;
@@ -285,55 +443,114 @@ public sealed class CabPanel : Control
context.DrawEllipse(fill, null, center, radius, radius);
}
- private static void Led(DrawingContext context, Point center, IBrush? lit)
+ /// A device's lamp and its name: lit in its colour, the name bright; off, both faint.
+ private static void Lamp(DrawingContext context, string text, IBrush? lit, double x, double y)
{
+ var center = new Point(x + 4, y + 7);
if (lit is not null)
context.DrawEllipse(Palette.Faded(lit, 0.3), null, center, 6, 6);
context.DrawEllipse(lit ?? Palette.LampOff, null, center, 3.5, 3.5);
+ context.DrawText(MapView.Text(text, lit is null ? Palette.FaintText : Palette.Text, 10.5), new Point(x + 14, y));
}
- /// Every device, with the colour of its lamp if it was on, in the groups the panel shows.
- private static (string Title, (string Key, IBrush? Lit)[] Lamps)[] Devices(CabSample s)
+ ///
+ /// Every device but the doors, with the colour of its lamp if it was on, in the groups the panel shows. A
+ /// diesel has its pumps where an electric vehicle has its pantographs; a recording made before the engine
+ /// was written has the devices it had then.
+ ///
+ private (string Title, (string Text, IBrush? Lit)[] Lamps)[] Devices(CabSample s, CultureInfo culture)
{
- IBrush? On(CabDevices device, IBrush colour) => s.Has(device) ? colour : null;
- IBrush? Flag(CabFlags flag, IBrush colour) => (s.Flags & flag) != 0 ? colour : null;
+ (string, IBrush?) On(string key, CabDevices device, IBrush colour) => (Strings[key], s.Has(device) ? colour : null);
+ (string, IBrush?) Flag(string key, CabFlags flag, IBrush colour) => (Strings[key], (s.Flags & flag) != 0 ? colour : null);
IBrush? Security(SecurityFlags flag, IBrush colour) => s.Has(flag) ? colour : null;
+ var extra = s.Extra;
+ bool diesel = extra is { Engine: EngineKind.Diesel or EngineKind.DieselElectric };
+ (string, IBrush?) Horn()
+ {
+ if (!s.Has(CabDevices.Horn))
+ return (Strings["DevHorn"], null);
+ string? tone = extra?.Horn switch
+ {
+ 1 => Strings["HornLow"],
+ 2 => Strings["HornHigh"],
+ 3 => Strings["HornBoth"],
+ _ => null
+ };
+ return (tone is null ? Strings["DevHorn"] : string.Format(culture, Strings["DevHornTone"], tone), Palette.LampWarning);
+ }
+ bool electric = extra is null or { Engine: EngineKind.Electric or EngineKind.ElectricInduction };
- var power = new[]
+ var power = new List<(string, IBrush?)> { On("DevBattery", CabDevices.Battery, Palette.LampOn), On("DevMainBreaker", CabDevices.MainBreaker, Palette.LampOn) };
+ if (electric)
{
- ("DevBattery", On(CabDevices.Battery, Palette.LampOn)),
- ("DevMainBreaker", On(CabDevices.MainBreaker, Palette.LampOn)),
- ("DevPantographFront", On(CabDevices.PantographFront, Palette.LampOn)),
- ("DevPantographRear", On(CabDevices.PantographRear, Palette.LampOn)),
- ("DevConverter", On(CabDevices.Converter, Palette.LampOn)),
- ("DevCompressor", On(CabDevices.Compressor, Palette.LampOn)),
- ("DevLineContactors", On(CabDevices.LineContactors, Palette.LampOn)),
- ("DevHeating", On(CabDevices.Heating, Palette.LampOn))
- };
- var driving = new[]
+ power.Add(On("DevPantographFront", CabDevices.PantographFront, Palette.LampOn));
+ power.Add(On("DevPantographRear", CabDevices.PantographRear, Palette.LampOn));
+ }
+ if (diesel)
{
- ("DevShuntMode", On(CabDevices.ShuntMode, Palette.LampOn)),
- ("DevRadio", On(CabDevices.Radio, Palette.LampOn)),
- ("DevSanding", On(CabDevices.Sanding, Palette.LampWarning)),
- ("DevHorn", On(CabDevices.Horn, Palette.LampWarning)),
- ("DevDoorsLeft", On(CabDevices.DoorsLeft, Palette.LampWarning)),
- ("DevDoorsRight", On(CabDevices.DoorsRight, Palette.LampWarning)),
- ("DevSpringBrake", On(CabDevices.SpringBrake, Palette.LampWarning))
+ power.Add(On("DevFuelPump", CabDevices.FuelPump, Palette.LampOn));
+ power.Add(On("DevOilPump", CabDevices.OilPump, Palette.LampOn));
+ }
+ power.Add(On("DevConverter", CabDevices.Converter, Palette.LampOn));
+ power.Add(On("DevCompressor", CabDevices.Compressor, Palette.LampOn));
+ power.Add(On("DevLineContactors", CabDevices.LineContactors, Palette.LampOn));
+ if (extra is not null)
+ power.Add(On("DevMotorBlowers", CabDevices.MotorBlowers, Palette.LampOn));
+ power.Add(On("DevHeating", CabDevices.Heating, Palette.LampOn));
+
+ // the radio with its channel
+ var radio = extra is { RadioChannel: > 0 } r
+ ? (string.Format(culture, Strings["DevRadioChannel"], r.RadioChannel), s.Has(CabDevices.Radio) ? Palette.LampOn : null)
+ : On("DevRadio", CabDevices.Radio, Palette.LampOn);
+ var driving = new List<(string, IBrush?)>
+ {
+ On("DevShuntMode", CabDevices.ShuntMode, Palette.LampOn), radio,
+ On("DevSanding", CabDevices.Sanding, Palette.LampWarning), Horn(),
+ On("DevSpringBrake", CabDevices.SpringBrake, Palette.LampWarning)
};
+ if (extra is not null)
+ {
+ driving.Add((Strings["DevHandBrake"], extra.Value.HandBrake > 0 ? Palette.LampWarning : null));
+ driving.Add(On("DevBrakeReleaser", CabDevices.BrakeReleaser, Palette.LampWarning));
+ driving.Add(On("DevSpeedControl", CabDevices.SpeedControl, Palette.LampOn));
+ }
+
var safety = new List<(string, IBrush?)>();
if (s.Security != SecurityFlags.None || s.CabEnd != 0)
{
// A recording which tells them apart.
- safety.Add(("DevVigilance", Security(SecurityFlags.VigilanceBeeping, Palette.LampAlarm) ?? Security(SecurityFlags.Vigilance, Palette.Vigilance)));
- safety.Add(("DevCabSignal", Security(SecurityFlags.CabSignalBeeping, Palette.LampAlarm) ?? Security(SecurityFlags.CabSignal, Palette.CabSignal)));
- safety.Add(("DevSecurityBraking", Security(SecurityFlags.Braking, Palette.LampAlarm)));
+ safety.Add((Strings["DevVigilance"], Security(SecurityFlags.VigilanceBeeping, Palette.LampAlarm) ?? Security(SecurityFlags.Vigilance, Palette.Vigilance)));
+ safety.Add((Strings["DevCabSignal"], Security(SecurityFlags.CabSignalBeeping, Palette.LampAlarm) ?? Security(SecurityFlags.CabSignal, Palette.CabSignal)));
+ safety.Add((Strings["DevSecurityBraking"], Security(SecurityFlags.Braking, Palette.LampAlarm)));
}
else
- safety.Add(("DevAlerter", Flag(CabFlags.AlerterBeeping, Palette.LampAlarm) ?? Flag(CabFlags.AlerterBlinking, Palette.LampWarning)));
- safety.Add(("DevWheelSlip", Flag(CabFlags.WheelSlip, Palette.LampAlarm)));
- safety.Add(("DevOverloadRelay", On(CabDevices.OverloadRelay, Palette.LampAlarm)));
- safety.Add(("DevRadioStop", Flag(CabFlags.RadioStop, Palette.LampAlarm)));
- safety.Add(("DevAlarmChain", Flag(CabFlags.AlarmChain, Palette.LampAlarm)));
- return [("CabGroupPower", power), ("CabGroupDriving", driving), ("CabGroupSafety", safety.ToArray())];
+ safety.Add((Strings["DevAlerter"], (s.Flags & CabFlags.AlerterBeeping) != 0 ? Palette.LampAlarm
+ : (s.Flags & CabFlags.AlerterBlinking) != 0 ? Palette.LampWarning : null));
+ safety.Add(Flag("DevWheelSlip", CabFlags.WheelSlip, Palette.LampAlarm));
+ safety.Add(On("DevOverloadRelay", CabDevices.OverloadRelay, Palette.LampAlarm));
+ safety.Add(Flag("DevRadioStop", CabFlags.RadioStop, Palette.LampAlarm));
+ safety.Add(Flag("DevAlarmChain", CabFlags.AlarmChain, Palette.LampAlarm));
+ if (diesel)
+ safety.Add(On("DevEngineHot", CabDevices.EngineHot, Palette.LampAlarm));
+ return [("CabGroupPower", power.ToArray()), ("CabGroupDriving", driving.ToArray()), ("CabGroupSafety", safety.ToArray())];
+ }
+
+ /// The doors of the vehicle the user is in, in rows of three: open, the lock, the permits, the steps, the departure signal.
+ private (string Text, IBrush? Lit)[] Doors(CabSample s)
+ {
+ (string, IBrush?) On(string key, CabDevices device, IBrush colour) => (Strings[key], s.Has(device) ? colour : null);
+ var doors = new List<(string, IBrush?)>
+ {
+ On("DevDoorsLeft", CabDevices.DoorsLeft, Palette.LampWarning), On("DevDoorsRight", CabDevices.DoorsRight, Palette.LampWarning)
+ };
+ if (s.Extra is null)
+ return doors.ToArray();
+ doors.Add(On("DevDoorLock", CabDevices.DoorLock, Palette.LampOn));
+ doors.Add(On("DevDoorPermitLeft", CabDevices.DoorPermitLeft, Palette.LampOn));
+ doors.Add(On("DevDoorPermitRight", CabDevices.DoorPermitRight, Palette.LampOn));
+ doors.Add(On("DevDoorsLocked", CabDevices.DoorsLocked, Palette.LampOn));
+ doors.Add(On("DevDoorStep", CabDevices.DoorStep, Palette.LampOn));
+ doors.Add(On("DevDepartureSignal", CabDevices.DepartureSignal, Palette.LampWarning));
+ return doors.ToArray();
}
}
diff --git a/Eu07.BlackBox.Views/EventList.cs b/Eu07.BlackBox.Views/EventList.cs
index 201bd52..d5db582 100644
--- a/Eu07.BlackBox.Views/EventList.cs
+++ b/Eu07.BlackBox.Views/EventList.cs
@@ -186,6 +186,7 @@ public sealed class EventList : ListBox
}
AddCameras(recording, strings, events, culture);
AddCouplings(recording, strings, events, culture);
+ AddCabChanges(recording, strings, events, culture);
foreach (var t in recording.Timetables)
events.Add(new BlackBoxEvent(t.Time, string.Format(culture, strings["EventTimetable"], $"{t.Category} {t.Train}".Trim(),
string.Format(culture, strings["Relation"], t.From, t.To)), Palette.Station, false));
@@ -266,6 +267,85 @@ public sealed class EventList : ListBox
_ => "CameraFree"
};
+ /// The switches the driver worked, as the cab tells them: what came on and went off.
+ private static readonly (CabDevices Device, string Key)[] Switched =
+ [
+ (CabDevices.MainBreaker, "DevMainBreaker"), (CabDevices.PantographFront, "DevPantographFront"),
+ (CabDevices.PantographRear, "DevPantographRear"), (CabDevices.FuelPump, "DevFuelPump"), (CabDevices.OilPump, "DevOilPump"),
+ (CabDevices.Converter, "DevConverter"), (CabDevices.Compressor, "DevCompressor"), (CabDevices.MotorBlowers, "DevMotorBlowers"),
+ (CabDevices.Heating, "DevHeating"), (CabDevices.Radio, "DevRadio"), (CabDevices.ShuntMode, "DevShuntMode"),
+ (CabDevices.SpringBrake, "DevSpringBrake"), (CabDevices.BrakeReleaser, "DevBrakeReleaser"),
+ (CabDevices.DoorPermitLeft, "DevDoorPermitLeft"), (CabDevices.DoorPermitRight, "DevDoorPermitRight"),
+ (CabDevices.DoorStep, "DevDoorStep"), (CabDevices.DoorLock, "DevDoorLock"), (CabDevices.SpeedControl, "DevSpeedControl")
+ ];
+
+ ///
+ /// What the driver did in the cab, from one look to the next in the same vehicle: the switches on and off, the
+ /// doors opened and closed, the departure signal, the hand brake, the brakes' setting, the radio's channel, the
+ /// speed the cruise control is set to; and, as faults, moving with the doors open and the engine overheating.
+ ///
+ private static void AddCabChanges(Recording recording, IBlackBoxStrings strings, List events, CultureInfo culture)
+ {
+ CabSample? last = null;
+ bool movingOpen = false;
+ foreach (var s in recording.Cab)
+ {
+ if (last is not { } b || b.Vehicle != s.Vehicle)
+ {
+ last = s;
+ movingOpen = false;
+ continue;
+ }
+ bool Came(CabDevices device) => !b.Has(device) && s.Has(device);
+ bool Went(CabDevices device) => b.Has(device) && !s.Has(device);
+ foreach (var (device, key) in Switched)
+ {
+ if (Came(device))
+ events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventDeviceOn"], strings[key]), Palette.LampOn, false));
+ else if (Went(device))
+ events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventDeviceOff"], strings[key]), Palette.FaintText, false));
+ }
+ foreach (var (device, open, closed) in new[] { (CabDevices.DoorsLeft, "DevDoorsLeft", "EventDoorsLeftClosed"), (CabDevices.DoorsRight, "DevDoorsRight", "EventDoorsRightClosed") })
+ {
+ if (Came(device))
+ events.Add(new BlackBoxEvent(s.Time, strings[open], Palette.LampWarning, false));
+ else if (Went(device))
+ events.Add(new BlackBoxEvent(s.Time, strings[closed], Palette.FaintText, false));
+ }
+ if (Came(CabDevices.DepartureSignal))
+ events.Add(new BlackBoxEvent(s.Time, strings["DevDepartureSignal"], Palette.LampWarning, false));
+ if (Came(CabDevices.EngineHot))
+ events.Add(new BlackBoxEvent(s.Time, strings["DevEngineHot"], Palette.Bad, true));
+ // moving with the doors open: once, until they are closed or the train stands
+ bool doorsOpen = s.Has(CabDevices.DoorsLeft) || s.Has(CabDevices.DoorsRight);
+ if (doorsOpen && s.AbsSpeedKmh > 2 && !movingOpen)
+ {
+ events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventMovingDoorsOpen"], s.AbsSpeedKmh), Palette.Bad, true));
+ movingOpen = true;
+ }
+ else if (!doorsOpen || s.AbsSpeedKmh < 0.5)
+ movingOpen = false;
+ if (b.Extra is { } x && s.Extra is { } y)
+ {
+ if (x.HandBrake <= 0 && y.HandBrake > 0)
+ events.Add(new BlackBoxEvent(s.Time, strings["EventHandBrakeOn"], Palette.LampWarning, false));
+ else if (x.HandBrake > 0 && y.HandBrake <= 0)
+ events.Add(new BlackBoxEvent(s.Time, strings["EventHandBrakeOff"], Palette.FaintText, false));
+ if (x.BrakeDelay != y.BrakeDelay || x.BrakeMode != y.BrakeMode)
+ {
+ string setting = string.Join(" · ", new[] { CabPanel.BrakeSetting(y.BrakeDelay), CabPanel.BrakeMode(y.BrakeMode) }.Where(t => t.Length > 0));
+ events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventBrakeSetting"], setting), Palette.Brakes, false));
+ }
+ if (x.RadioChannel != y.RadioChannel && y.RadioChannel > 0)
+ events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventRadioChannel"], y.RadioChannel), Palette.LampOn, false));
+ // the speed set, while the cruise control holds one
+ if (s.Has(CabDevices.SpeedControl) && (Came(CabDevices.SpeedControl) || Math.Abs(x.SpeedControlKmh - y.SpeedControlKmh) >= 1))
+ events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventSpeedControlSet"], y.SpeedControlKmh), Palette.Target, false));
+ }
+ last = s;
+ }
+ }
+
private static void AddCouplings(Recording recording, IBlackBoxStrings strings, List events, CultureInfo culture)
{
string Name(int id) => recording.Vehicle(id)?.Name ?? id.ToString(culture);
diff --git a/Eu07.BlackBox/BlackBoxReader.cs b/Eu07.BlackBox/BlackBoxReader.cs
index 417b0b0..55b4da9 100644
--- a/Eu07.BlackBox/BlackBoxReader.cs
+++ b/Eu07.BlackBox/BlackBoxReader.cs
@@ -165,6 +165,13 @@ public sealed class BlackBoxReader
// The lamps the vehicles at the ends have, which a pattern is checked against, later again.
if (r.Remaining >= 4)
sample = sample with { EquippedHead = (Lights)r.U16(), EquippedTail = (Lights)r.U16() };
+ // What drives it, the doors' permits and the rest, later again; the arguments are read in their order.
+ if (r.Remaining >= 54)
+ sample = sample with
+ {
+ Extra = new CabExtra((EngineKind)r.U8(), r.U8(), r.U8(), r.F32(), r.F32(), r.F32(), r.F32(), r.F32(),
+ r.F32(), r.F32(), r.F32(), r.I8(), r.U8(), r.U8(), r.F32(), r.F32(), r.F32(), r.F32())
+ };
rec.AddCab(sample);
break;
}
diff --git a/Eu07.BlackBox/HeadCodes.cs b/Eu07.BlackBox/HeadCodes.cs
index ccd98ea..fb4f335 100644
--- a/Eu07.BlackBox/HeadCodes.cs
+++ b/Eu07.BlackBox/HeadCodes.cs
@@ -71,7 +71,7 @@ public static class HeadCodes
};
/// Whether the head of the train showed what it was to; null when nothing tells it.
- public static bool? HeadRight(CabSample sample) => sample.Movement switch
+ public static bool? HeadRight(CabSample sample) => NoLamps(sample.EquippedHead) ? null : sample.Movement switch
{
Movement.Train => Meets(sample.LightsHead, sample.EquippedHead, sample.LightHintHead, HeadCode.Pc1),
Movement.Shunting => Shows(HeadCode.Tb1, sample.LightsHead, sample.EquippedHead),
@@ -79,7 +79,7 @@ public static class HeadCodes
};
/// Whether the tail of the train showed what it was to; null when nothing tells it.
- public static bool? TailRight(CabSample sample) => sample.Movement switch
+ public static bool? TailRight(CabSample sample) => NoLamps(sample.EquippedTail) ? null : sample.Movement switch
{
Movement.Train => Meets(sample.LightsTail, sample.EquippedTail, sample.LightHintTail, HeadCode.Pc5),
Movement.Shunting => Shows(HeadCode.Tb1, sample.LightsTail, sample.EquippedTail),
@@ -87,6 +87,12 @@ public static class HeadCodes
};
/// Whether both ends showed what they were to; null when nothing tells it.
+ ///
+ /// An end with no lamp at all, a draisine's: the simulator still asks it for a signal it can't show, so it
+ /// is not judged. A recording made before the lamps were written can't tell.
+ ///
+ public static bool NoLamps(Lights? equipped) => equipped == Lights.None;
+
public static bool? Right(CabSample sample) =>
HeadRight(sample) is { } head && TailRight(sample) is { } tail ? head && tail : null;
diff --git a/Eu07.BlackBox/Records.cs b/Eu07.BlackBox/Records.cs
index 44a7d90..fa2ca96 100644
--- a/Eu07.BlackBox/Records.cs
+++ b/Eu07.BlackBox/Records.cs
@@ -79,9 +79,56 @@ public enum CabDevices : uint
Heating = 0x2000,
/// Applied.
SpringBrake = 0x4000,
- ShuntMode = 0x8000
+ ShuntMode = 0x8000,
+ /// Sounding.
+ DepartureSignal = 0x0001_0000,
+ /// The doors may be opened.
+ DoorPermitLeft = 0x0002_0000,
+ DoorPermitRight = 0x0004_0000,
+ /// The steps come out with the doors.
+ DoorStep = 0x0008_0000,
+ /// The doors lock above a speed.
+ DoorLock = 0x0010_0000,
+ /// Locked now.
+ DoorsLocked = 0x0020_0000,
+ /// Releasing the brakes of the vehicle.
+ BrakeReleaser = 0x0040_0000,
+ FuelPump = 0x0080_0000,
+ OilPump = 0x0100_0000,
+ /// The fans of the traction motors.
+ MotorBlowers = 0x0200_0000,
+ /// The diesel engine is over its temperature.
+ EngineHot = 0x0400_0000,
+ /// The cruise control holds a speed.
+ SpeedControl = 0x0800_0000
}
+/// What drives the vehicle whose motors the cab drives.
+public enum EngineKind : byte
+{
+ /// Nothing of its own: a draisine, a car.
+ None = 0,
+ Electric = 1,
+ ElectricInduction = 2,
+ /// With a mechanical or a hydraulic transmission.
+ Diesel = 3,
+ DieselElectric = 4,
+ Steam = 5
+}
+
+///
+/// The rest of a look at the user's train, written from the same version as the doors' permits: what drives it,
+/// the brakes' setting and mode, the hand brake, the radio, the horn, the pressures of the equalizing reservoir
+/// and the main reservoir pipe, a diesel's oil, temperatures, clutch and torque converter, the cruise control,
+/// the power and the integrated controller of a unit, the highest brake cylinder pressure of the train. The
+/// temperatures and the oil mean something for a diesel only.
+///
+public readonly record struct CabExtra(EngineKind Engine, byte BrakeDelay, byte RadioChannel, float EqualizingReservoir,
+ float FeedPipe, float OilPressure, float WaterTemperature, float OilTemperature,
+ float EngineTemperature, float SpeedControlKmh, float EnginePower, sbyte HandBrake,
+ byte BrakeMode, byte Horn, float IntegratedController, float Clutch, float ConverterFill,
+ float ConsistBrakeCylinder);
+
/// The train protection: the alerter (CA) and the cab signalling (SHP) apart.
[Flags]
public enum SecurityFlags : byte
@@ -319,7 +366,7 @@ public readonly record struct CabSample(double Time, int Vehicle, Position Posit
Lights LightsHead, Lights LightsTail, sbyte CabEnd = 0,
SecurityFlags Security = SecurityFlags.None, float LateralAcceleration = 0,
Movement Movement = Movement.Unknown, short LightHintHead = -1, short LightHintTail = -1,
- Lights? EquippedHead = null, Lights? EquippedTail = null)
+ Lights? EquippedHead = null, Lights? EquippedTail = null, CabExtra? Extra = null)
{
public bool Has(CabDevices device) => (Devices & device) == device;
public bool Has(SecurityFlags flag) => (Security & flag) == flag;
diff --git a/StarterNG.Tests/BlackBox/BlackBoxFile.cs b/StarterNG.Tests/BlackBox/BlackBoxFile.cs
index 1daaf52..b0a49e7 100644
--- a/StarterNG.Tests/BlackBox/BlackBoxFile.cs
+++ b/StarterNG.Tests/BlackBox/BlackBoxFile.cs
@@ -108,7 +108,8 @@ internal sealed class BlackBoxFile
float target = 40, float nextLimit = -1, float nextDistance = -1, float brakeCylinder = 0,
uint devices = 0x0007, bool extended = true, sbyte cabEnd = 1, byte security = 0,
float lateral = 0.2f, ushort lightsHead = 0x15, ushort lightsTail = 0x22, byte? movement = null,
- short hintHead = -1, short hintTail = -1, ushort? equippedHead = null, ushort? equippedTail = null) =>
+ short hintHead = -1, short hintTail = -1, ushort? equippedHead = null, ushort? equippedTail = null,
+ byte? engine = null, float oilTemperature = 70) =>
Record(0x20, w =>
{
w.Write(time);
@@ -159,6 +160,28 @@ internal sealed class BlackBoxFile
{
w.Write(head);
w.Write(equippedTail ?? head);
+ // what drives it and the rest: a recording made before them ends above
+ if (engine is { } kind2)
+ {
+ w.Write(kind2);
+ w.Write((byte)2); // brake setting P
+ w.Write((byte)3); // radio channel
+ w.Write(4.8f); // equalizing reservoir
+ w.Write(8.0f); // main reservoir pipe
+ w.Write(0.45f); // oil pressure
+ w.Write(75.0f); // water
+ w.Write(oilTemperature);
+ w.Write(80.0f); // engine
+ w.Write(0.0f); // cruise control
+ w.Write(900.0f); // power
+ w.Write((sbyte)0); // hand brake
+ w.Write((byte)1); // brake mode
+ w.Write((byte)0); // horn
+ w.Write(0.5f); // integrated controller
+ w.Write(1.0f); // clutch
+ w.Write(0.0f); // torque converter
+ w.Write(2.5f); // the highest brake cylinder of the train
+ }
}
}
}
diff --git a/StarterNG.Tests/BlackBox/BlackBoxSignalsTests.cs b/StarterNG.Tests/BlackBox/BlackBoxSignalsTests.cs
index 41678bd..be035bc 100644
--- a/StarterNG.Tests/BlackBox/BlackBoxSignalsTests.cs
+++ b/StarterNG.Tests/BlackBox/BlackBoxSignalsTests.cs
@@ -108,6 +108,34 @@ public class HeadCodeTests
Assert.True(fault.TailWrong);
}
+ [Fact]
+ public void What_drives_the_train_and_the_rest_are_read_when_written()
+ {
+ var f = new BlackBoxFile().Info();
+ f.Cab(0, 1, 1000, 40, movement: 1, equippedHead: 0x15, equippedTail: 0x22, engine: 4, oilTemperature: 71.5f);
+ f.Cab(0.5, 1, 1000, 40, movement: 1, equippedHead: 0x15, equippedTail: 0x22);
+ var cab = BlackBoxReader.Read(f.End(1).Stream()).Cab;
+ var extra = Assert.NotNull(cab[0].Extra);
+ Assert.Equal(EngineKind.DieselElectric, extra.Engine);
+ Assert.Equal(3, extra.RadioChannel);
+ Assert.Equal(71.5f, extra.OilTemperature);
+ Assert.Equal(1.0f, extra.Clutch);
+ Assert.Equal(2.5f, extra.ConsistBrakeCylinder);
+ // an older sample has none
+ Assert.Null(cab[1].Extra);
+ }
+
+ [Fact]
+ public void A_vehicle_with_no_lamps_is_not_judged()
+ {
+ var recording = DriveWith(0, 0, 0, 0);
+ Assert.Null(HeadCodes.HeadRight(recording.Cab[0]));
+ Assert.Null(HeadCodes.TailRight(recording.Cab[0]));
+ var summary = recording.Summarize();
+ Assert.Equal(0, summary.LightsJudgedSeconds);
+ Assert.Empty(summary.LightsFaults);
+ }
+
[Fact]
public void A_vehicle_with_all_the_lamps_is_to_light_all_three_for_Pc1()
{