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