feat: restore bench section UI with controls, PID RPM ramp, flowmeter charts, and fix CAN IDs

Restore the full bench control panel from the old source with MVVM architecture:

- Two-column left panel layout: bench info displays (RPM with target/voltage,
  temps, pressures, Q-flow, pump live values) and user commands (direction
  toggle, start/stop with RPM popup and quick-select buttons, oil pump toggle,
  turn downcounter with CAN send)
- PID RPM ramp controller (BenchPidController) with bumpless startup,
  anti-windup, and derivative-on-measurement for smooth motor speed transitions
- Real-time flowmeter charts (LiveChartsCore) for Q-Delivery and Q-Over
  with tolerance band overlays
- Bench/pump CAN liveness detection in PcanAdapter (receive-only IDs)
- K-Line connection status indicator (placeholder)
- Periodic relay bitmask sender (~21ms) and ElectronicMsg keepalive start
  on CAN connect, pump sender starts immediately on pump load

Fix critical CAN message ID bug: default bench XML values were incorrectly
converted from old source (decimal-notation hex parsed as actual hex digits,
e.g. "10" -> "A" instead of keeping "10" which parses as 0x10). Corrected
all IDs to match hardware: 0x10, 0x11, 0x13, 0x14, 0x15, 0x50, 0x51, 0x55.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 14:24:59 +02:00
parent 6d5605cddf
commit e343006f45
13 changed files with 1242 additions and 141 deletions

View File

@@ -113,9 +113,33 @@ namespace HC_APTBS.Services
/// <summary>
/// Commands the bench motor to the specified RPM by computing and applying
/// the corresponding voltage from the RPM-to-voltage lookup table.
/// Used by automated test execution for direct voltage jumps.
/// </summary>
void SetRpm(double rpm);
/// <summary>
/// Starts the PID-based RPM ramp controller to smoothly reach the target RPM.
/// Sends an initial voltage jump from the RPM-voltage lookup table, then hands
/// control to the PID loop after an approach delay.
/// Used for interactive (manual) bench control.
/// </summary>
/// <param name="targetRpm">Desired RPM setpoint.</param>
void StartRpmPid(double targetRpm);
/// <summary>
/// Stops the PID RPM controller, sends 0 V to the motor.
/// </summary>
void StopRpmPid();
/// <summary>The last RPM target that was commanded via <see cref="SetRpm"/> or <see cref="StartRpmPid"/>.</summary>
double LastTargetRpm { get; }
/// <summary>The last voltage value sent to the motor CAN parameter.</summary>
double LastCommandVoltage { get; }
/// <summary>Raised after a voltage command is sent to the motor (from <see cref="SetRpm"/> or the PID loop).</summary>
event Action? RpmCommandSent;
// ── Temperature control ───────────────────────────────────────────────────
/// <summary>
@@ -136,6 +160,18 @@ namespace HC_APTBS.Services
/// <param name="state">True = ON, false = OFF.</param>
void SetRelay(string relayName, bool state);
/// <summary>
/// Transmits the current relay bitmask once. Called on CAN connect so the
/// bench controller receives the initial relay state immediately.
/// </summary>
void SendInitialRelayState();
/// <summary>Starts the periodic relay bitmask sender (~21 ms cycle).</summary>
void StartRelaySender();
/// <summary>Stops the periodic relay bitmask sender.</summary>
void StopRelaySender();
// ── Test execution ────────────────────────────────────────────────────────
/// <summary>

View File

@@ -21,6 +21,18 @@ namespace HC_APTBS.Services
/// </summary>
event Action<string, bool>? StatusChanged;
/// <summary>
/// Raised when the bench controller starts or stops sending CAN frames.
/// <c>alive</c> is true when frames are being received, false after a timeout.
/// </summary>
event Action<bool>? BenchLivenessChanged;
/// <summary>
/// Raised when the pump ECU starts or stops sending CAN frames.
/// <c>alive</c> is true when frames are being received, false after a timeout.
/// </summary>
event Action<bool>? PumpLivenessChanged;
// ── Properties ────────────────────────────────────────────────────────────
/// <summary>Most recent PCAN status code.</summary>
@@ -59,6 +71,18 @@ namespace HC_APTBS.Services
/// <summary>Removes entries whose keys match the supplied dictionary.</summary>
void RemoveParameters(Dictionary<uint, List<CanBusParameter>> parameters);
/// <summary>
/// Registers CAN message IDs that belong to the bench controller.
/// Frames with these IDs drive <see cref="BenchLivenessChanged"/>.
/// </summary>
void RegisterBenchMessageIds(IReadOnlyCollection<uint> ids);
/// <summary>
/// Registers CAN message IDs that belong to the pump ECU.
/// Frames with these IDs drive <see cref="PumpLivenessChanged"/>.
/// </summary>
void RegisterPumpMessageIds(IReadOnlyCollection<uint> ids);
// ── Transmit ──────────────────────────────────────────────────────────────
/// <summary>

View File

@@ -0,0 +1,204 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace HC_APTBS.Services.Impl
{
/// <summary>
/// PID controller for smooth RPM ramping on the test bench motor.
/// Runs a background loop that reads actual RPM, computes the PID output,
/// and writes the corresponding motor voltage via a callback.
///
/// <para>
/// All values are normalized to 01.0 internally. The output is scaled back
/// to the configured voltage range before calling the write delegate.
/// </para>
/// </summary>
public sealed class BenchPidController : IDisposable
{
// ── Gains ────────────────────────────────────────────────────────────────
private readonly double _kp;
private readonly double _ki;
private readonly double _kd;
// ── Ranges ───────────────────────────────────────────────────────────────
/// <summary>Maximum process variable (RPM).</summary>
private readonly double _pvMax;
/// <summary>Maximum output variable (voltage).</summary>
private readonly double _outMax;
// ── Delegates ────────────────────────────────────────────────────────────
private readonly Func<double> _readActualRpm;
private readonly Func<double> _readTargetRpm;
private readonly Action<double> _sendVoltage;
// ── Loop state ───────────────────────────────────────────────────────────
private readonly int _intervalMs;
private CancellationTokenSource? _cts;
private Task? _loopTask;
// ── PID state ────────────────────────────────────────────────────────────
private double _errSum;
private double _lastPv;
private Stopwatch _sw = new();
/// <summary>True when the PID loop is actively running.</summary>
public bool IsRunning { get; private set; }
// ── Constructor ──────────────────────────────────────────────────────────
/// <summary>
/// Creates a new PID controller for bench RPM control.
/// </summary>
/// <param name="kp">Proportional gain.</param>
/// <param name="ki">Integral gain.</param>
/// <param name="kd">Derivative gain.</param>
/// <param name="intervalMs">Loop interval in milliseconds.</param>
/// <param name="pvMax">Maximum process variable (RPM). Minimum is always 0.</param>
/// <param name="outMax">Maximum output (voltage). Minimum is always 0.</param>
/// <param name="readActualRpm">Delegate to read the current bench RPM.</param>
/// <param name="readTargetRpm">Delegate to read the desired RPM setpoint.</param>
/// <param name="sendVoltage">Delegate to write the output voltage to the CAN bus.</param>
public BenchPidController(
double kp, double ki, double kd, int intervalMs,
double pvMax, double outMax,
Func<double> readActualRpm,
Func<double> readTargetRpm,
Action<double> sendVoltage)
{
_kp = kp;
_ki = ki;
_kd = kd;
_intervalMs = Math.Max(1, intervalMs);
_pvMax = pvMax;
_outMax = outMax;
_readActualRpm = readActualRpm;
_readTargetRpm = readTargetRpm;
_sendVoltage = sendVoltage;
}
// ── Public API ───────────────────────────────────────────────────────────
/// <summary>
/// Starts the PID loop with bumpless transfer from the given initial conditions.
/// If already running, stops first before restarting.
/// </summary>
/// <param name="initialVoltage">Current motor voltage (for bumpless startup).</param>
/// <param name="initialRpm">Current actual RPM (for bumpless startup).</param>
public void Start(double initialVoltage, double initialRpm)
{
if (IsRunning) Stop();
// Bumpless startup: pre-initialize integral sum so the first output
// matches the current voltage, avoiding a step response.
double normalizedOutput = Scale(initialVoltage, 0, _outMax, 0, 1.0);
_errSum = _ki != 0 ? normalizedOutput / _ki : 0;
_lastPv = Scale(Clamp(initialRpm, 0, _pvMax), 0, _pvMax, 0, 1.0);
_sw = Stopwatch.StartNew();
IsRunning = true;
_cts = new CancellationTokenSource();
var ct = _cts.Token;
_loopTask = Task.Run(async () =>
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_intervalMs));
try
{
while (IsRunning && await timer.WaitForNextTickAsync(ct))
{
Compute();
}
}
catch (OperationCanceledException) { }
}, ct);
}
/// <summary>
/// Stops the PID loop and sends 0 V to the motor.
/// </summary>
public void Stop()
{
if (!IsRunning) return;
IsRunning = false;
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
_sendVoltage(0);
}
// ── PID core ─────────────────────────────────────────────────────────────
private void Compute()
{
double pv = _readActualRpm();
double sp = _readTargetRpm();
// Clamp and normalize to 01.0
pv = Scale(Clamp(pv, 0, _pvMax), 0, _pvMax, 0, 1.0);
sp = Scale(Clamp(sp, 0, _pvMax), 0, _pvMax, 0, 1.0);
double err = sp - pv;
// Time delta in seconds
double dt = _sw.Elapsed.TotalSeconds;
_sw.Restart();
if (dt <= 0) dt = _intervalMs / 1000.0;
// P term
double pTerm = _kp * err;
// I term (with anti-windup: only integrate when PV is in-range)
double partialSum = _errSum;
double iTerm = 0;
if (pv >= 0 && pv <= 1.0)
{
partialSum = _errSum + dt * err;
iTerm = _ki * partialSum;
}
// D term (derivative-on-measurement, not on error)
double dTerm = 0;
if (dt > 0)
dTerm = _kd * (pv - _lastPv) / dt;
// Combine, clamp, scale to output voltage range
double output = Clamp(pTerm + iTerm + dTerm, 0, 1.0);
double voltage = Scale(output, 0, 1.0, 0, _outMax);
_sendVoltage(voltage);
// Update state
_errSum = partialSum;
_lastPv = pv;
}
// ── Helpers ──────────────────────────────────────────────────────────────
private static double Clamp(double value, double min, double max)
=> value > max ? max : value < min ? min : value;
private static double Scale(double value, double fromMin, double fromMax, double toMin, double toMax)
{
if (Math.Abs(fromMax - fromMin) < 1e-12) return toMin;
double pct = (value - fromMin) / (fromMax - fromMin);
return toMin + pct * (toMax - toMin);
}
/// <inheritdoc/>
public void Dispose()
{
IsRunning = false;
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
}
}

View File

@@ -56,6 +56,18 @@ namespace HC_APTBS.Services.Impl
private CancellationTokenSource? _electronicMsgCts;
private volatile bool _electronicMsgActive;
// Periodic relay bitmask sender
private const int RelaySendIntervalMs = 21;
private Task? _relaySendTask;
private CancellationTokenSource? _relaySendCts;
private volatile bool _relaySendActive;
// RPM PID ramp controller
private BenchPidController? _pidController;
private double _lastTargetRpm;
private double _lastCommandVoltage;
private double _pidTargetRpm;
// ── Events ────────────────────────────────────────────────────────────────
/// <inheritdoc/>
@@ -78,6 +90,8 @@ namespace HC_APTBS.Services.Impl
public event Action<string, bool>? PhaseCompleted;
/// <inheritdoc/>
public event Action<string, double>? PumpControlValueSet;
/// <inheritdoc/>
public event Action? RpmCommandSent;
// ── Constructor ───────────────────────────────────────────────────────────
@@ -96,6 +110,12 @@ namespace HC_APTBS.Services.Impl
/// <inheritdoc/>
public bool IsAutoDfiEnabled => _autoDfiEnabled;
/// <inheritdoc/>
public double LastTargetRpm => _lastTargetRpm;
/// <inheritdoc/>
public double LastCommandVoltage => _lastCommandVoltage;
// ── IBenchService: active pump ────────────────────────────────────────────
/// <inheritdoc/>
@@ -161,14 +181,93 @@ namespace HC_APTBS.Services.Impl
double voltage = RpmVoltageRelation.VoltageForRpm(
(int)safeRpm, _config.Settings.Relations);
// Write the voltage value into the RPM parameter and transmit.
SetParameter(BenchParameterNames.Rpm, voltage);
SendParameters(_config.Bench.ParametersByName.TryGetValue(
BenchParameterNames.Rpm, out var rpmParam) ? rpmParam.MessageId : 0x0A);
SendRpmVoltage(voltage);
_lastTargetRpm = safeRpm;
RpmCommandSent?.Invoke();
_log.Debug(LogId, $"SetRpm({safeRpm}) → voltage={voltage:F3}V");
}
/// <inheritdoc/>
public void StartRpmPid(double targetRpm)
{
// Stop any existing PID loop.
_pidController?.Stop();
double safeRpm = Math.Min(targetRpm, _config.Settings.SecurityRpmLimit);
_lastTargetRpm = safeRpm;
_pidTargetRpm = safeRpm;
if (safeRpm <= 0)
{
SendRpmVoltage(0);
RpmCommandSent?.Invoke();
return;
}
// Step 1: Send initial voltage from lookup table (open-loop jump).
double initialVoltage = RpmVoltageRelation.VoltageForRpm(
(int)safeRpm, _config.Settings.Relations);
SendRpmVoltage(initialVoltage);
RpmCommandSent?.Invoke();
// Step 2: Calculate approach delay based on RPM distance.
double actualRpm = ReadBenchParameter(BenchParameterNames.BenchRpm);
int delaySec = (int)(Math.Abs(safeRpm - actualRpm) * 0.004 + 0.7);
// Step 3: Create PID controller and start after delay.
var settings = _config.Settings;
_pidController = new BenchPidController(
settings.PidP, settings.PidI, settings.PidD, settings.PidLoopMs,
settings.MaxRpm, settings.VoltageForMaxRpm,
() => ReadBenchParameter(BenchParameterNames.BenchRpm),
() => _pidTargetRpm,
SendRpmVoltage);
_ = Task.Run(async () =>
{
try
{
await Task.Delay(delaySec * 1000);
if (_pidTargetRpm > 0)
_pidController.Start(initialVoltage, actualRpm);
}
catch (Exception ex)
{
_log.Error(LogId, $"StartRpmPid delay task: {ex.Message}");
}
});
_log.Info(LogId, $"StartRpmPid({safeRpm}) → initial voltage={initialVoltage:F3}V, PID delay={delaySec}s");
}
/// <inheritdoc/>
public void StopRpmPid()
{
_pidTargetRpm = 0;
_pidController?.Stop();
_pidController?.Dispose();
_pidController = null;
_lastTargetRpm = 0;
SendRpmVoltage(0);
RpmCommandSent?.Invoke();
_log.Info(LogId, "StopRpmPid: PID stopped, 0V sent.");
}
/// <summary>
/// Writes a voltage value to the RPM CAN parameter and transmits it.
/// Used as the PID output callback and by <see cref="SetRpm"/>.
/// </summary>
private void SendRpmVoltage(double volts)
{
if (volts < 0) volts = 0;
_lastCommandVoltage = volts;
SetParameter(BenchParameterNames.Rpm, volts);
SendParameters(_config.Bench.ParametersByName.TryGetValue(
BenchParameterNames.Rpm, out var rpmParam) ? rpmParam.MessageId : 0x10);
}
// ── IBenchService: temperature ────────────────────────────────────────────
/// <inheritdoc/>
@@ -193,6 +292,58 @@ namespace HC_APTBS.Services.Impl
TransmitRelayMask(relay.MessageId);
}
/// <inheritdoc/>
public void SendInitialRelayState()
{
// Collect all distinct relay message IDs and transmit each one.
var sent = new HashSet<uint>();
foreach (var relay in _config.Bench.Relays.Values)
{
if (sent.Add(relay.MessageId))
TransmitRelayMask(relay.MessageId);
}
}
/// <inheritdoc/>
public void StartRelaySender()
{
if (_relaySendActive) return;
_relaySendActive = true;
_relaySendCts = new CancellationTokenSource();
var ct = _relaySendCts.Token;
_relaySendTask = Task.Run(async () =>
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RelaySendIntervalMs));
try
{
while (_relaySendActive && await timer.WaitForNextTickAsync(ct))
{
var sent = new HashSet<uint>();
foreach (var relay in _config.Bench.Relays.Values)
{
if (sent.Add(relay.MessageId))
TransmitRelayMask(relay.MessageId);
}
}
}
catch (OperationCanceledException) { }
}, ct);
_log.Debug(LogId, "Relay sender started.");
}
/// <inheritdoc/>
public void StopRelaySender()
{
_relaySendActive = false;
_relaySendCts?.Cancel();
_relaySendCts?.Dispose();
_relaySendCts = null;
_log.Debug(LogId, "Relay sender stopped.");
}
private void TransmitRelayMask(uint messageId)
{
// Collect all relays mapped to this CAN message ID and pack their bits.

View File

@@ -577,38 +577,38 @@ namespace HC_APTBS.Services.Impl
/// <summary>Returns the factory-default bench parameter XML used when bench.xml is absent.</summary>
private static string DefaultBenchXml() => @"<?xml version=""1.0"" encoding=""utf-8""?>
<Bench>
<RPM id=""A"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
<Counter id=""B"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
<BaudRate id=""37"" byteh=""0"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
<BenchRPM id=""D"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""true"" />
<BenchCounter id=""D"" byteh=""3"" bytel=""2"" filter=""1"" disableparams=""true"" />
<BenchTemp id=""E"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
<T-in id=""E"" byteh=""3"" bytel=""2"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
<T-out id=""E"" byteh=""5"" bytel=""4"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
<T4 id=""E"" byteh=""7"" bytel=""6"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
<RPM id=""10"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
<Counter id=""11"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
<BaudRate id=""55"" byteh=""0"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
<BenchRPM id=""13"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""true"" />
<BenchCounter id=""13"" byteh=""3"" bytel=""2"" filter=""1"" disableparams=""true"" />
<BenchTemp id=""14"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
<T-in id=""14"" byteh=""3"" bytel=""2"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
<T-out id=""14"" byteh=""5"" bytel=""4"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
<T4 id=""14"" byteh=""7"" bytel=""6"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
<QDelivery id=""8"" byteh=""5"" bytel=""3"" filter=""0.01"" disableparams=""false"" p1=""0"" p2=""2.03"" p3=""1E-06"" p4=""0"" p5=""0"" p6=""0"" />
<QOver id=""8"" byteh=""2"" bytel=""0"" filter=""0.11"" disableparams=""false"" p1=""0"" p2=""0.51"" p3=""1E-06"" p4=""0"" p5=""0"" p6=""0"" />
<PSGEncoderValue id=""32"" byteh=""4"" bytel=""5"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
<PSGEncoderWorking id=""32"" byteh=""7"" bytel=""7"" filter=""1"" disableparams=""true"" />
<InyectorEncoderValue id=""32"" byteh=""2"" bytel=""3"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
<InyectorEncoderWorking id=""32"" byteh=""6"" bytel=""6"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
<ManualEncoderValue id=""32"" byteh=""0"" bytel=""1"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
<EncoderResolution id=""33"" byteh=""6"" bytel=""7"" filter=""1"" disableparams=""true"" send=""true"" />
<ElectronicMsg id=""33"" byteh=""0"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
<PSGEncoderValue id=""50"" byteh=""4"" bytel=""5"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
<PSGEncoderWorking id=""50"" byteh=""7"" bytel=""7"" filter=""1"" disableparams=""true"" />
<InyectorEncoderValue id=""50"" byteh=""2"" bytel=""3"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
<InyectorEncoderWorking id=""50"" byteh=""6"" bytel=""6"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
<ManualEncoderValue id=""50"" byteh=""0"" bytel=""1"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
<EncoderResolution id=""51"" byteh=""6"" bytel=""7"" filter=""1"" disableparams=""true"" send=""true"" />
<ElectronicMsg id=""51"" byteh=""0"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
<Alarms id=""8"" byteh=""7"" bytel=""6"" filter=""1"" disableparams=""true"" />
<Pressure id=""D"" byteh=""4"" bytel=""5"" filter=""1"" disableparams=""true"" />
<AnalogicSensor2 id=""D"" byteh=""6"" bytel=""7"" filter=""1"" disableparams=""true"" />
<Pressure id=""13"" byteh=""4"" bytel=""5"" filter=""1"" disableparams=""true"" />
<AnalogicSensor2 id=""13"" byteh=""6"" bytel=""7"" filter=""1"" disableparams=""true"" />
<Reles>
<Rele name=""Electronic"" id=""F"" bit=""0"" />
<Rele name=""OilPump"" id=""F"" bit=""4"" />
<Rele name=""DepositCooler"" id=""F"" bit=""8"" />
<Rele name=""DepositHeater"" id=""F"" bit=""12"" />
<Rele name=""Reserve"" id=""F"" bit=""16"" />
<Rele name=""Counter"" id=""F"" bit=""20"" />
<Rele name=""Direction"" id=""F"" bit=""24"" />
<Rele name=""TinCooler"" id=""F"" bit=""28"" />
<Rele name=""Pulse4Signal"" id=""F"" bit=""32"" />
<Rele name=""Flasher"" id=""F"" bit=""44"" />
<Rele name=""Electronic"" id=""15"" bit=""0"" />
<Rele name=""OilPump"" id=""15"" bit=""4"" />
<Rele name=""DepositCooler"" id=""15"" bit=""8"" />
<Rele name=""DepositHeater"" id=""15"" bit=""12"" />
<Rele name=""Reserve"" id=""15"" bit=""16"" />
<Rele name=""Counter"" id=""15"" bit=""20"" />
<Rele name=""Direction"" id=""15"" bit=""24"" />
<Rele name=""TinCooler"" id=""15"" bit=""28"" />
<Rele name=""Pulse4Signal"" id=""15"" bit=""32"" />
<Rele name=""Flasher"" id=""15"" bit=""44"" />
</Reles>
</Bench>";