@@ -0,0 +1,235 @@
|
||||
using System.IO.Ports;
|
||||
|
||||
namespace PamhagenSysCtrl.Helpers.Plc {
|
||||
public class LegacyPamhagenPlc : IDisposable {
|
||||
|
||||
public record struct SensorStates(
|
||||
long PressureSensors,
|
||||
FillState FillLevelMW1,
|
||||
FillState FillLevelMW2,
|
||||
bool ClearancePress1,
|
||||
bool ClearancePress2,
|
||||
int TroughSelector,
|
||||
double GradationOe,
|
||||
FillState[] FillLevelTanks,
|
||||
MotorState PressCrossAuger
|
||||
) {
|
||||
public readonly bool GetDS(int n) => (PressureSensors & (1L << (n - 1))) != 0;
|
||||
}
|
||||
|
||||
public record struct ActuatorStates(
|
||||
long Valves,
|
||||
MotorState MP1, MotorState MP2, MotorState P12, MotorState P3, MotorState P4, MotorState P5, MotorState P6,
|
||||
bool TroughAuger, bool Rebler, bool StirrerMW1, bool StirrerMW2, bool Auger2, bool Auger3, bool Auger1, bool GradationPump
|
||||
) {
|
||||
public readonly bool GetV(int n) => (Valves & (1L << (n - 1))) != 0;
|
||||
public void SetV(int n, bool val) {
|
||||
if (val) {
|
||||
Valves |= 1L << (n - 1);
|
||||
} else {
|
||||
Valves &= ~(1L << (n - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly T1 Plc;
|
||||
|
||||
public string PortName => Plc.PortName;
|
||||
public T1.Status LastStatus => Plc.LastStatus;
|
||||
public T1Error LastError => Plc.LastError;
|
||||
|
||||
public LegacyPamhagenPlc(string portName, Parity parity = Parity.Odd) {
|
||||
Plc = new T1(portName, parity);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
Plc.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected static FillState Int3ToFillState(int n, bool bigEndian = true) {
|
||||
return
|
||||
(bigEndian && (n & 0x4) != 0) || (!bigEndian && (n & 0x1) != 0) ? FillState.Full :
|
||||
(bigEndian && (n & 0x2) != 0) || (!bigEndian && (n & 0x2) != 0) ? FillState.Half :
|
||||
(bigEndian && (n & 0x1) != 0) || (!bigEndian && (n & 0x4) != 0) ? FillState.Filled :
|
||||
n == 0 ? FillState.Empty :
|
||||
FillState.Invalid;
|
||||
}
|
||||
|
||||
protected static FillState Int2ToFillState(int n, bool bigEndian = true) {
|
||||
return
|
||||
(bigEndian && (n & 0x2) != 0) || (!bigEndian && (n & 0x1) != 0) ? FillState.Full :
|
||||
(bigEndian && (n & 0x1) != 0) || (!bigEndian && (n & 0x2) != 0) ? FillState.Filled :
|
||||
n == 0 ? FillState.Empty :
|
||||
FillState.Invalid;
|
||||
}
|
||||
|
||||
protected static FillState Int1ToFillState(int n) {
|
||||
return
|
||||
n == 1 ? FillState.Full :
|
||||
n == 0 ? FillState.Unknown :
|
||||
FillState.Invalid;
|
||||
}
|
||||
|
||||
protected static MotorState Int2ToPumpState(int n, bool inverse) {
|
||||
return n switch {
|
||||
0 => MotorState.Halt,
|
||||
1 => inverse ? MotorState.Backward : MotorState.Forward,
|
||||
2 => inverse ? MotorState.Forward : MotorState.Backward,
|
||||
_ => MotorState.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
protected static MotorState Int1ToPumpState(int n) {
|
||||
return n switch {
|
||||
1 => MotorState.Forward,
|
||||
_ => MotorState.Halt,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task EnsureRunning() {
|
||||
var s = await Plc.StatusRead();
|
||||
if (s.Mode == T1.Mode.RUN || s.Mode == T1.Mode.RUN_F)
|
||||
return;
|
||||
var error = await Plc.ErrorStatusRead();
|
||||
throw new T1Exception($"Interner Fehler: {error} ({(int)error:0000})");
|
||||
}
|
||||
|
||||
public async Task<(ushort RW010, ushort RW011, ushort RW012, ushort RW013, ushort RW014, ushort RW015, ushort RW016)> ReadInputRegisters() {
|
||||
var d = await Plc.DataRead("RW010", 7);
|
||||
// RW010, RW011, RW012, RW013 Drucksensoren
|
||||
// RW014 Füllstände A1, A2, Presse1, Presse2, Wannenselekt
|
||||
// RW015 Messwert von Marselli Gerät
|
||||
// RW016 Füllstände der Tanks
|
||||
return (d[0], d[1], d[2], d[3], d[4], d[5], d[6]);
|
||||
}
|
||||
|
||||
public async Task<SensorStates> ReadInputs() {
|
||||
await EnsureRunning();
|
||||
var (r1, r2, r3, r4, f1, v, f2) = await ReadInputRegisters();
|
||||
|
||||
long pressure = (long)r1 | ((long)r2 << 16) | ((long)r3 << 32) | ((long)r4 << 48);
|
||||
pressure = (pressure & 0x01FFFFFFFFFFFFFF) | ((pressure & 0x0600000000000000) << 1);
|
||||
|
||||
var mw1 = Int3ToFillState((f1 ) & 0x7, false); // 111 011 001 000
|
||||
var mw2 = Int3ToFillState((f1 >> 3) & 0x7, false); // 111 011 001 000
|
||||
var p1 = ((f1 >> 6) & 0x1) == 0; // 1 0
|
||||
var p2 = ((f1 >> 7) & 0x1) == 0; // 1 0
|
||||
var wp = ((f1 >> 8) & 0x3); // 10 01
|
||||
|
||||
// Masseligerät : 4mA => 50 Oe
|
||||
// 20mA => 150 Oe
|
||||
// ---> 0mA => 25 Oe
|
||||
// Oe mA T1Wert
|
||||
// 25 0 0
|
||||
// 50 4 800
|
||||
// 75 8 1600
|
||||
// 100 12 2400
|
||||
// 125 16 3200
|
||||
// 150 20 4000
|
||||
// -> factor: 0,03125 + (offset) 25 Oe
|
||||
double oe = v * 0.03125 + 25;
|
||||
|
||||
var t1 = Int1ToFillState(((f2 ) & 0x3) >> 1); // 1X 0X
|
||||
var t2 = Int1ToFillState(((f2 >> 2) & 0x3) >> 1); // 1X 0X
|
||||
var t3 = Int1ToFillState(((f2 >> 4) & 0x3) >> 1); // 1X 0X
|
||||
var t4 = Int1ToFillState(((f2 >> 6) & 0x3) >> 1); // 1X 0X
|
||||
var t5 = Int1ToFillState(((f2 >> 8) & 0x3) >> 1); // 1X 0X
|
||||
var t6 = Int1ToFillState((f2 >> 10) & 0x1); // 1 0
|
||||
var t7 = Int1ToFillState((f2 >> 11) & 0x1); // 1 0
|
||||
var t8 = Int1ToFillState((f2 >> 12) & 0x1); // 1 0
|
||||
var t9 = Int1ToFillState((f2 >> 13) & 0x1); // 1 0
|
||||
var q1 = Int1ToPumpState((f2 >> 15) & 0x1); // 1 0
|
||||
|
||||
return new SensorStates(pressure, mw1, mw2, p1, p2, wp == 3 ? -1 : wp == 1 ? 2 : wp == 2 ? 1 : 0, oe, [t1, t2, t3, t4, t5, t6, t7, t8, t9], q1);
|
||||
}
|
||||
|
||||
protected async Task<(ushort RW000, ushort RW001, ushort RW002, ushort RW003, ushort RW004, ushort RW005, ushort RW006)> ReadOutputRegisters() {
|
||||
var d = await Plc.DataRead("RW000", 7);
|
||||
// RW000, RW001, RW002, RW003 Ventile
|
||||
// RW004 Pumpensteuerung MP1, MP2, P1/2, P3, P4, P5, P6
|
||||
// RW005 Steuerung Wannenschnecke, Rebler, Förderung, Rührer
|
||||
// RW006 MessungsSteuerung
|
||||
return (d[0], d[1], d[2], d[3], d[4], d[5], d[6]);
|
||||
}
|
||||
|
||||
protected async Task WriteOutputRegisters(ushort rw000, ushort rw001, ushort rw002, ushort rw003, ushort rw004, ushort rw005, ushort rw006) {
|
||||
// RW000, RW001, RW002, RW003 Ventile
|
||||
// RW004 Pumpensteuerung MP1, MP2, P1/2, P3, P4, P5, P6
|
||||
// RW005 Steuerung Wannenschnecke, Rebler, Förderung, Rührer
|
||||
// RW006 MessungsSteuerung
|
||||
await Plc.DataWrite("RW000", rw000, rw001, rw002, rw003, rw004, rw005, rw006);
|
||||
}
|
||||
|
||||
public async Task<ActuatorStates> ReadOutputs() {
|
||||
await EnsureRunning();
|
||||
var (v1, v2, v3, v4, p1, s1, s2) = await ReadOutputRegisters();
|
||||
|
||||
long valves = (long)v1 | ((long)v2 << 16) | ((long)v3 << 32) | ((long)v4 << 48);
|
||||
|
||||
var mp1 = Int2ToPumpState((p1 ) & 0x3, true);
|
||||
var mp2 = Int2ToPumpState((p1 >> 2) & 0x3, false);
|
||||
var p12 = Int1ToPumpState((p1 >> 4) & 0x1);
|
||||
var p3 = Int1ToPumpState((p1 >> 5) & 0x1);
|
||||
var p4 = Int1ToPumpState((p1 >> 6) & 0x1);
|
||||
var p5 = Int1ToPumpState((p1 >> 7) & 0x1);
|
||||
var p6 = Int1ToPumpState((p1 >> 8) & 0x1);
|
||||
|
||||
var m1 = (s1 & 0x01) != 0;
|
||||
var m2 = (s1 & 0x02) != 0;
|
||||
var m3 = (s1 & 0x08) != 0;
|
||||
var m4 = (s1 & 0x10) != 0;
|
||||
var m5 = (s1 & 0x20) != 0;
|
||||
var m6 = (s1 & 0x40) != 0;
|
||||
var m7 = (s1 & 0x80) != 0;
|
||||
|
||||
var m8 = (s2 & 0x04) != 0;
|
||||
var state = new ActuatorStates(valves, mp1, mp2, p12, p3, p4, p5, p6, m1, m2, m3, m4, m5, m6, m7, m8);
|
||||
state.SetV(58, (s1 & 0x04) != 0);
|
||||
state.SetV(59, (s2 & 0x01) != 0);
|
||||
state.SetV(60, (s2 & 0x02) != 0);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public async Task WriteOutputs(ActuatorStates state) {
|
||||
await EnsureRunning();
|
||||
var v1 = (ushort)((state.Valves ) & 0xFFFF);
|
||||
var v2 = (ushort)((state.Valves >> 16) & 0xFFFF);
|
||||
var v3 = (ushort)((state.Valves >> 32) & 0xFFFF);
|
||||
var v4 = (ushort)((state.Valves >> 48) & 0x01FF);
|
||||
|
||||
int pumps =
|
||||
(state.MP1 == MotorState.Backward ? 0x001 : 0x000) |
|
||||
(state.MP1 == MotorState.Forward ? 0x002 : 0x000) |
|
||||
(state.MP2 == MotorState.Forward ? 0x004 : 0x000) |
|
||||
(state.MP2 == MotorState.Backward ? 0x008 : 0x000) |
|
||||
(state.P12 == MotorState.Forward ? 0x010 : 0x000) |
|
||||
(state.P3 == MotorState.Forward ? 0x020 : 0x000) |
|
||||
(state.P4 == MotorState.Forward ? 0x040 : 0x000) |
|
||||
(state.P5 == MotorState.Forward ? 0x080 : 0x000) |
|
||||
(state.P6 == MotorState.Forward ? 0x100 : 0x000);
|
||||
|
||||
int devices =
|
||||
(state.TroughAuger ? 0x01 : 0x00) |
|
||||
(state.Rebler ? 0x02 : 0x00) |
|
||||
(state.GetV(58) ? 0x04 : 0x00) |
|
||||
(state.StirrerMW1 ? 0x08 : 0x00) |
|
||||
(state.StirrerMW2 ? 0x10 : 0x00) |
|
||||
(state.Auger2 ? 0x20 : 0x00) |
|
||||
(state.Auger3 ? 0x40 : 0x00) |
|
||||
(state.Auger1 ? 0x80 : 0x00);
|
||||
|
||||
int measuring =
|
||||
(state.GetV(59) ? 0x01 : 0x00) |
|
||||
(state.GetV(60) ? 0x02 : 0x00) |
|
||||
(state.GradationPump ? 0x04 : 0x00);
|
||||
|
||||
await WriteOutputRegisters(v1, v2, v3, v4, (ushort)pumps, (ushort)devices, (ushort)measuring);
|
||||
}
|
||||
|
||||
public async Task TestConnection() {
|
||||
await Plc.Test();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
using System.IO.Ports;
|
||||
|
||||
namespace PamhagenSysCtrl.Helpers.Plc {
|
||||
public class PamhagenPlc : IDisposable {
|
||||
|
||||
private bool _manualControlMode = false;
|
||||
private bool _overridePathClearnaces = false;
|
||||
private bool _overrideDryRunClearances = false;
|
||||
public bool ManualControlMode { get => _manualControlMode; set { _manualControlMode = value; UpdateInterlocks(); } }
|
||||
public bool OverridePathClearances { get => _overridePathClearnaces; set { _overridePathClearnaces = value; UpdateInterlocks(); } }
|
||||
public bool OverrideDryRunClearances { get => _overrideDryRunClearances; set { _overrideDryRunClearances = value; UpdateInterlocks(); } }
|
||||
|
||||
protected LegacyPamhagenPlc Plc;
|
||||
public PamhagenState State { get; private set; }
|
||||
|
||||
public string? PortName => Plc?.PortName;
|
||||
public T1.Status? LastStatus => Plc?.LastStatus;
|
||||
public T1Error? LastError => Plc?.LastError;
|
||||
|
||||
protected LegacyPamhagenPlc.ActuatorStates Actuators;
|
||||
protected LegacyPamhagenPlc.ActuatorStates LastActuators;
|
||||
protected LegacyPamhagenPlc.SensorStates Sensors = new(default, default, default, default, default, default, default, [
|
||||
FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown, FillState.Unknown
|
||||
], default);
|
||||
|
||||
public PamhagenPlc(string portName, Parity parity = Parity.Odd) {
|
||||
Plc = new(portName, parity);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
GC.SuppressFinalize(this);
|
||||
Plc.Dispose();
|
||||
}
|
||||
|
||||
public async Task Initialize() {
|
||||
await Plc.TestConnection();
|
||||
Actuators = await Plc.ReadOutputs();
|
||||
LastActuators = Actuators;
|
||||
}
|
||||
|
||||
public async Task Shutdown() {
|
||||
await Plc.WriteOutputs(new() { Valves = Actuators.Valves });
|
||||
}
|
||||
|
||||
public async Task Update() {
|
||||
UpdateInterlocks();
|
||||
|
||||
if (Actuators != LastActuators) {
|
||||
// FIXME race condition!
|
||||
await Plc.WriteOutputs(Actuators);
|
||||
LastActuators = Actuators;
|
||||
}
|
||||
Sensors = await Plc.ReadInputs();
|
||||
State = new(Sensors, () => Actuators, OverridePathClearances, OverrideDryRunClearances);
|
||||
|
||||
UpdateInterlocks();
|
||||
}
|
||||
|
||||
protected void UpdateInterlocks() {
|
||||
if (!ManualControlMode) {
|
||||
if (State.PressCrossAuger_IsActive && !State.Auger2_IsActive) StartAuger2();
|
||||
if (State.Rebler_IsActive && !State.Auger1_IsActive) StartAuger1();
|
||||
if (State.Auger1_IsActive && !State.Auger2_IsActive) StartAuger2();
|
||||
if (State.Auger2_IsActive && !State.Auger3_IsActive) StartAuger3();
|
||||
if (State.TroughAuger_IsActive && !State.TroughAuger_HasClearance)
|
||||
StopTroughAuger();
|
||||
if (!State.PressCrossAuger_IsActive && !State.Auger1_IsActive) {
|
||||
StopAuger2();
|
||||
StopAuger3();
|
||||
}
|
||||
}
|
||||
|
||||
if (State.MP1_IsActive && !State.MP1_HasClearance) StopMP1();
|
||||
if (State.MP2_IsActive && !State.MP2_HasClearance) StopMP2();
|
||||
|
||||
if (!ManualControlMode) {
|
||||
if (State.MP1_State == MotorState.Forward && State.MW1_FillLevel >= FillState.Half && !State.MW1_Stirrer_IsActive) {
|
||||
StartStirrerMW1();
|
||||
} else if (State.MW2_Stirrer_IsActive && (!State.MP1_IsActive || State.MW1_FillLevel < FillState.Half)) {
|
||||
StopStirrerMW1();
|
||||
}
|
||||
if (State.MP2_State == MotorState.Forward && State.MW2_FillLevel >= FillState.Half && !State.MW2_Stirrer_IsActive) {
|
||||
StartStirrerMW2();
|
||||
} else if (State.MW2_Stirrer_IsActive && (!State.MP2_IsActive || State.MW2_FillLevel < FillState.Half)) {
|
||||
StopStirrerMW2();
|
||||
}
|
||||
}
|
||||
|
||||
if (State.P12_IsActive && !State.P12_HasClearance) DisableP12();
|
||||
if (State.P3_IsActive && !State.P3_HasClearance) DisableP3();
|
||||
if (State.P4_IsActive && !State.P4_HasClearance) DisableP4();
|
||||
if (State.P5_IsActive && !State.P5_HasClearance) DisableP5();
|
||||
if (State.P6_IsActive && !State.P6_HasClearance) DisableP6();
|
||||
}
|
||||
|
||||
public void OpenV(int n) => SetV(n, true);
|
||||
public void CloseV(int n) => SetV(n, false);
|
||||
|
||||
protected void SetV(int n, bool val) {
|
||||
if (n < 1 || n > 57) throw new ArgumentException("Invalid value for n: 1 <= #V <= 57");
|
||||
Actuators.SetV(n, val);
|
||||
}
|
||||
|
||||
public void SelectMW1() {
|
||||
Actuators.SetV(58, true);
|
||||
}
|
||||
|
||||
public void SelectMW2() {
|
||||
Actuators.SetV(58, false);
|
||||
}
|
||||
|
||||
public void StartTroughAuger() {
|
||||
if (!ManualControlMode && !State.TroughAuger_HasClearance)
|
||||
throw new NoClearanceException("Schnecke Auffangwanne hat keine Freigabe");
|
||||
Actuators.TroughAuger = true;
|
||||
}
|
||||
|
||||
public void StopTroughAuger() {
|
||||
Actuators.TroughAuger = false;
|
||||
}
|
||||
|
||||
public void StartRebler() {
|
||||
Actuators.Rebler = true;
|
||||
if (!ManualControlMode) StartAuger1();
|
||||
}
|
||||
|
||||
public void StopRebler() {
|
||||
if (!ManualControlMode) StopTroughAuger();
|
||||
Actuators.Rebler = false;
|
||||
}
|
||||
|
||||
public void StartStirrerMW1() {
|
||||
Actuators.StirrerMW1 = true;
|
||||
}
|
||||
|
||||
public void StopStirrerMW1() {
|
||||
Actuators.StirrerMW1 = false;
|
||||
}
|
||||
|
||||
public void StartStirrerMW2() {
|
||||
Actuators.StirrerMW2 = true;
|
||||
}
|
||||
|
||||
public void StopStirrerMW2() {
|
||||
Actuators.StirrerMW2 = false;
|
||||
}
|
||||
|
||||
public void StartAuger1() {
|
||||
Actuators.Auger1 = true;
|
||||
if (!ManualControlMode) StartAuger2();
|
||||
}
|
||||
|
||||
public void StopAuger1() {
|
||||
if (!ManualControlMode && State.Rebler_IsActive) throw new NoClearanceException("Rebler aktiv");
|
||||
Actuators.Auger1 = false;
|
||||
}
|
||||
|
||||
public void StartAuger2() {
|
||||
Actuators.Auger2 = true;
|
||||
if (!ManualControlMode) StartAuger3();
|
||||
}
|
||||
|
||||
public void StopAuger2() {
|
||||
if (!ManualControlMode && State.PressCrossAuger_IsActive) throw new NoClearanceException("Presse-Querschnecke aktiv");
|
||||
if (!ManualControlMode && State.Auger1_IsActive) throw new NoClearanceException("Förderschnecke 1 aktiv");
|
||||
Actuators.Auger2 = false;
|
||||
}
|
||||
|
||||
public void StartAuger3() {
|
||||
Actuators.Auger3 = true;
|
||||
}
|
||||
|
||||
public void StopAuger3() {
|
||||
if (!ManualControlMode && State.Auger2_IsActive) throw new NoClearanceException("Förderschnecke 2 aktiv");
|
||||
Actuators.Auger3 = false;
|
||||
}
|
||||
|
||||
public void StartGradationPump() {
|
||||
Actuators.GradationPump = true;
|
||||
}
|
||||
|
||||
public void StopGradationPump() {
|
||||
Actuators.GradationPump = false;
|
||||
}
|
||||
|
||||
public void StartMP1Forward() {
|
||||
if (!State.MP1_HasClearance) throw new NoClearanceException("MP1 hat keine Freigabe");
|
||||
if (Actuators.MP1 == MotorState.Backward) throw new NoClearanceException("MP1 läuft bereits (zurück)");
|
||||
Actuators.MP1 = MotorState.Forward;
|
||||
if (State.MW1_FillLevel >= FillState.Half) StartStirrerMW1();
|
||||
}
|
||||
|
||||
public void StartMP1Backward() {
|
||||
if (!State.MP1_HasClearance) throw new NoClearanceException("MP1 hat keine Freigabe");
|
||||
if (Actuators.MP1 == MotorState.Forward) throw new NoClearanceException("MP1 läuft bereits (vor)");
|
||||
Actuators.MP1 = MotorState.Backward;
|
||||
}
|
||||
|
||||
public void StopMP1() {
|
||||
Actuators.MP1 = MotorState.Halt;
|
||||
if (!ManualControlMode) StopStirrerMW1();
|
||||
}
|
||||
|
||||
public void StartMP2Forward() {
|
||||
if (!State.MP2_HasClearance) throw new NoClearanceException("MP2 hat keine Freigabe");
|
||||
if (Actuators.MP2 == MotorState.Backward) throw new NoClearanceException("MP2 läuft bereits (zurück)");
|
||||
Actuators.MP2 = MotorState.Forward;
|
||||
if (State.MW2_FillLevel >= FillState.Half) StartStirrerMW2();
|
||||
}
|
||||
|
||||
public void StartMP2Backward() {
|
||||
if (!State.MP2_HasClearance) throw new NoClearanceException("MP2 hat keine Freigabe");
|
||||
if (Actuators.MP2 == MotorState.Forward) throw new NoClearanceException("MP2 läuft bereits (vor)");
|
||||
Actuators.MP2 = MotorState.Backward;
|
||||
}
|
||||
|
||||
public void StopMP2() {
|
||||
Actuators.MP2 = MotorState.Halt;
|
||||
if (!ManualControlMode) StopStirrerMW1();
|
||||
}
|
||||
|
||||
public void EnableP12() {
|
||||
if (!State.P12_HasClearance) throw new NoClearanceException("P1/2 hat keine Freigabe");
|
||||
Actuators.P12 = MotorState.Forward;
|
||||
}
|
||||
|
||||
public void DisableP12() {
|
||||
Actuators.P12 = MotorState.Halt;
|
||||
}
|
||||
|
||||
public void EnableP3() {
|
||||
if (!State.P3_HasClearance) throw new NoClearanceException("P3 hat keine Freigabe");
|
||||
Actuators.P3 = MotorState.Forward;
|
||||
}
|
||||
|
||||
public void DisableP3() {
|
||||
Actuators.P3 = MotorState.Halt;
|
||||
}
|
||||
|
||||
public void EnableP4() {
|
||||
if (!State.P4_HasClearance) throw new NoClearanceException("P4 hat keine Freigabe");
|
||||
Actuators.P4 = MotorState.Forward;
|
||||
}
|
||||
|
||||
public void DisableP4() {
|
||||
Actuators.P4 = MotorState.Halt;
|
||||
}
|
||||
|
||||
public void EnableP5() {
|
||||
if (!State.P5_HasClearance) throw new NoClearanceException("P5 hat keine Freigabe");
|
||||
Actuators.P5 = MotorState.Forward;
|
||||
}
|
||||
|
||||
public void DisableP5() {
|
||||
Actuators.P5 = MotorState.Halt;
|
||||
}
|
||||
|
||||
public void EnableP6() {
|
||||
if (!State.P6_HasClearance) throw new NoClearanceException("P6 hat keine Freigabe");
|
||||
Actuators.P6 = MotorState.Forward;
|
||||
}
|
||||
|
||||
public void DisableP6() {
|
||||
Actuators.P6 = MotorState.Halt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
namespace PamhagenSysCtrl.Helpers.Plc {
|
||||
public readonly record struct PamhagenState {
|
||||
|
||||
private readonly LegacyPamhagenPlc.SensorStates Sensors;
|
||||
private readonly Func<LegacyPamhagenPlc.ActuatorStates> ActuatorsCb;
|
||||
private LegacyPamhagenPlc.ActuatorStates Actuators { get => ActuatorsCb == null ? default : ActuatorsCb(); }
|
||||
|
||||
public long Valves => Actuators.Valves;
|
||||
public long PressureSensors => Sensors.PressureSensors;
|
||||
|
||||
public bool MW1_IsSelected => Sensors.TroughSelector == 1;
|
||||
public bool MW2_IsSelected => Sensors.TroughSelector == 2;
|
||||
public bool MW1_WantSelected => Actuators.GetV(58);
|
||||
public bool MW2_WantSelected => !Actuators.GetV(58);
|
||||
public FillState MW1_FillLevel => Sensors.FillLevelMW1;
|
||||
public FillState MW2_FillLevel => Sensors.FillLevelMW2;
|
||||
|
||||
public FillState[] Tanks_FillLevel => Sensors.FillLevelTanks;
|
||||
public FillState T1_FillLevel => Sensors.FillLevelTanks[0];
|
||||
public FillState T2_FillLevel => Sensors.FillLevelTanks[1];
|
||||
public FillState T3_FillLevel => Sensors.FillLevelTanks[2];
|
||||
public FillState T4_FillLevel => Sensors.FillLevelTanks[3];
|
||||
public FillState T5_FillLevel => Sensors.FillLevelTanks[4];
|
||||
public FillState T6_FillLevel => Sensors.FillLevelTanks[5];
|
||||
public FillState T7_FillLevel => Sensors.FillLevelTanks[6];
|
||||
public FillState T8_FillLevel => Sensors.FillLevelTanks[7];
|
||||
public FillState T9_FillLevel => Sensors.FillLevelTanks[8];
|
||||
|
||||
public MotorState PressCrossAuger_State => Sensors.PressCrossAuger;
|
||||
public bool PressCrossAuger_IsActive => PressCrossAuger_State == MotorState.Forward;
|
||||
|
||||
public bool TroughAuger_IsActive => Actuators.TroughAuger;
|
||||
public bool Rebler_IsActive => Actuators.Rebler;
|
||||
public bool MW1_Stirrer_IsActive => Actuators.StirrerMW1;
|
||||
public bool MW2_Stirrer_IsActive => Actuators.StirrerMW2;
|
||||
public bool Auger1_IsActive => Actuators.Auger1;
|
||||
public bool Auger2_IsActive => Actuators.Auger2;
|
||||
public bool Auger3_IsActive => Actuators.Auger3;
|
||||
public bool WasteDisposal_IsActive => Auger1_IsActive || Auger2_IsActive || Auger3_IsActive;
|
||||
|
||||
public MotorState MP1_State => Actuators.MP1;
|
||||
public MotorState MP2_State => Actuators.MP2;
|
||||
public MotorState P12_State => Actuators.P12;
|
||||
public MotorState P3_State => Actuators.P3;
|
||||
public MotorState P4_State => Actuators.P4;
|
||||
public MotorState P5_State => Actuators.P5;
|
||||
public MotorState P6_State => Actuators.P6;
|
||||
public bool MP1_IsActive => Actuators.MP1 == MotorState.Forward || Actuators.MP1 == MotorState.Backward;
|
||||
public bool MP2_IsActive => Actuators.MP2 == MotorState.Forward || Actuators.MP2 == MotorState.Backward;
|
||||
public bool P12_IsActive => Actuators.P12 == MotorState.Forward;
|
||||
public bool P3_IsActive => Actuators.P3 == MotorState.Forward;
|
||||
public bool P4_IsActive => Actuators.P4 == MotorState.Forward;
|
||||
public bool P5_IsActive => Actuators.P5 == MotorState.Forward;
|
||||
public bool P6_IsActive => Actuators.P6 == MotorState.Forward;
|
||||
|
||||
public bool Press1_HasClearance => Sensors.ClearancePress1;
|
||||
public bool Press2_HasClearance => Sensors.ClearancePress2;
|
||||
public bool TroughAuger_HasClearance => Rebler_IsActive && !(MW1_IsSelected && MW1_FillLevel == FillState.Full) && !(MW2_IsSelected && MW2_FillLevel == FillState.Full);
|
||||
|
||||
public readonly bool MP1_HasPathClearance { get; }
|
||||
public readonly bool MP2_HasPathClearance { get; }
|
||||
public readonly bool P12_HasPathClearance { get; }
|
||||
public readonly bool P3_HasPathClearance { get; }
|
||||
public readonly bool P4_HasPathClearance { get; }
|
||||
public readonly bool P5_HasPathClearance { get; }
|
||||
public readonly bool P6_HasPathClearance { get; }
|
||||
public readonly bool MP1_HasDryRunClearance { get; }
|
||||
public readonly bool MP2_HasDryRunClearance { get; }
|
||||
public readonly bool P12_HasDryRunClearance { get; }
|
||||
public readonly bool P3_HasDryRunClearance { get; }
|
||||
public readonly bool P4_HasDryRunClearance { get; }
|
||||
public readonly bool P5_HasDryRunClearance { get; }
|
||||
public readonly bool P6_HasDryRunClearance { get; }
|
||||
public bool MP1_HasClearance => MP1_HasDryRunClearance && MP1_HasPathClearance;
|
||||
public bool MP2_HasClearance => MP2_HasDryRunClearance && MP2_HasPathClearance;
|
||||
public bool P12_HasClearance => P12_HasDryRunClearance && P12_HasPathClearance;
|
||||
public bool P3_HasClearance => P3_HasDryRunClearance && P3_HasPathClearance;
|
||||
public bool P4_HasClearance => P4_HasDryRunClearance && P4_HasPathClearance;
|
||||
public bool P5_HasClearance => P4_HasDryRunClearance && P5_HasPathClearance;
|
||||
public bool P6_HasClearance => P6_HasDryRunClearance && P6_HasPathClearance;
|
||||
|
||||
public PamhagenState(LegacyPamhagenPlc.SensorStates sensors, Func<LegacyPamhagenPlc.ActuatorStates> actuatorsCb, bool overridePathClearances = false, bool overrideDryRunClearances = false) {
|
||||
Sensors = sensors;
|
||||
ActuatorsCb = actuatorsCb;
|
||||
|
||||
MP1_HasPathClearance = overridePathClearances || HasPathClearance(this, Paths.MP1);
|
||||
MP2_HasPathClearance = overridePathClearances || HasPathClearance(this, Paths.MP2);
|
||||
P12_HasPathClearance = overridePathClearances || HasPathClearance(this, Paths.P12);
|
||||
P3_HasPathClearance = overridePathClearances || HasPathClearance(this, Paths.P3);
|
||||
P4_HasPathClearance = overridePathClearances || HasPathClearance(this, Paths.P4);
|
||||
P5_HasPathClearance = overridePathClearances || HasPathClearance(this, Paths.P5);
|
||||
P6_HasPathClearance = overridePathClearances || HasPathClearance(this, Paths.P6);
|
||||
|
||||
MP1_HasDryRunClearance = overrideDryRunClearances || MW1_FillLevel != FillState.Empty;
|
||||
MP2_HasDryRunClearance = overrideDryRunClearances || MW2_FillLevel != FillState.Empty;
|
||||
P12_HasDryRunClearance = overrideDryRunClearances || T1_FillLevel != FillState.Empty || T2_FillLevel != FillState.Empty;
|
||||
P3_HasDryRunClearance = overrideDryRunClearances || T3_FillLevel != FillState.Empty;
|
||||
P4_HasDryRunClearance = overrideDryRunClearances || T4_FillLevel != FillState.Empty;
|
||||
P5_HasDryRunClearance = overrideDryRunClearances || T5_FillLevel != FillState.Empty;
|
||||
P6_HasDryRunClearance = true;
|
||||
}
|
||||
|
||||
public bool Valve_IsClosed(int n) => Sensors.GetDS(n);
|
||||
public bool Valve_IsOpen(int n) => !Valve_IsClosed(n);
|
||||
public bool Valve_WantOpen(int n) => Actuators.GetV(n);
|
||||
public bool Valve_WantClosed(int n) => !Valve_WantOpen(n);
|
||||
|
||||
private record struct ValveStates(int[] Open, int[] Closed) {
|
||||
public readonly ValveStateMask ToMask() => new(Open.Aggregate(0L, (m, n) => m | (1L << (n - 1))), Open.Concat(Closed).Aggregate(0L, (m, n) => m | (1L << (n - 1))));
|
||||
public static ValveStates operator +(ValveStates a, ValveStates b) {
|
||||
return new([.. a.Open, .. b.Open], [.. a.Closed, .. b.Closed]);
|
||||
}
|
||||
}
|
||||
|
||||
private record struct ValveStateMask(long States, long Mask) {
|
||||
public static ValveStateMask[] From(IEnumerable<ValveStates> states) => [.. states.Select(s => s.ToMask())];
|
||||
public static bool operator ==(long a, ValveStateMask b) => (a & b.Mask) == b.States;
|
||||
public static bool operator >=(long a, ValveStateMask b) => (a & b.States) == b.States;
|
||||
public static bool operator !=(long a, ValveStateMask b) => !(a == b);
|
||||
public static bool operator <=(long a, ValveStateMask b) => !(a >= b);
|
||||
}
|
||||
|
||||
private static bool HasSinglePathClearance(PamhagenState state, IEnumerable<ValveStateMask> masks) {
|
||||
return masks.Any(m => state.Valves == m && ~state.PressureSensors == m);
|
||||
}
|
||||
|
||||
private static bool HasAnyClearance(PamhagenState state, IEnumerable<ValveStateMask> masks) {
|
||||
return masks.Any(m => state.Valves >= m && ~state.PressureSensors >= m);
|
||||
}
|
||||
|
||||
private static bool HasPathClearance(PamhagenState state, (ValveStateMask[] Press1, ValveStateMask[] Press2, ValveStateMask[][] Tanks) masks) {
|
||||
return
|
||||
(HasSinglePathClearance(state, masks.Press1) && state.Press1_HasClearance) ||
|
||||
(HasSinglePathClearance(state, masks.Press2) && state.Press2_HasClearance) ||
|
||||
(HasSinglePathClearance(state, masks.Tanks[0]) && state.T1_FillLevel != FillState.Full) ||
|
||||
(HasSinglePathClearance(state, masks.Tanks[1]) && state.T2_FillLevel != FillState.Full) ||
|
||||
(HasSinglePathClearance(state, masks.Tanks[2]) && state.T3_FillLevel != FillState.Full) ||
|
||||
(HasSinglePathClearance(state, masks.Tanks[3]) && state.T4_FillLevel != FillState.Full) ||
|
||||
(HasSinglePathClearance(state, masks.Tanks[4]) && state.T5_FillLevel != FillState.Full) ||
|
||||
(HasSinglePathClearance(state, masks.Tanks[5]) && state.T6_FillLevel != FillState.Full) ||
|
||||
(HasSinglePathClearance(state, masks.Tanks[6]) && state.T7_FillLevel != FillState.Full) ||
|
||||
(HasSinglePathClearance(state, masks.Tanks[7]) && state.T8_FillLevel != FillState.Full) ||
|
||||
(HasSinglePathClearance(state, masks.Tanks[8]) && state.T9_FillLevel != FillState.Full) ||
|
||||
false;
|
||||
}
|
||||
|
||||
private static bool HasPathClearance(PamhagenState state, (ValveStateMask[] Press1, ValveStateMask[] Press2) masks) {
|
||||
return
|
||||
(HasSinglePathClearance(state, masks.Press1) && state.Press1_HasClearance) ||
|
||||
(HasSinglePathClearance(state, masks.Press2) && state.Press2_HasClearance) ||
|
||||
false;
|
||||
}
|
||||
|
||||
private static readonly (
|
||||
(ValveStateMask[] Press1, ValveStateMask[] Press2, ValveStateMask[][] Tanks) MP1,
|
||||
(ValveStateMask[] Press1, ValveStateMask[] Press2, ValveStateMask[][] Tanks) MP2,
|
||||
(ValveStateMask[] Press1, ValveStateMask[] Press2, ValveStateMask[][] Tanks) P12,
|
||||
(ValveStateMask[] Press1, ValveStateMask[] Press2, ValveStateMask[][] Tanks) P3,
|
||||
(ValveStateMask[] Press1, ValveStateMask[] Press2, ValveStateMask[][] Tanks) P4,
|
||||
(ValveStateMask[] Press1, ValveStateMask[] Press2, ValveStateMask[][] Tanks) P5,
|
||||
(ValveStateMask[] Press1, ValveStateMask[] Press2) P6
|
||||
) Paths;
|
||||
|
||||
static PamhagenState() {
|
||||
ValveStates[] wt_aa = [
|
||||
new([12], [15, 16, 20]), // bypass
|
||||
new([15, 18, 16], [11, 12, 14, 17, 19, 20]), // WT1
|
||||
new([15, 17, 20], [11, 12, 18, 19, 21]), // WT2
|
||||
new([15, 18, 19, 20], [11, 12, 14, 16, 17, 21]), // WT1+WT2
|
||||
];
|
||||
ValveStates[] wt_ab = [
|
||||
new([15, 18, 14], [11, 12, 13, 16, 17, 19, 21]), // WT1
|
||||
new([15, 17, 21], [11, 12, 13, 18, 19, 20]), // WT2
|
||||
new([15, 18, 19, 21], [11, 12, 13, 14, 16, 17, 20]), // WT1+WT2
|
||||
];
|
||||
ValveStates[] wt_ba = [
|
||||
new([11, 18, 16], [15, 12, 13, 14, 17, 19, 20]), // WT1
|
||||
new([11, 17, 20], [15, 12, 13, 18, 19, 21]), // WT2
|
||||
new([11, 18, 19, 20], [15, 12, 13, 14, 16, 17, 21]), // WT1+WT2
|
||||
];
|
||||
ValveStates[] wt_bb = [
|
||||
new([13], [11, 14, 21]), // bypass
|
||||
new([11, 18, 14], [15, 13, 16, 17, 19, 21]), // WT1
|
||||
new([11, 17, 21], [15, 13, 18, 19, 20]), // WT2
|
||||
new([11, 18, 19, 21], [15, 13, 14, 16, 17, 20]), // WT1+WT2
|
||||
];
|
||||
|
||||
ValveStates[][] tanks = [
|
||||
[new([22], [24, 26, 28]), new([23], [25, 27, 29])],
|
||||
[new([24], [22, 26, 28]), new([25], [23, 27, 29])],
|
||||
[new([26], [22, 24, 28]), new([27], [23, 25, 29])],
|
||||
[new([28, 30], [22, 24, 26, 32, 34]), new([29, 31], [23, 25, 27, 33, 35])],
|
||||
[new([28, 32], [22, 24, 26, 30, 34]), new([29, 33], [23, 25, 27, 31, 35])],
|
||||
[new([28, 34, 37], [22, 24, 26, 30, 32, 39, 41, 43]), new([29, 35, 36], [23, 25, 27, 31, 33, 38, 40, 42])],
|
||||
[new([28, 34, 39], [22, 24, 26, 30, 32, 37, 41, 43]), new([29, 35, 38], [23, 25, 27, 31, 33, 36, 40, 42])],
|
||||
[new([28, 34, 41], [22, 24, 26, 30, 32, 37, 39, 43]), new([29, 35, 40], [23, 25, 27, 31, 33, 36, 38, 42])],
|
||||
[new([28, 34, 43], [22, 24, 26, 30, 32, 37, 39, 41]), new([29, 35, 42], [23, 25, 27, 31, 33, 36, 38, 40])],
|
||||
];
|
||||
|
||||
ValveStates[] mp1_pr1 = [
|
||||
new([1, 6, 51], [3, 4, 5, 50, 52, 53, 55]),
|
||||
new([4, 8, 52], [1, 2, 7, 50, 51, 53, 56]),
|
||||
];
|
||||
ValveStates[] mp1_pr2 = [
|
||||
new([1, 6, 55], [3, 4, 5, 51, 54, 56, 57]),
|
||||
new([4, 8, 56], [1, 2, 7, 52, 54, 55, 57]),
|
||||
];
|
||||
ValveStates mp1_wt_a = new([1, 5], [3, 4, 6]);
|
||||
ValveStates mp1_wt_b = new([4, 7, 9], [1, 2, 8, 10]);
|
||||
ValveStates[] mp1_a = [.. wt_aa.Select(m => mp1_wt_a + m), .. wt_ba.Select(m => mp1_wt_b + m)];
|
||||
ValveStates[] mp1_b = [.. wt_ab.Select(m => mp1_wt_a + m), .. wt_bb.Select(m => mp1_wt_b + m)];
|
||||
ValveStates[][] mp1_t = [.. tanks.Select<ValveStates[], ValveStates[]>(t => [.. mp1_a.Select(m => m + t[0]), .. mp1_b.Select(m => m + t[1])])];
|
||||
|
||||
ValveStates[] mp2_pr1 = [
|
||||
new([2, 8, 52], [3, 4, 7, 50, 51, 53, 56]),
|
||||
new([3, 6, 51], [1, 2, 5, 50, 52, 53, 55]),
|
||||
];
|
||||
ValveStates[] mp2_pr2 = [
|
||||
new([2, 8, 56], [3, 4, 7, 52, 54, 55, 57]),
|
||||
new([3, 6, 55], [2, 1, 5, 51, 54, 56, 57]),
|
||||
];
|
||||
ValveStates mp2_wt_a = new([3, 5], [1, 2, 6]);
|
||||
ValveStates mp2_wt_b = new([2, 7, 9], [3, 4, 8, 10]);
|
||||
ValveStates[] mp2_a = [.. wt_aa.Select(m => mp2_wt_a + m), .. wt_ba.Select(m => mp2_wt_b + m)];
|
||||
ValveStates[] mp2_b = [.. wt_ab.Select(m => mp2_wt_a + m), .. wt_bb.Select(m => mp2_wt_b + m)];
|
||||
ValveStates[][] mp2_t = [.. tanks.Select<ValveStates[], ValveStates[]>(t => [.. mp2_a.Select(m => m + t[0]), .. mp2_b.Select(m => m + t[1])])];
|
||||
|
||||
ValveStates[] p12_pr1 = [new([44, 46, 49, 50], [10, 45, 47, 48, 54, 51, 52, 53])];
|
||||
ValveStates[] p12_pr2 = [new([44, 46, 49, 54], [10, 45, 47, 48, 50, 55, 56, 57])];
|
||||
ValveStates[] p12_b = [new([44, 10], [9, 45, 46])];
|
||||
ValveStates[][] p12_t = [.. tanks.Select<ValveStates[], ValveStates[]>(t => [.. p12_b.Select(m => m + t[1])])];
|
||||
|
||||
ValveStates[] p3_pr1 = [new([45, 46, 49, 50], [10, 44, 47, 48, 51, 52, 53, 54])];
|
||||
ValveStates[] p3_pr2 = [new([45, 46, 49, 54], [10, 44, 47, 48, 50, 55, 56, 57])];
|
||||
ValveStates[] p3_b = [new([45, 10], [9, 44, 46])];
|
||||
ValveStates[][] p3_t = [.. tanks.Select<ValveStates[], ValveStates[]>(t => [.. p3_b.Select(m => m + t[1])])];
|
||||
|
||||
ValveStates[] p4_pr1 = [new([47, 49, 50], [46, 48, 51, 52, 53, 54])];
|
||||
ValveStates[] p4_pr2 = [new([47, 49, 54], [46, 48, 50, 55, 56, 57])];
|
||||
ValveStates[] p4_b = [new([47, 46, 10], [9, 44, 45, 48, 49])];
|
||||
ValveStates[][] p4_t = [.. tanks.Select<ValveStates[], ValveStates[]>(t => [.. p4_b.Select(m => m + t[1])])];
|
||||
|
||||
ValveStates[] p5_pr1 = [new([48, 49, 50], [46, 47, 51, 52, 53, 54])];
|
||||
ValveStates[] p5_pr2 = [new([48, 49, 54], [46, 47, 50, 55, 56, 57])];
|
||||
ValveStates[] p5_b = [new([48, 46, 10], [9, 44, 45, 47, 49])];
|
||||
ValveStates[][] p5_t = [.. tanks.Select<ValveStates[], ValveStates[]>(t => [.. p5_b.Select(m => m + t[1])])];
|
||||
|
||||
ValveStates[] p6_pr1 = [new([53], [50, 51, 52, 57])];
|
||||
ValveStates[] p6_pr2 = [new([57], [53, 54, 55, 56])];
|
||||
|
||||
Paths = (
|
||||
(ValveStateMask.From(mp1_pr1), ValveStateMask.From(mp1_pr2), [.. mp1_t.Select(t => ValveStateMask.From(t))]),
|
||||
(ValveStateMask.From(mp2_pr1), ValveStateMask.From(mp2_pr2), [.. mp2_t.Select(t => ValveStateMask.From(t))]),
|
||||
(ValveStateMask.From(p12_pr1), ValveStateMask.From(p12_pr2), [.. p12_t.Select(t => ValveStateMask.From(t))]),
|
||||
(ValveStateMask.From(p3_pr1), ValveStateMask.From(p3_pr2), [.. p3_t.Select(t => ValveStateMask.From(t))]),
|
||||
(ValveStateMask.From(p4_pr1), ValveStateMask.From(p4_pr2), [.. p4_t.Select(t => ValveStateMask.From(t))]),
|
||||
(ValveStateMask.From(p5_pr1), ValveStateMask.From(p5_pr2), [.. p5_t.Select(t => ValveStateMask.From(t))]),
|
||||
(ValveStateMask.From(p6_pr1), ValveStateMask.From(p6_pr2))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Text;
|
||||
|
||||
namespace PamhagenSysCtrl.Helpers.Plc {
|
||||
public class T1 : IDisposable {
|
||||
|
||||
public enum Mode { HALT = 1, RUN = 2, RUN_F = 3, HOLD = 4, ERROR = 6 }
|
||||
public readonly record struct Status(Mode Mode, int Flags);
|
||||
|
||||
protected SerialPort Serial;
|
||||
|
||||
public string PortName => Serial.PortName;
|
||||
public Status LastStatus { get; private set; }
|
||||
public T1Error LastError { get; private set; }
|
||||
|
||||
public T1(string portName, Parity parity = Parity.Odd) {
|
||||
Serial = new SerialPort(portName, 9600, parity, 8, StopBits.One) {
|
||||
Handshake = Handshake.None,
|
||||
NewLine = "\r",
|
||||
ReadTimeout = 500,
|
||||
WriteTimeout = 500,
|
||||
DtrEnable = true,
|
||||
RtsEnable = true,
|
||||
Encoding = Encoding.ASCII,
|
||||
};
|
||||
Serial.Open();
|
||||
Serial.DiscardInBuffer();
|
||||
Serial.DiscardOutBuffer();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
Serial.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected async Task SendCommand(string cmd, string? data = null, bool checksum = true) {
|
||||
// max 255 bytes
|
||||
var line = new StringBuilder("(A", 256);
|
||||
line.AppendFormat("{0:00}{1}", 1, cmd);
|
||||
if (data != null) {
|
||||
if (data.Length > 244) throw new ArgumentException("T1 playload too long");
|
||||
line.Append(data);
|
||||
}
|
||||
if (checksum) {
|
||||
line.Append('&');
|
||||
var chk = Enumerable.Range(0, line.Length).Aggregate(0, (c, i) => c + line[i]) & 0xFF;
|
||||
line.AppendFormat("{0:X2}", chk);
|
||||
}
|
||||
line.Append(')');
|
||||
Serial.WriteLine(line.ToString());
|
||||
}
|
||||
|
||||
protected async Task<(string Command, string Data)> ReceiveResponse(string? expectedCmd = null) {
|
||||
var line = Serial.ReadLine();
|
||||
if (line.Length < 8 || !line.StartsWith("(A") || !line.EndsWith(')') || line[^4] != '&') {
|
||||
throw new FormatException("Invalid response from PLC (T1)");
|
||||
}
|
||||
|
||||
var chk1 = Convert.ToInt32(line[^3..^1], 16);
|
||||
var chk2 = Enumerable.Range(0, line.Length - 3).Aggregate(0, (c, i) => c + line[i]) & 0xFF;
|
||||
if (chk1 != chk2) {
|
||||
throw new IOException($"Ungültige Prüfsumme von SPS (T1)");
|
||||
}
|
||||
|
||||
var (cmd, data) = (line[4..6], line[6..^4]);
|
||||
if (cmd == "ST" && data[3] == '6') {
|
||||
// TODO check for other error bits?
|
||||
await SendCommand("ER");
|
||||
var (_, res) = await ReceiveResponse("ER");
|
||||
int code = Convert.ToInt32(res);
|
||||
var error = Enum.IsDefined(typeof(T1Error), code) ? (T1Error)code : T1Error.UNKNOWN_ERROR;
|
||||
throw new T1Exception($"PLC Error ({data}): {error.GetName()}" + (error.GetDescription() != null ? $" ({error.GetDescription()})" : ""));
|
||||
} else if (cmd == expectedCmd) {
|
||||
return (cmd, data);
|
||||
} else if (cmd == "CE") {
|
||||
int code = Convert.ToInt32(data);
|
||||
throw code switch {
|
||||
1 => new T1Exception($"Computer Link Error ({data}): Command error (Received command is illegal)"),
|
||||
2 => new T1Exception($"Computer Link Error ({data}): Format error (Received message format is invalid)"),
|
||||
3 => new T1Exception($"Computer Link Error ({data}): Checksum error (Checksum mismatch is detected)"),
|
||||
_ => new T1Exception($"Computer Link Error ({data}): Unknown error"),
|
||||
};
|
||||
} else if (cmd == "EE") {
|
||||
int code = Convert.ToInt32(data);
|
||||
var error = Enum.IsDefined(typeof(T1Error), code) ? (T1Error)code : T1Error.UNKNOWN_ERROR;
|
||||
throw new T1Exception($"PLC Error ({data}): {error.GetName()}" + (error.GetDescription() != null ? $" ({error.GetDescription()})" : ""));
|
||||
} else if (expectedCmd != null) {
|
||||
throw new T1Exception($"Unexpected response from PLC (T1): {cmd} {data}");
|
||||
}
|
||||
|
||||
return (cmd, data);
|
||||
}
|
||||
|
||||
public async Task Test(string data = "_Ping_Pong_0123456789_abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_") {
|
||||
await SendCommand("TS", data);
|
||||
var (_, res) = await ReceiveResponse("TS");
|
||||
if (data.Replace(" ", "") != res) throw new T1Exception("Wrong test response from PLC (T1)");
|
||||
}
|
||||
|
||||
public async Task<Status> StatusRead() {
|
||||
await SendCommand("ST", checksum: false);
|
||||
var (_, res) = await ReceiveResponse("ST");
|
||||
LastStatus = new Status((Mode)(res[3] - '0'), ((res[0] - '0') << 4) | (res[1] - '0'));
|
||||
return LastStatus;
|
||||
}
|
||||
|
||||
public async Task<T1Error> ErrorStatusRead() {
|
||||
await SendCommand("ER", checksum: false);
|
||||
var (_, res) = await ReceiveResponse("ER");
|
||||
var code = Convert.ToInt32(res, 10);
|
||||
LastError = Enum.IsDefined(typeof(T1Error), code) ? (T1Error)code : T1Error.UNKNOWN_ERROR;
|
||||
return LastError;
|
||||
}
|
||||
|
||||
public Task<ushort[]> DataRead(string register, int length) {
|
||||
return DataRead((register, length));
|
||||
}
|
||||
|
||||
public async Task<ushort[]> DataRead(params (string Register, int Length)[] registers) {
|
||||
await SendCommand("DR", string.Join(',', registers.Select(r => $"{r.Register},{r.Length}")));
|
||||
var (_, res) = await ReceiveResponse("DR");
|
||||
if (res.Length != registers.Sum(r => r.Length) * 4) throw new T1Exception("Invalid response length from PLC (T1)");
|
||||
return [.. Enumerable.Range(0, res.Length / 4).Select(i => Convert.ToUInt16(res.Substring(i * 4, 4), 16))];
|
||||
}
|
||||
|
||||
public Task<Status> DataWrite(string register, params ushort[] values) {
|
||||
return DataWrite((register, values));
|
||||
}
|
||||
|
||||
public async Task<Status> DataWrite(params (string Register, ushort[] Values)[] registers) {
|
||||
await SendCommand("DW", string.Join(',', registers.Select(r => $"{r.Register},{r.Values.Length}," + string.Join(',', r.Values.Select(v => $"{v:X4}")))));
|
||||
var (_, res) = await ReceiveResponse("ST");
|
||||
return new Status((Mode)(res[3] - '0'), ((res[0] - '0') << 4) | (res[1] - '0'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
namespace PamhagenSysCtrl.Helpers.Plc {
|
||||
public enum T1Error {
|
||||
UNKNOWN_ERROR = -1,
|
||||
NO_ERROR = 0,
|
||||
SYSTEM_POWER_ON = 10,
|
||||
SYSTEM_POWER_OFF = 11,
|
||||
RAM_CHECK_ERROR = 20,
|
||||
PROGRAM_BCC_ERROR = 21,
|
||||
BATTERY_VOLTAGE_DROP = 22,
|
||||
EEPROM_BCC_ERROR = 23,
|
||||
EEPROM_WARNING = 26,
|
||||
SYSTEM_RAM_CHECK_ERROR = 30,
|
||||
SYSTEM_ROM_BCC_ERROR = 31,
|
||||
PERIPHERAL_LSI_ERROR = 32,
|
||||
CLOCK_CALENDAR_CHECK_ERROR = 33,
|
||||
ILLEGAL_SYSTEM_INTERRUPT = 34,
|
||||
WD_TIMER_ERROR = 35,
|
||||
IO_BUS_ERROR = 40,
|
||||
IO_MISMATCH = 41,
|
||||
IO_NO_ANSWER = 42,
|
||||
IO_PARITY_ERROR = 43,
|
||||
ILLEGAL_IO_REGISTER = 46,
|
||||
COMMUNICATION_BUSY = 51,
|
||||
FORMAT_ERROR = 52,
|
||||
SCAN_TIME_OVER = 64,
|
||||
NO_END_IRET_ERROR = 80,
|
||||
PAIR_INSTRUCTION_ERROR = 81,
|
||||
OPERAND_ERROR = 82,
|
||||
INVALID_PROGRAM = 83,
|
||||
NO_SUBROUTINE_ENTRY = 86,
|
||||
NO_RET_ERROR = 87,
|
||||
SUBROUTINE_NESTING_ERROR = 88,
|
||||
LOOP_NESTING_ERROR = 89,
|
||||
INVALID_FUNCTION_INSTRUCTION = 98,
|
||||
PASSWORD_PROTECT = 106,
|
||||
ILLEGAL_INSTRUCTION = 110,
|
||||
REGISTER_ADDRESS_ERROR = 111,
|
||||
BOUNDARY_ERROR = 112,
|
||||
MEMORY_FULL = 113,
|
||||
MODE_MISMATCH = 114,
|
||||
REGISTER_ADDRESS_SIZE_ERROR = 115,
|
||||
DUPLICATE_ENTRY_NR = 121,
|
||||
}
|
||||
|
||||
public static class T1ErrorExtension {
|
||||
public static bool IsError(this T1Error error) {
|
||||
return error switch {
|
||||
T1Error.NO_ERROR => false,
|
||||
T1Error.SYSTEM_POWER_ON => false,
|
||||
T1Error.SYSTEM_POWER_OFF => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
public static string GetName(this T1Error error) {
|
||||
return error switch {
|
||||
T1Error.UNKNOWN_ERROR => "Unknown error",
|
||||
T1Error.NO_ERROR => "No error",
|
||||
T1Error.SYSTEM_POWER_ON => "System power on",
|
||||
T1Error.SYSTEM_POWER_OFF => "System power off",
|
||||
T1Error.RAM_CHECK_ERROR => "RAM check error",
|
||||
T1Error.PROGRAM_BCC_ERROR => "Program BCC error",
|
||||
T1Error.BATTERY_VOLTAGE_DROP => "Battery voltage drop",
|
||||
T1Error.EEPROM_BCC_ERROR => "EEPROM BCC error",
|
||||
T1Error.EEPROM_WARNING => "EEPROM warning",
|
||||
T1Error.SYSTEM_RAM_CHECK_ERROR => "System RAM check error",
|
||||
T1Error.SYSTEM_ROM_BCC_ERROR => "System ROM BCC error",
|
||||
T1Error.PERIPHERAL_LSI_ERROR => "Peripheral LSI error",
|
||||
T1Error.CLOCK_CALENDAR_CHECK_ERROR => "Clock-calendar check error",
|
||||
T1Error.ILLEGAL_SYSTEM_INTERRUPT => "Illegal system interrupt",
|
||||
T1Error.WD_TIMER_ERROR => "WD timer error",
|
||||
T1Error.IO_BUS_ERROR => "I/O bus error",
|
||||
T1Error.IO_MISMATCH => "I/O mismatch",
|
||||
T1Error.IO_NO_ANSWER => "I/O no answer",
|
||||
T1Error.IO_PARITY_ERROR => "I/O parity error",
|
||||
T1Error.ILLEGAL_IO_REGISTER => "Illegal I/O register",
|
||||
T1Error.COMMUNICATION_BUSY => "Communication busy",
|
||||
T1Error.FORMAT_ERROR => "Format error",
|
||||
T1Error.SCAN_TIME_OVER => "Scan time over",
|
||||
T1Error.NO_END_IRET_ERROR => "No END/IRET error",
|
||||
T1Error.PAIR_INSTRUCTION_ERROR => "Pair instruction error",
|
||||
T1Error.OPERAND_ERROR => "Operand error",
|
||||
T1Error.INVALID_PROGRAM => "Invalid program",
|
||||
T1Error.NO_SUBROUTINE_ENTRY => "No subroutine entry",
|
||||
T1Error.NO_RET_ERROR => "No RET error",
|
||||
T1Error.SUBROUTINE_NESTING_ERROR => "Subroutine nesting error",
|
||||
T1Error.LOOP_NESTING_ERROR => "Loop nesting error",
|
||||
T1Error.INVALID_FUNCTION_INSTRUCTION => "Invalid function instruction",
|
||||
T1Error.PASSWORD_PROTECT => "Password protect",
|
||||
T1Error.ILLEGAL_INSTRUCTION => "Illegal instruction",
|
||||
T1Error.REGISTER_ADDRESS_ERROR => "Register address error",
|
||||
T1Error.BOUNDARY_ERROR => "Boundary error",
|
||||
T1Error.MEMORY_FULL => "Memory full",
|
||||
T1Error.MODE_MISMATCH => "Mode mismatch",
|
||||
T1Error.REGISTER_ADDRESS_SIZE_ERROR => "Register address/size error",
|
||||
T1Error.DUPLICATE_ENTRY_NR => "Duplicate entry No.",
|
||||
_ => error.ToString(),
|
||||
};
|
||||
}
|
||||
public static string? GetDescription(this T1Error error) {
|
||||
return error switch {
|
||||
T1Error.NO_ERROR => "No error recorded",
|
||||
T1Error.SYSTEM_POWER_ON => "Power on (no error)",
|
||||
T1Error.SYSTEM_POWER_OFF => "Power off (no error)",
|
||||
T1Error.RAM_CHECK_ERROR => "RAM read/write error has been detected",
|
||||
T1Error.PROGRAM_BCC_ERROR => "Program BCC (memory check code) error has been detected",
|
||||
T1Error.BATTERY_VOLTAGE_DROP => "Data invalidity of RAM (back-up area) has been detected",
|
||||
T1Error.EEPROM_BCC_ERROR => "BCC error of built-in EEPROM has been detected",
|
||||
T1Error.EEPROM_WARNING => "EEPROM write operation has exceeded 100,000 times",
|
||||
T1Error.SYSTEM_RAM_CHECK_ERROR => "System RAM read/write error has been detected",
|
||||
T1Error.SYSTEM_ROM_BCC_ERROR => "BCC error of system ROM has been detected",
|
||||
T1Error.PERIPHERAL_LSI_ERROR => "CPU hardware error has been detected",
|
||||
T1Error.CLOCK_CALENDAR_CHECK_ERROR => "Invalid clock-calendar data has been detected",
|
||||
T1Error.ILLEGAL_SYSTEM_INTERRUPT => "Unregistered interrupt has occurred",
|
||||
T1Error.WD_TIMER_ERROR => "Watchdog timer error has occurred",
|
||||
T1Error.IO_BUS_ERROR => "I/O bus error has been detected",
|
||||
T1Error.IO_MISMATCH => "Registered I/O allocation table and actual I/O configuration are not identical",
|
||||
T1Error.IO_NO_ANSWER => "No response from I/O module has been received",
|
||||
T1Error.IO_PARITY_ERROR => "I/O bus parity error has been detected",
|
||||
T1Error.ILLEGAL_IO_REGISTER => "Excess I/O register allocation has been detected",
|
||||
T1Error.COMMUNICATION_BUSY => "The T1/T1S is busy in processing for other peripheral communications",
|
||||
T1Error.FORMAT_ERROR => "Received request is invalid (detected by the T1/T1S)",
|
||||
T1Error.SCAN_TIME_OVER => "Scan time has exceeded 200 ms",
|
||||
T1Error.NO_END_IRET_ERROR => "END or IRET instruction has not been programmed",
|
||||
T1Error.PAIR_INSTRUCTION_ERROR => "Illegal combination of pair instructions has been programmed",
|
||||
T1Error.OPERAND_ERROR => "Illegal operand has been detected",
|
||||
T1Error.INVALID_PROGRAM => "Program abnormality has been detected",
|
||||
T1Error.NO_SUBROUTINE_ENTRY => "Subroutine corresponding to CALL instruction has not been programmed",
|
||||
T1Error.NO_RET_ERROR => "RET (subroutine return) instruction has not been in a subroutine",
|
||||
T1Error.SUBROUTINE_NESTING_ERROR => "CALL instruction has been programmed in a subroutine (subroutine nesting)",
|
||||
T1Error.LOOP_NESTING_ERROR => "Nesting of FOR-NEXT loop has been programmed",
|
||||
T1Error.INVALID_FUNCTION_INSTRUCTION => "Function instruction which is not supported by T1/T1S has been programmed",
|
||||
T1Error.PASSWORD_PROTECT => "Requested operation is protected by password",
|
||||
T1Error.ILLEGAL_INSTRUCTION => "Illegal instruction has been detected",
|
||||
T1Error.REGISTER_ADDRESS_ERROR => "Excess register address range has been programmed",
|
||||
T1Error.BOUNDARY_ERROR => "Illegal register address is designated by index modification",
|
||||
T1Error.MEMORY_FULL => "Program memory is insufficient for the requested command",
|
||||
T1Error.MODE_MISMATCH => "Received command is invalid in the current T1/T1S operation mode",
|
||||
T1Error.REGISTER_ADDRESS_SIZE_ERROR => "Specified register range exceeds the limit",
|
||||
T1Error.DUPLICATE_ENTRY_NR => "Multiple subroutines which has same subroutine number have been programmed",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace PamhagenSysCtrl.Helpers.Plc {
|
||||
public class T1Exception(string msg) : Exception(msg) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user