更新20260521
This commit is contained in:
@@ -16,6 +16,8 @@ public interface ITcpDeviceConnectionService : IAsyncDisposable
|
||||
|
||||
bool TryReadFloat(ushort registerAddress, out double value);
|
||||
|
||||
bool TryReadFloatValues(ushort registerAddress, out ModbusFloatReadResult result);
|
||||
|
||||
bool TryReadInt16(ushort registerAddress, out int value);
|
||||
|
||||
bool TryWriteInt16(ushort registerAddress, short value);
|
||||
|
||||
45
ConeCalorimeter/Services/ModbusFloatReadResult.cs
Normal file
45
ConeCalorimeter/Services/ModbusFloatReadResult.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace ConeCalorimeter.Services;
|
||||
|
||||
public enum ModbusFloatByteOrder
|
||||
{
|
||||
Abcd,
|
||||
Cdab,
|
||||
Badc,
|
||||
Dcba
|
||||
}
|
||||
|
||||
public readonly record struct ModbusFloatReadResult(byte A, byte B, byte C, byte D)
|
||||
{
|
||||
public string RawHex => $"{A:X2} {B:X2} {C:X2} {D:X2}";
|
||||
|
||||
public double Abcd => Decode(A, B, C, D);
|
||||
|
||||
public double Cdab => Decode(C, D, A, B);
|
||||
|
||||
public double Badc => Decode(B, A, D, C);
|
||||
|
||||
public double Dcba => Decode(D, C, B, A);
|
||||
|
||||
public double GetValue(ModbusFloatByteOrder byteOrder)
|
||||
{
|
||||
return byteOrder switch
|
||||
{
|
||||
ModbusFloatByteOrder.Abcd => Abcd,
|
||||
ModbusFloatByteOrder.Cdab => Cdab,
|
||||
ModbusFloatByteOrder.Badc => Badc,
|
||||
ModbusFloatByteOrder.Dcba => Dcba,
|
||||
_ => double.NaN
|
||||
};
|
||||
}
|
||||
|
||||
private static double Decode(byte b0, byte b1, byte b2, byte b3)
|
||||
{
|
||||
var rawValue = unchecked((int)(
|
||||
((uint)b0 << 24)
|
||||
| ((uint)b1 << 16)
|
||||
| ((uint)b2 << 8)
|
||||
| b3));
|
||||
|
||||
return BitConverter.Int32BitsToSingle(rawValue);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using ConeCalorimeter.Models;
|
||||
|
||||
namespace ConeCalorimeter.Services;
|
||||
@@ -23,8 +24,16 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
private const ushort IgnitionSecondsRegister = 1014;
|
||||
private const ushort TestSecondsRegister = 1015;
|
||||
private const ushort M3FlameMonitorBit = 3;
|
||||
private static readonly ModbusFloatByteOrder[] AlternateFloatByteOrders =
|
||||
[
|
||||
ModbusFloatByteOrder.Cdab,
|
||||
ModbusFloatByteOrder.Badc,
|
||||
ModbusFloatByteOrder.Dcba
|
||||
];
|
||||
|
||||
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||
private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
|
||||
private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];
|
||||
|
||||
public ModbusRealtimeDataService(ITcpDeviceConnectionService tcpDeviceConnectionService)
|
||||
{
|
||||
@@ -34,24 +43,24 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
public RealtimeSnapshot GetCurrentSnapshot(TimeSpan elapsed)
|
||||
{
|
||||
return new RealtimeSnapshot(
|
||||
OrificeFlow: ReadFloatOrEmpty(OrificeFlowRegister),
|
||||
OrificePressure: ReadFloatOrEmpty(OrificePressureRegister),
|
||||
OrificeTemperature: ReadFloatOrEmpty(OrificeTemperatureRegister),
|
||||
ConeTemperature: ReadFloatOrEmpty(ConeTemperatureRegister),
|
||||
SampleTemperature: ReadFloatOrEmpty(SampleTemperatureRegister),
|
||||
Irradiance: ReadFloatOrEmpty(IrradianceRegister),
|
||||
OrificeFlow: ReadRangedFloatOrEmpty("OrificeFlow", OrificeFlowRegister, 0, 10),
|
||||
OrificePressure: ReadRangedFloatOrEmpty("OrificePressure", OrificePressureRegister, -5000, 5000),
|
||||
OrificeTemperature: ReadRangedFloatOrEmpty("OrificeTemperature", OrificeTemperatureRegister, 200, 700),
|
||||
ConeTemperature: ReadRangedFloatOrEmpty("ConeTemperature", ConeTemperatureRegister, 0, 1200),
|
||||
SampleTemperature: ReadRangedFloatOrEmpty("SampleTemperature", SampleTemperatureRegister, -50, 1200),
|
||||
Irradiance: ReadRangedFloatOrEmpty("Irradiance", IrradianceRegister, 0, 100),
|
||||
FlameDetected: ReadCoilOrFalse(M3FlameMonitorBit),
|
||||
Oxygen: ReadFloatOrEmpty(OxygenRegister),
|
||||
CarbonDioxide: ReadFloatOrEmpty(CarbonDioxideRegister),
|
||||
CarbonMonoxide: ReadFloatOrEmpty(CarbonMonoxideRegister),
|
||||
HeatReleaseRate: ReadFloatOrEmpty(HeatReleaseRateRegister),
|
||||
PeakHeatReleaseRate: ReadFloatOrEmpty(PeakHeatReleaseRateRegister),
|
||||
CFactor: ReadFloatOrEmpty(CFactorRegister),
|
||||
Qa180: ReadFloatOrEmpty(Qa180Register),
|
||||
Qa300: ReadFloatOrEmpty(Qa300Register),
|
||||
Oxygen: ReadRangedFloatOrEmpty("O2", OxygenRegister, 0, 30),
|
||||
CarbonDioxide: ReadRangedFloatOrEmpty("CO2", CarbonDioxideRegister, 0, 20),
|
||||
CarbonMonoxide: ReadRangedFloatOrEmpty("CO", CarbonMonoxideRegister, 0, 10),
|
||||
HeatReleaseRate: ReadRangedFloatOrEmpty("HeatReleaseRate", HeatReleaseRateRegister, 0, 5000),
|
||||
PeakHeatReleaseRate: ReadRangedFloatOrEmpty("PeakHeatReleaseRate", PeakHeatReleaseRateRegister, 0, 5000),
|
||||
CFactor: ReadRangedFloatOrEmpty("CFactor", CFactorRegister, 0, 1000),
|
||||
Qa180: ReadRangedFloatOrEmpty("Qa180", Qa180Register, 0, 1000),
|
||||
Qa300: ReadRangedFloatOrEmpty("Qa300", Qa300Register, 0, 1000),
|
||||
TotalHeatRelease: double.NaN,
|
||||
SmokeProduction: ReadFloatOrEmpty(SmokeProductionRegister),
|
||||
CurrentMass: ReadFloatOrEmpty(CurrentMassRegister),
|
||||
SmokeProduction: ReadRangedFloatOrEmpty("SmokeProduction", SmokeProductionRegister, 0, 100),
|
||||
CurrentMass: ReadRangedFloatOrEmpty("CurrentMass", CurrentMassRegister, 0, 100000),
|
||||
InitialMass: double.NaN,
|
||||
MassLoss: double.NaN,
|
||||
MassLossRate: double.NaN,
|
||||
@@ -60,11 +69,93 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
TotalSmoke: double.NaN);
|
||||
}
|
||||
|
||||
private double ReadFloatOrEmpty(ushort registerAddress)
|
||||
private double ReadRangedFloatOrEmpty(string label, ushort registerAddress, double minimum, double maximum)
|
||||
{
|
||||
return _tcpDeviceConnectionService.TryReadFloat(registerAddress, out var value)
|
||||
? value
|
||||
: double.NaN;
|
||||
if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
if (!TrySelectRangedFloat(result, minimum, maximum, out var byteOrder, out var value, out var matchCount))
|
||||
{
|
||||
LogFloatDiagnostic(label, registerAddress, result, null, matchCount);
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount);
|
||||
return NormalizeRealtimeValue(value);
|
||||
}
|
||||
|
||||
private static bool TrySelectRangedFloat(
|
||||
ModbusFloatReadResult result,
|
||||
double minimum,
|
||||
double maximum,
|
||||
out ModbusFloatByteOrder byteOrder,
|
||||
out double value,
|
||||
out int matchCount)
|
||||
{
|
||||
var alternateMatches = AlternateFloatByteOrders
|
||||
.Where(candidate => IsInRange(result.GetValue(candidate), minimum, maximum))
|
||||
.ToArray();
|
||||
var isAbcdInRange = IsInRange(result.Abcd, minimum, maximum);
|
||||
|
||||
matchCount = alternateMatches.Length + (isAbcdInRange ? 1 : 0);
|
||||
|
||||
if (isAbcdInRange)
|
||||
{
|
||||
byteOrder = ModbusFloatByteOrder.Abcd;
|
||||
value = result.Abcd;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alternateMatches.Length != 1)
|
||||
{
|
||||
byteOrder = default;
|
||||
value = double.NaN;
|
||||
return false;
|
||||
}
|
||||
|
||||
byteOrder = alternateMatches[0];
|
||||
value = result.GetValue(byteOrder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void LogFloatDiagnostic(
|
||||
string label,
|
||||
ushort registerAddress,
|
||||
ModbusFloatReadResult result,
|
||||
ModbusFloatByteOrder? selectedByteOrder,
|
||||
int matchCount)
|
||||
{
|
||||
if (selectedByteOrder is null)
|
||||
{
|
||||
if (!_loggedInvalidFloatDiagnostics.Add(registerAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!_loggedFloatDiagnostics.Add(registerAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedText = selectedByteOrder?.ToString() ?? "none";
|
||||
|
||||
Debug.WriteLine(
|
||||
$"Realtime float {label} register {registerAddress} raw [{result.RawHex}], "
|
||||
+ $"ABCD={result.Abcd:G9}, CDAB={result.Cdab:G9}, "
|
||||
+ $"BADC={result.Badc:G9}, DCBA={result.Dcba:G9}, "
|
||||
+ $"selected={selectedText}, matches={matchCount}.");
|
||||
}
|
||||
|
||||
private static bool IsInRange(double value, double minimum, double maximum)
|
||||
{
|
||||
return double.IsFinite(value) && value >= minimum && value <= maximum;
|
||||
}
|
||||
|
||||
private static double NormalizeRealtimeValue(double value)
|
||||
{
|
||||
return Math.Abs(value) < 0.005 ? 0 : value;
|
||||
}
|
||||
|
||||
private int ReadInt16OrEmpty(ushort registerAddress)
|
||||
|
||||
@@ -146,6 +146,33 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReadFloatValues(ushort registerAddress, out ModbusFloatReadResult result)
|
||||
{
|
||||
result = default;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_client is null || !IsTcpClientConnected(_client))
|
||||
{
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = ReadFloatValues(_client, registerAddress);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device register {registerAddress} float values read failed: {ex.Message}");
|
||||
SetConnectionState(false, $"读取寄存器 {registerAddress} 失败:{ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReadInt16(ushort registerAddress, out int value)
|
||||
{
|
||||
value = 0;
|
||||
@@ -393,6 +420,11 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
}
|
||||
|
||||
private double ReadFloat(TcpClient client, ushort registerAddress)
|
||||
{
|
||||
return ReadFloatValues(client, registerAddress).Abcd;
|
||||
}
|
||||
|
||||
private ModbusFloatReadResult ReadFloatValues(TcpClient client, ushort registerAddress)
|
||||
{
|
||||
var pdu = ReadHoldingRegisters(client, registerAddress, 2);
|
||||
|
||||
@@ -401,8 +433,7 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
throw new InvalidDataException("Invalid Modbus TCP float response.");
|
||||
}
|
||||
|
||||
var rawValue = BinaryPrimitives.ReadInt32BigEndian(pdu[2..6]);
|
||||
return BitConverter.Int32BitsToSingle(rawValue);
|
||||
return new ModbusFloatReadResult(pdu[2], pdu[3], pdu[4], pdu[5]);
|
||||
}
|
||||
|
||||
private int ReadInt16(TcpClient client, ushort registerAddress)
|
||||
|
||||
@@ -14,6 +14,9 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
private const ushort PressureDifferenceRegister = 284;
|
||||
private const ushort OxygenRegister = 286;
|
||||
private const ushort CValueRegister = 308;
|
||||
private const string BaselineCollectionAction = "基线采集";
|
||||
private const string CalibrationStartAction = "标定开始";
|
||||
private static readonly TimeSpan PulseDelay = TimeSpan.FromMilliseconds(300);
|
||||
|
||||
private readonly Action _closeAction;
|
||||
private readonly Action _helpAction;
|
||||
@@ -117,6 +120,13 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
}
|
||||
|
||||
LastAction = action;
|
||||
|
||||
if (IsPulseAction(action))
|
||||
{
|
||||
_ = PulseActionCoilAsync(action);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteActionCoil(action);
|
||||
}
|
||||
|
||||
@@ -151,6 +161,27 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PulseActionCoilAsync(string action)
|
||||
{
|
||||
if (!TryGetActionCoil(action, out var coilAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
|
||||
{
|
||||
Debug.WriteLine($"C value calibration action '{action}' pulse start failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(PulseDelay);
|
||||
|
||||
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, false))
|
||||
{
|
||||
Debug.WriteLine($"C value calibration action '{action}' pulse reset failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetActionCoil(string action, out ushort coilAddress)
|
||||
{
|
||||
switch (action)
|
||||
@@ -167,10 +198,10 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
case "取样泵":
|
||||
coilAddress = 50;
|
||||
return true;
|
||||
case "基线采集":
|
||||
case BaselineCollectionAction:
|
||||
coilAddress = 60;
|
||||
return true;
|
||||
case "标定开始":
|
||||
case CalibrationStartAction:
|
||||
coilAddress = 70;
|
||||
return true;
|
||||
default:
|
||||
@@ -178,4 +209,9 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPulseAction(string action)
|
||||
{
|
||||
return action is BaselineCollectionAction or CalibrationStartAction;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +245,34 @@ public sealed class TestPageViewModel : PageViewModel
|
||||
}
|
||||
}
|
||||
|
||||
public bool StartMomentaryDeviceAction(string? action)
|
||||
{
|
||||
return TryWriteMomentaryDeviceAction(action, true);
|
||||
}
|
||||
|
||||
public bool StopMomentaryDeviceAction(string? action)
|
||||
{
|
||||
return TryWriteMomentaryDeviceAction(action, false);
|
||||
}
|
||||
|
||||
private bool TryWriteMomentaryDeviceAction(string? action, bool isRunning)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(action)
|
||||
|| !TryGetMomentaryDeviceActionCoil(action, out var coilAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
LastAction = isRunning ? action : $"停止:{action}";
|
||||
|
||||
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, isRunning))
|
||||
{
|
||||
Debug.WriteLine($"Momentary device action '{action}' write failed.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ExecuteDeviceAction(string? action)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(action))
|
||||
@@ -252,6 +280,11 @@ public sealed class TestPageViewModel : PageViewModel
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsMomentaryDeviceAction(action))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LastAction = action;
|
||||
|
||||
if (action == "测试开始")
|
||||
@@ -292,18 +325,6 @@ public sealed class TestPageViewModel : PageViewModel
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "称重台升":
|
||||
coilAddress = 93;
|
||||
return true;
|
||||
case "称重台降":
|
||||
coilAddress = 94;
|
||||
return true;
|
||||
case "辐射锥升":
|
||||
coilAddress = 83;
|
||||
return true;
|
||||
case "辐射锥降":
|
||||
coilAddress = 84;
|
||||
return true;
|
||||
case "复位":
|
||||
coilAddress = 88;
|
||||
return true;
|
||||
@@ -328,4 +349,31 @@ public sealed class TestPageViewModel : PageViewModel
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsMomentaryDeviceAction(string action)
|
||||
{
|
||||
return TryGetMomentaryDeviceActionCoil(action, out _);
|
||||
}
|
||||
|
||||
private static bool TryGetMomentaryDeviceActionCoil(string action, out ushort coilAddress)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "称重台升":
|
||||
coilAddress = 93;
|
||||
return true;
|
||||
case "称重台降":
|
||||
coilAddress = 94;
|
||||
return true;
|
||||
case "辐射锥升":
|
||||
coilAddress = 83;
|
||||
return true;
|
||||
case "辐射锥降":
|
||||
coilAddress = 84;
|
||||
return true;
|
||||
default:
|
||||
coilAddress = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,15 @@
|
||||
CommandParameter="{Binding Label}"
|
||||
Margin="5,3"
|
||||
MinHeight="40"
|
||||
PreviewMouseLeftButtonDown="DeviceActionButton_PreviewMouseLeftButtonDown"
|
||||
PreviewMouseLeftButtonUp="DeviceActionButton_PreviewMouseLeftButtonUp"
|
||||
MouseLeave="DeviceActionButton_MouseLeave"
|
||||
LostMouseCapture="DeviceActionButton_LostMouseCapture"
|
||||
LostKeyboardFocus="DeviceActionButton_LostKeyboardFocus"
|
||||
TouchDown="DeviceActionButton_TouchDown"
|
||||
TouchUp="DeviceActionButton_TouchUp"
|
||||
TouchLeave="DeviceActionButton_TouchLeave"
|
||||
LostTouchCapture="DeviceActionButton_LostTouchCapture"
|
||||
Style="{StaticResource InstrumentButtonStyle}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
|
||||
@@ -1,11 +1,159 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using ConeCalorimeter.ViewModels;
|
||||
|
||||
namespace ConeCalorimeter.Views;
|
||||
|
||||
public partial class TestPageView : UserControl
|
||||
{
|
||||
private readonly HashSet<string> _runningMomentaryActions = [];
|
||||
|
||||
public TestPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void DeviceActionButton_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (StartMomentaryAction(sender))
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
button.CaptureMouse();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeviceActionButton_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (StopMomentaryAction(sender))
|
||||
{
|
||||
ReleaseInputCapture(sender);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeviceActionButton_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (StopMomentaryAction(sender))
|
||||
{
|
||||
ReleaseInputCapture(sender);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeviceActionButton_LostMouseCapture(object sender, MouseEventArgs e)
|
||||
{
|
||||
StopMomentaryAction(sender);
|
||||
}
|
||||
|
||||
private void DeviceActionButton_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||
{
|
||||
if (StopMomentaryAction(sender))
|
||||
{
|
||||
ReleaseInputCapture(sender);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeviceActionButton_TouchDown(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (StartMomentaryAction(sender))
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
button.CaptureTouch(e.TouchDevice);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeviceActionButton_TouchUp(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (StopMomentaryAction(sender))
|
||||
{
|
||||
ReleaseInputCapture(sender);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeviceActionButton_TouchLeave(object sender, TouchEventArgs e)
|
||||
{
|
||||
if (StopMomentaryAction(sender))
|
||||
{
|
||||
ReleaseInputCapture(sender);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeviceActionButton_LostTouchCapture(object sender, TouchEventArgs e)
|
||||
{
|
||||
StopMomentaryAction(sender);
|
||||
}
|
||||
|
||||
private bool StartMomentaryAction(object sender)
|
||||
{
|
||||
if (!TryGetDeviceAction(sender, out var label, out var viewModel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_runningMomentaryActions.Contains(label))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!viewModel.StartMomentaryDeviceAction(label))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_runningMomentaryActions.Add(label);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool StopMomentaryAction(object sender)
|
||||
{
|
||||
if (!TryGetDeviceAction(sender, out var label, out var viewModel)
|
||||
|| !_runningMomentaryActions.Remove(label))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return viewModel.StopMomentaryDeviceAction(label);
|
||||
}
|
||||
|
||||
private bool TryGetDeviceAction(
|
||||
object sender,
|
||||
out string label,
|
||||
out TestPageViewModel viewModel)
|
||||
{
|
||||
label = string.Empty;
|
||||
viewModel = null!;
|
||||
|
||||
if (sender is not Button { DataContext: DeviceActionViewModel action }
|
||||
|| DataContext is not TestPageViewModel testPageViewModel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
label = action.Label;
|
||||
viewModel = testPageViewModel;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ReleaseInputCapture(object sender)
|
||||
{
|
||||
if (sender is not Button button)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (button.IsMouseCaptured)
|
||||
{
|
||||
button.ReleaseMouseCapture();
|
||||
}
|
||||
|
||||
button.ReleaseAllTouchCaptures();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user