feat(blackbox): the cab panel as fits the vehicle, every switch the driver worked in the events

- The rest of the cab record is read when written: what drives the
  vehicle, the doors' permits and departure signal, the brakes of the
  whole train, a diesel's oil and temperatures, the cruise control and
  the rest.
- Cab panel: the traction as fits what drives the vehicle (voltage and
  current of an electric one; revolutions, oil pressure and temperatures
  of a diesel; clutch or torque converter of a mechanical or hydraulic
  one; nothing for a draisine), the equalizing reservoir, the main
  reservoir pipe and the most braked cylinder of the train, the brakes'
  setting and mode, a group of the doors, the pumps of a diesel where an
  electric vehicle has its pantographs, the hand brake, the releaser, the
  horn's tone, the radio's channel, the integrated handle of a unit.
- Events: every switch on and off, doors opened and closed, the departure
  signal, the hand brake, the brakes' setting, the radio channel, the
  cruise control's speed; moving with the doors open and an overheating
  engine as faults.
- A vehicle with no lamps at an end (a draisine) is not judged on its
  lights, and one which never had a voltage shows none.
This commit is contained in:
maj00r
2026-11-11 02:43:51 +01:00
parent 4dcfd1c4e3
commit 0d16524953
9 changed files with 585 additions and 89 deletions

View File

@@ -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",

View File

@@ -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));

View File

@@ -9,26 +9,32 @@ using Avalonia.Media;
namespace Eu07.BlackBox.Views;
/// <summary>
/// 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.
/// </summary>
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<int> _powered = new();
private readonly HashSet<int> _feedPipe = new();
private readonly HashSet<int> _integrated = new();
private readonly HashSet<int> _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);
}
}
/// <summary>The title of a part of the panel, with a line from it to the right; the top of the part returned.</summary>
private static double Section(DrawingContext context, string title, double x, double y, double right)
/// <summary>
/// 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.
/// </summary>
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;
}
}
/// <summary>The setting of the brakes from its bits: G, P, R, R+Mg.</summary>
public static string BrakeSetting(byte bits)
{
var parts = new List<string>();
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);
}
/// <summary>The mode of the brakes from its bits: PS, PN, EP, MED.</summary>
public static string BrakeMode(byte bits)
{
var parts = new List<string>();
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);
}
/// <summary>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.</summary>
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;
}
/// <summary>The highest step of the controller used in the recording, for its bar.</summary>
/// <summary>
/// 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.
/// </summary>
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));
}
/// <summary>The steps of the controller as a row of cells, lit up to the one it stands at.</summary>
@@ -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)
/// <summary>A device's lamp and its name: lit in its colour, the name bright; off, both faint.</summary>
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));
}
/// <summary>Every device, with the colour of its lamp if it was on, in the groups the panel shows.</summary>
private static (string Title, (string Key, IBrush? Lit)[] Lamps)[] Devices(CabSample s)
/// <summary>
/// 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.
/// </summary>
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())];
}
/// <summary>The doors of the vehicle the user is in, in rows of three: open, the lock, the permits, the steps, the departure signal.</summary>
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();
}
}

View File

@@ -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"
};
/// <summary>The switches the driver worked, as the cab tells them: what came on and went off.</summary>
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")
];
/// <summary>
/// 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.
/// </summary>
private static void AddCabChanges(Recording recording, IBlackBoxStrings strings, List<BlackBoxEvent> 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<BlackBoxEvent> events, CultureInfo culture)
{
string Name(int id) => recording.Vehicle(id)?.Name ?? id.ToString(culture);

View File

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

View File

@@ -71,7 +71,7 @@ public static class HeadCodes
};
/// <summary>Whether the head of the train showed what it was to; null when nothing tells it.</summary>
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
};
/// <summary>Whether the tail of the train showed what it was to; null when nothing tells it.</summary>
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
};
/// <summary>Whether both ends showed what they were to; null when nothing tells it.</summary>
/// <summary>
/// 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.
/// </summary>
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;

View File

@@ -79,9 +79,56 @@ public enum CabDevices : uint
Heating = 0x2000,
/// <summary>Applied.</summary>
SpringBrake = 0x4000,
ShuntMode = 0x8000
ShuntMode = 0x8000,
/// <summary>Sounding.</summary>
DepartureSignal = 0x0001_0000,
/// <summary>The doors may be opened.</summary>
DoorPermitLeft = 0x0002_0000,
DoorPermitRight = 0x0004_0000,
/// <summary>The steps come out with the doors.</summary>
DoorStep = 0x0008_0000,
/// <summary>The doors lock above a speed.</summary>
DoorLock = 0x0010_0000,
/// <summary>Locked now.</summary>
DoorsLocked = 0x0020_0000,
/// <summary>Releasing the brakes of the vehicle.</summary>
BrakeReleaser = 0x0040_0000,
FuelPump = 0x0080_0000,
OilPump = 0x0100_0000,
/// <summary>The fans of the traction motors.</summary>
MotorBlowers = 0x0200_0000,
/// <summary>The diesel engine is over its temperature.</summary>
EngineHot = 0x0400_0000,
/// <summary>The cruise control holds a speed.</summary>
SpeedControl = 0x0800_0000
}
/// <summary>What drives the vehicle whose motors the cab drives.</summary>
public enum EngineKind : byte
{
/// <summary>Nothing of its own: a draisine, a car.</summary>
None = 0,
Electric = 1,
ElectricInduction = 2,
/// <summary>With a mechanical or a hydraulic transmission.</summary>
Diesel = 3,
DieselElectric = 4,
Steam = 5
}
/// <summary>
/// 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.
/// </summary>
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);
/// <summary>The train protection: the alerter (CA) and the cab signalling (SHP) apart.</summary>
[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;

View File

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

View File

@@ -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()
{