更新20260602

This commit is contained in:
GukSang.Jin
2026-06-02 18:45:14 +08:00
parent fee2310977
commit 212dca6abe
9 changed files with 282 additions and 11 deletions

View File

@@ -5,12 +5,17 @@ using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModels;
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Views;
using Serilog;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance
{
public partial class App : Application
{
private static bool exceptionHandlersRegistered;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@@ -18,6 +23,8 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance
public override void OnFrameworkInitializationCompleted()
{
RegisterExceptionHandlers();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
@@ -28,5 +35,33 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance
base.OnFrameworkInitializationCompleted();
}
private static void RegisterExceptionHandlers()
{
if (exceptionHandlersRegistered)
{
return;
}
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
{
if (args.ExceptionObject is Exception exception)
{
Log.Fatal(exception, "捕获到 AppDomain 未处理异常IsTerminating={IsTerminating}", args.IsTerminating);
}
else
{
Log.Fatal("捕获到 AppDomain 未处理异常:{ExceptionObject}", args.ExceptionObject);
}
};
TaskScheduler.UnobservedTaskException += (_, args) =>
{
Log.Error(args.Exception, "捕获到未观察的后台任务异常");
args.SetObserved();
};
exceptionHandlersRegistered = true;
}
}
}
}

View File

@@ -25,6 +25,8 @@
<PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.4" />
<PackageReference Include="NModbus" Version="3.0.83" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SukiUI" Version="6.0.2" />
<PackageReference Include="System.IO.Ports" Version="10.0.8" />
<PackageReference Include="Tmds.DBus.Protocol" Version="0.94.1" />

View File

@@ -0,0 +1,7 @@
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models
{
public sealed record AdcZeroCalibration(
int NormalPressureZero,
int FrictionZero1,
int FrictionZero2);
}

View File

@@ -0,0 +1,7 @@
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models
{
public sealed record PlcDeviceParameters(
double ManualSpeed,
double ManualDisplacement,
double TestSpeed);
}

View File

@@ -1,18 +1,35 @@
using Avalonia;
using Avalonia;
using Serilog;
using System;
using System.IO;
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance
{
internal sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
public static int Main(string[] args)
{
ConfigureLogging();
try
{
Log.Information("鞋类整鞋防滑性能试验程序启动");
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "程序发生致命异常并退出");
return 1;
}
finally
{
Log.Information("鞋类整鞋防滑性能试验程序退出");
Log.CloseAndFlush();
}
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
@@ -21,5 +38,27 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance
#endif
.WithInterFont()
.LogToTrace();
private static void ConfigureLogging()
{
var logDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"FootwearSlipResistance",
"Logs");
Directory.CreateDirectory(logDirectory);
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(
Path.Combine(logDirectory, "app-.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
fileSizeLimitBytes: 20 * 1024 * 1024,
rollOnFileSizeLimit: true,
shared: true,
flushToDiskInterval: TimeSpan.FromSeconds(1),
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
}
}
}

View File

@@ -2,6 +2,7 @@ using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
using Serilog;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -28,6 +29,13 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
: report.TestNumber);
var path = Path.Combine(directory, $"{safeNumber}_防滑性能测试报告.xlsx");
Log.Information(
"开始导出防滑性能 ExcelTestNumber={TestNumber}, ResultCount={ResultCount}, PointCount={PointCount}, Path={Path}",
report.TestNumber,
report.Results.Count,
report.Points.Count,
path);
using var document = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook);
var workbookPart = document.AddWorkbookPart();
workbookPart.Workbook = new Workbook();
@@ -38,6 +46,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
AddDataSheet(workbookPart, sheets, 3, report.Points);
workbookPart.Workbook.Save();
Log.Information("防滑性能 Excel 导出完成Path={Path}", path);
return path;
}

View File

@@ -1,6 +1,7 @@
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
using NModbus;
using NModbus.IO;
using Serilog;
using System;
using System.Globalization;
using System.IO.Ports;
@@ -37,10 +38,16 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
private IModbusSerialMaster? adcMaster;
private DeviceSettings settings = new("0.00", "0.00", "0.30", "0", "0.00", "0", "0.00", "0", "0.00", "COM7", "COM8", 115200);
private SlipDeviceSnapshot snapshot = SlipDeviceSnapshot.Offline();
private DateTime lastAdcErrorLoggedAt = DateTime.MinValue;
private DateTime lastPlcErrorLoggedAt = DateTime.MinValue;
private double verticalLoadN;
private double horizontalFrictionN;
private double displacementMm;
private int pressureRawValue;
private int frictionRawValue1;
private int frictionRawValue2;
private bool hasAdcRawValues;
private bool isTestRunning;
private bool isResetting;
private bool isConnected;
@@ -64,6 +71,13 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
try
{
Log.Information(
"启动防滑测试设备连接PLC={PlcPort}, ADC={AdcPort}, BaudRate={BaudRate}, SlaveId={SlaveId}",
settings.PlcPortName,
settings.AdcPortName,
settings.BaudRate,
SlaveId);
plcPort = CreatePort(settings.PlcPortName, settings.BaudRate);
adcPort = CreatePort(settings.AdcPortName, settings.BaudRate);
plcPort.Open();
@@ -79,9 +93,16 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
SetConnected(true, string.Empty);
adcTask = Task.Run(() => PollAdc(token), token);
plcTask = Task.Run(() => PollPlc(token), token);
Log.Information("防滑测试设备连接成功ADC/PLC 轮询已启动");
}
catch (Exception ex)
{
Log.Error(
ex,
"防滑测试设备连接失败PLC={PlcPort}, ADC={AdcPort}, BaudRate={BaudRate}",
settings.PlcPortName,
settings.AdcPortName,
settings.BaudRate);
SetConnected(false, ex.Message);
Stop();
}
@@ -90,6 +111,14 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
public void UpdateSettings(DeviceSettings deviceSettings)
{
settings = deviceSettings;
Log.Debug(
"设备设置已更新PLC={PlcPort}, ADC={AdcPort}, BaudRate={BaudRate}, TestSpeed={TestSpeed}, ManualSpeed={ManualSpeed}, ManualDisplacement={ManualDisplacement}",
settings.PlcPortName,
settings.AdcPortName,
settings.BaudRate,
settings.TestSpeed,
settings.ManualSpeed,
settings.ManualDisplacement);
}
public Task PulseStartTestAsync() => PulseCoilAsync(StartTestCoil);
@@ -120,8 +149,50 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
public Task WriteManualDisplacementAsync(double value) => WriteFloatRegisterAsync(ManualDisplacementRegister, value);
public Task<PlcDeviceParameters> ReadDeviceParametersAsync() =>
Task.Run(() =>
{
var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接");
var manualSpeedWords = master.ReadHoldingRegisters(SlaveId, ManualSpeedRegister, 2);
var manualDisplacementWords = master.ReadHoldingRegisters(SlaveId, ManualDisplacementRegister, 2);
var testSpeedWords = master.ReadHoldingRegisters(SlaveId, TestSpeedRegister, 2);
var parameters = new PlcDeviceParameters(
UshortToFloat(manualSpeedWords[1], manualSpeedWords[0]),
UshortToFloat(manualDisplacementWords[1], manualDisplacementWords[0]),
UshortToFloat(testSpeedWords[1], testSpeedWords[0]));
Log.Information(
"读取 PLC 参数完成D{ManualSpeedRegister}={ManualSpeed:F3}, D{ManualDisplacementRegister}={ManualDisplacement:F3}, D{TestSpeedRegister}={TestSpeed:F3}",
ManualSpeedRegister,
parameters.ManualSpeed,
ManualDisplacementRegister,
parameters.ManualDisplacement,
TestSpeedRegister,
parameters.TestSpeed);
return parameters;
});
public AdcZeroCalibration CaptureCurrentAdcZero()
{
lock (sync)
{
if (!hasAdcRawValues)
{
throw new InvalidOperationException("ADC 尚未读取到有效原始数据");
}
Log.Information(
"采集 ADC 零点NormalPressureZero={NormalPressureZero}, FrictionZero1={FrictionZero1}, FrictionZero2={FrictionZero2}",
pressureRawValue,
frictionRawValue1,
frictionRawValue2);
return new AdcZeroCalibration(pressureRawValue, frictionRawValue1, frictionRawValue2);
}
}
public void Stop()
{
Log.Information("停止防滑测试设备连接与轮询");
cancellationTokenSource?.Cancel();
try
@@ -209,6 +280,10 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
lock (sync)
{
pressureRawValue = pressureRaw;
frictionRawValue1 = friction1Raw;
frictionRawValue2 = friction2Raw;
hasAdcRawValues = true;
verticalLoadN = pressure;
horizontalFrictionN = friction;
lastError = string.Empty;
@@ -220,6 +295,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
}
catch (Exception ex)
{
LogPollingError("ADC", ex, ref lastAdcErrorLoggedAt);
SetConnected(false, ex.Message);
Thread.Sleep(250);
}
@@ -255,6 +331,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
}
catch (Exception ex)
{
LogPollingError("PLC", ex, ref lastPlcErrorLoggedAt);
SetConnected(false, ex.Message);
Thread.Sleep(250);
}
@@ -263,6 +340,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
private async Task PulseCoilAsync(ushort coil)
{
Log.Information("发送 PLC 脉冲线圈M{Coil}", coil);
await WriteCoilAsync(coil, true);
await Task.Delay(80);
await WriteCoilAsync(coil, false);
@@ -274,6 +352,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接");
var current = master.ReadCoils(SlaveId, coil, 1)[0];
master.WriteSingleCoil(SlaveId, coil, !current);
Log.Information("切换 PLC 线圈M{Coil}, Before={Before}, After={After}", coil, current, !current);
});
private Task WriteCoilAsync(ushort coil, bool value) =>
@@ -281,6 +360,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
{
var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接");
master.WriteSingleCoil(SlaveId, coil, value);
Log.Debug("写入 PLC 线圈M{Coil}={Value}", coil, value);
});
private Task WriteFloatRegisterAsync(ushort register, double value) =>
@@ -288,8 +368,27 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
{
var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接");
master.WriteMultipleRegisters(SlaveId, register, SplitFloatToUShortArray((float)value));
Log.Information("写入 PLC 浮点寄存器D{Register}={Value:F3}", register, value);
});
private void LogPollingError(string source, Exception exception, ref DateTime lastLoggedAt)
{
var now = DateTime.UtcNow;
if (now - lastLoggedAt < TimeSpan.FromSeconds(5))
{
return;
}
lastLoggedAt = now;
Log.Error(
exception,
"{Source} 轮询失败PLC={PlcPort}, ADC={AdcPort}, BaudRate={BaudRate}",
source,
settings.PlcPortName,
settings.AdcPortName,
settings.BaudRate);
}
private void SetConnected(bool connected, string error)
{
lock (sync)

View File

@@ -7,6 +7,7 @@ using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using Serilog;
using SkiaSharp;
using System;
using System.Collections.Generic;
@@ -215,6 +216,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
public MainWindowViewModel()
{
Log.Information("初始化主页面 ViewModel");
Series =
[
CreateLineSeries("垂直压力(N)", verticalLoadPoints, "#DC2626", 0, 0.65),
@@ -226,6 +228,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
LoadDeviceSettings();
UpdateTargetLoad();
deviceService.Start(CurrentSettings());
_ = LoadPlcParametersAsync();
refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(33) };
refreshTimer.Tick += (_, _) => RefreshFromDevice();
@@ -263,6 +266,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
var points = currentRun.Count > 0 ? currentRun.ToList() : lastCompletedRun;
if (points.Count == 0)
{
Log.Warning("请求导出 Excel 时没有可导出的实时采样数据TestNumber={TestNumber}", TestNumber);
CurrentStatus = "没有可导出的实时采样数据,请先完成一次测试";
return;
}
@@ -292,6 +296,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
}
catch (Exception ex)
{
Log.Error(ex, "Excel 导出失败TestNumber={TestNumber}, PointCount={PointCount}", TestNumber, points.Count);
CurrentStatus = $"Excel 导出失败:{ex.Message}";
}
}
@@ -336,6 +341,41 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
deviceService.UpdateSettings(CurrentSettings());
}
[RelayCommand]
private void CalibrateNormalPressureZero()
{
try
{
var zero = deviceService.CaptureCurrentAdcZero();
NormalPressureZero = zero.NormalPressureZero.ToString(CultureInfo.InvariantCulture);
SaveAndApplySettings();
CurrentStatus = "正压力零点已按当前 ADC 原始值采集";
}
catch (Exception ex)
{
Log.Error(ex, "正压力零点采集失败");
CurrentStatus = $"正压力零点采集失败:{ex.Message}";
}
}
[RelayCommand]
private void CalibrateFrictionZero()
{
try
{
var zero = deviceService.CaptureCurrentAdcZero();
FrictionZero1 = zero.FrictionZero1.ToString(CultureInfo.InvariantCulture);
FrictionZero2 = zero.FrictionZero2.ToString(CultureInfo.InvariantCulture);
SaveAndApplySettings();
CurrentStatus = "摩擦零点已按当前 ADC 原始值采集";
}
catch (Exception ex)
{
Log.Error(ex, "摩擦零点采集失败");
CurrentStatus = $"摩擦零点采集失败:{ex.Message}";
}
}
partial void OnUploadProgressChanged(int value) => OnPropertyChanged(nameof(UploadProgressText));
partial void OnManualSpeedChanged(string value)
@@ -434,6 +474,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
runStopwatch.Restart();
UploadProgress = 0;
CurrentStatus = "测试运行:按标准采集垂直载荷、摩擦力、位移与摩擦系数";
Log.Information("测试开始TestNumber={TestNumber}, TargetLoad={TargetLoad}, TestSpeed={TestSpeed}", TestNumber, TargetLoadText, TestSpeedText);
}
private void RecordPoint(SlipDeviceSnapshot device)
@@ -466,6 +507,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
runStopwatch.Stop();
if (currentRun.Count < 3)
{
Log.Warning("测试停止但采样点不足TestNumber={TestNumber}, PointCount={PointCount}", TestNumber, currentRun.Count);
CurrentStatus = "测试已停止,但有效采样点不足,未生成结果";
return;
}
@@ -503,6 +545,13 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
CurrentStatus = verdict == "有效"
? "测试完成:已按标准生成静/动摩擦系数"
: "测试完成:最近三次结果差异超过 10%,建议重新测试";
Log.Information(
"测试完成TestNumber={TestNumber}, PointCount={PointCount}, StaticCoefficient={StaticCoefficient:F3}, DynamicCoefficient={DynamicCoefficient:F3}, Verdict={Verdict}",
TestNumber,
currentRun.Count,
staticCoefficientValue,
dynamicCoefficientValue,
verdict);
}
private bool NeedsRetest(double staticCoefficientValue, double dynamicCoefficientValue)
@@ -571,6 +620,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
}
catch (Exception ex)
{
Log.Error(ex, "设备指令失败:{SuccessMessage}", successMessage);
CurrentStatus = $"设备指令失败:{ex.Message}";
}
}
@@ -591,6 +641,25 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
deviceService.UpdateSettings(CurrentSettings());
}
private async Task LoadPlcParametersAsync()
{
try
{
var parameters = await deviceService.ReadDeviceParametersAsync();
isLoadingDeviceSettings = true;
ManualSpeed = parameters.ManualSpeed.ToString("F2", CultureInfo.InvariantCulture);
ManualDisplacement = parameters.ManualDisplacement.ToString("F2", CultureInfo.InvariantCulture);
TestSpeed = parameters.TestSpeed.ToString("F2", CultureInfo.InvariantCulture);
isLoadingDeviceSettings = false;
SaveAndApplySettings();
}
catch (Exception ex)
{
isLoadingDeviceSettings = false;
Log.Warning(ex, "启动时读取 PLC 参数失败,将使用本地保存的设备设置");
}
}
private void LoadDeviceSettings()
{
if (!File.Exists(DeviceSettingsPath))
@@ -621,8 +690,9 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
AdcPortName = settings.AdcPortName ?? AdcPortName;
BaudRate = settings.BaudRate > 0 ? settings.BaudRate : BaudRate;
}
catch
catch (Exception ex)
{
Log.Warning(ex, "读取设备设置失败Path={Path}", DeviceSettingsPath);
}
finally
{
@@ -643,8 +713,9 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
var json = JsonSerializer.Serialize(CurrentSettings(), new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(DeviceSettingsPath, json);
}
catch
catch (Exception ex)
{
Log.Warning(ex, "保存设备设置失败Path={Path}", DeviceSettingsPath);
}
}

View File

@@ -328,7 +328,7 @@
<Button Content="复位" Classes="action" Command="{Binding ClearCommand}"/>
<Button Grid.Column="1" Content="测试" Classes="success action" Command="{Binding StartTestCommand}"/>
<Button Grid.Row="1" Content="停止" Classes="danger action" Command="{Binding StopTestCommand}"/>
<Button Grid.Row="1" Grid.Column="1" Content="导出Excel" Classes="primary action" Command="{Binding ExportReportCommand}"/>
<Button Grid.Row="1" Grid.Column="1" Content="导出报告" Classes="primary action" Command="{Binding ExportReportCommand}"/>
</Grid>
<StackPanel Grid.Row="2" Spacing="8">
@@ -432,12 +432,14 @@
<Label Grid.Row="6" Content="正压力零点" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="6" Grid.Column="1" Classes="setting-value" Text="{Binding NormalPressureZero, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Row="6" Grid.Column="2" Content="采零" Classes="compact" Command="{Binding CalibrateNormalPressureZeroCommand}"/>
<Label Grid.Row="6" Grid.Column="4" Content="正压力系数" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="6" Grid.Column="5" Classes="setting-value" Text="{Binding NormalPressureCoefficient, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="7" Content="摩擦1零点" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="7" Grid.Column="1" Classes="setting-value" Text="{Binding FrictionZero1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Row="7" Grid.Column="2" Content="摩擦采零" Classes="compact" Command="{Binding CalibrateFrictionZeroCommand}"/>
<Label Grid.Row="7" Grid.Column="4" Content="摩擦1系数" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="7" Grid.Column="5" Classes="setting-value" Text="{Binding FrictionCoefficient1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>