更新数据接口

This commit is contained in:
GukSang.Jin
2026-06-02 18:14:01 +08:00
parent 27439505a9
commit fee2310977
11 changed files with 1401 additions and 205 deletions

View File

@@ -22,6 +22,7 @@
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<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="SukiUI" Version="6.0.2" />

View File

@@ -0,0 +1,16 @@
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models
{
public sealed record DeviceSettings(
string ManualSpeed,
string ManualDisplacement,
string TestSpeed,
string NormalPressureZero,
string NormalPressureCoefficient,
string FrictionZero1,
string FrictionCoefficient1,
string FrictionZero2,
string FrictionCoefficient2,
string PlcPortName,
string AdcPortName,
int BaudRate);
}

View File

@@ -0,0 +1,12 @@
using System;
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models
{
public sealed record SlipDataPoint(
DateTime Timestamp,
double TimeSeconds,
double VerticalLoadN,
double HorizontalFrictionN,
double DisplacementMm,
double FrictionCoefficient);
}

View File

@@ -0,0 +1,22 @@
using System;
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models
{
public sealed record SlipDeviceSnapshot(
DateTime Timestamp,
double VerticalLoadN,
double HorizontalFrictionN,
double DisplacementMm,
bool IsTestRunning,
bool IsResetting,
bool IsConnected,
string LastError)
{
public double FrictionCoefficient => Math.Abs(VerticalLoadN) > 0.0001
? HorizontalFrictionN / VerticalLoadN
: 0;
public static SlipDeviceSnapshot Offline(string error = "") =>
new(DateTime.Now, 0, 0, 0, false, false, false, error);
}
}

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models
{
public sealed record SlipReportExport(
string TestNumber,
string OperatorName,
string MethodName,
string ReportName,
string SampleFeature,
string ShoeSize,
string Lubricant,
string TestMode,
string Surface,
string TargetLoad,
string ActualLoad,
string TestSpeed,
string StandardReference,
IReadOnlyList<TestSample> Results,
IReadOnlyList<SlipDataPoint> Points);
}

View File

@@ -0,0 +1,11 @@
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models
{
public sealed record TestSample(
int Index,
string Time,
string StaticCoefficient,
string DynamicCoefficient,
string Verdict,
double StaticCoefficientValue,
double DynamicCoefficientValue);
}

View File

@@ -0,0 +1,365 @@
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using A = DocumentFormat.OpenXml.Drawing;
using C = DocumentFormat.OpenXml.Drawing.Charts;
using Xdr = DocumentFormat.OpenXml.Drawing.Spreadsheet;
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
{
public sealed class SlipExcelExportService
{
private const string InfoSheetName = "试验信息";
private const string ResultSheetName = "结果汇总";
private const string DataSheetName = "实时数据";
public string Export(SlipReportExport report)
{
var directory = GetExportDirectory();
Directory.CreateDirectory(directory);
var safeNumber = MakeSafeFileName(string.IsNullOrWhiteSpace(report.TestNumber)
? DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)
: report.TestNumber);
var path = Path.Combine(directory, $"{safeNumber}_防滑性能测试报告.xlsx");
using var document = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook);
var workbookPart = document.AddWorkbookPart();
workbookPart.Workbook = new Workbook();
var sheets = workbookPart.Workbook.AppendChild(new Sheets());
AddInfoSheet(workbookPart, sheets, 1, report);
AddResultSheet(workbookPart, sheets, 2, report.Results);
AddDataSheet(workbookPart, sheets, 3, report.Points);
workbookPart.Workbook.Save();
return path;
}
private static string GetExportDirectory()
{
var legacyDirectory = Path.Combine(@"D:\文件保存", DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture));
if (Directory.Exists(@"D:\"))
{
return legacyDirectory;
}
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"FootwearSlipResistance",
DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture));
}
private static void AddInfoSheet(WorkbookPart workbookPart, Sheets sheets, uint sheetId, SlipReportExport report)
{
var rows = new List<IReadOnlyList<object?>>
{
new object?[] { "字段", "内容" },
new object?[] { "试验编号", report.TestNumber },
new object?[] { "报告名称", report.ReportName },
new object?[] { "操作人员", report.OperatorName },
new object?[] { "测试方法", report.MethodName },
new object?[] { "样品特征", report.SampleFeature },
new object?[] { "鞋码区间/目标负荷", $"{report.ShoeSize} / {report.TargetLoad}" },
new object?[] { "实际负荷", report.ActualLoad },
new object?[] { "润滑介质", report.Lubricant },
new object?[] { "测试模式", report.TestMode },
new object?[] { "地面材质", report.Surface },
new object?[] { "测试速度", report.TestSpeed },
new object?[] { "标准依据", report.StandardReference },
new object?[] { "导出时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) }
};
AddWorksheet(workbookPart, sheets, sheetId, InfoSheetName, rows, "A", "B");
}
private static void AddResultSheet(WorkbookPart workbookPart, Sheets sheets, uint sheetId, IReadOnlyList<TestSample> results)
{
var rows = new List<IReadOnlyList<object?>>
{
new object?[] { "序号", "时间", "静摩擦系数", "动摩擦系数", "判定" }
};
foreach (var result in results)
{
rows.Add(new object?[]
{
result.Index,
result.Time,
result.StaticCoefficientValue,
result.DynamicCoefficientValue,
result.Verdict
});
}
AddWorksheet(workbookPart, sheets, sheetId, ResultSheetName, rows, "A", "E");
}
private static void AddDataSheet(WorkbookPart workbookPart, Sheets sheets, uint sheetId, IReadOnlyList<SlipDataPoint> points)
{
var rows = new List<IReadOnlyList<object?>>
{
new object?[] { "时间(s)", "正压力(N)", "摩擦力(N)", "位移量(mm)", "摩擦系数" }
};
foreach (var point in points)
{
rows.Add(new object?[]
{
point.TimeSeconds,
point.VerticalLoadN,
point.HorizontalFrictionN,
point.DisplacementMm,
point.FrictionCoefficient
});
}
var worksheetPart = AddWorksheet(workbookPart, sheets, sheetId, DataSheetName, rows, "A", "E");
if (points.Count > 0)
{
AddChart(worksheetPart, points.Count + 1);
}
}
private static WorksheetPart AddWorksheet(
WorkbookPart workbookPart,
Sheets sheets,
uint sheetId,
string sheetName,
IReadOnlyList<IReadOnlyList<object?>> rows,
string firstColumn,
string lastColumn)
{
var worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
var sheetData = new SheetData();
var worksheet = new Worksheet();
worksheet.Append(CreateColumns(firstColumn, lastColumn));
worksheet.Append(sheetData);
worksheetPart.Worksheet = worksheet;
for (var rowIndex = 0; rowIndex < rows.Count; rowIndex++)
{
var row = new Row { RowIndex = (uint)(rowIndex + 1) };
for (var columnIndex = 0; columnIndex < rows[rowIndex].Count; columnIndex++)
{
row.Append(CreateCell(columnIndex + 1, rowIndex + 1, rows[rowIndex][columnIndex]));
}
sheetData.Append(row);
}
var relationshipId = workbookPart.GetIdOfPart(worksheetPart);
sheets.Append(new Sheet
{
Id = relationshipId,
SheetId = sheetId,
Name = sheetName
});
worksheetPart.Worksheet.Save();
return worksheetPart;
}
private static Columns CreateColumns(string firstColumn, string lastColumn)
{
var first = ColumnNameToIndex(firstColumn);
var last = ColumnNameToIndex(lastColumn);
var columns = new Columns();
for (var index = first; index <= last; index++)
{
columns.Append(new Column
{
Min = (uint)index,
Max = (uint)index,
Width = index == 1 ? 15 : 18,
CustomWidth = true
});
}
return columns;
}
private static Cell CreateCell(int columnIndex, int rowIndex, object? value)
{
var cell = new Cell { CellReference = $"{GetColumnName(columnIndex)}{rowIndex}" };
switch (value)
{
case null:
cell.DataType = CellValues.String;
cell.CellValue = new CellValue(string.Empty);
break;
case int intValue:
cell.CellValue = new CellValue(intValue);
break;
case double doubleValue:
cell.CellValue = new CellValue(doubleValue.ToString("0.#####", CultureInfo.InvariantCulture));
break;
default:
cell.DataType = CellValues.String;
cell.CellValue = new CellValue(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty);
break;
}
return cell;
}
private static void AddChart(WorksheetPart worksheetPart, int lastRow)
{
var drawingsPart = worksheetPart.AddNewPart<DrawingsPart>();
var drawingRelationshipId = worksheetPart.GetIdOfPart(drawingsPart);
worksheetPart.Worksheet.Append(new DocumentFormat.OpenXml.Spreadsheet.Drawing { Id = drawingRelationshipId });
drawingsPart.WorksheetDrawing = new Xdr.WorksheetDrawing();
var chartPart = drawingsPart.AddNewPart<ChartPart>();
var chartRelationshipId = drawingsPart.GetIdOfPart(chartPart);
chartPart.ChartSpace = CreateChartSpace(lastRow);
chartPart.ChartSpace.Save();
var graphicFrame = new Xdr.GraphicFrame(
new Xdr.NonVisualGraphicFrameProperties(
new Xdr.NonVisualDrawingProperties { Id = 2U, Name = "防滑性能曲线图" },
new Xdr.NonVisualGraphicFrameDrawingProperties()),
new Xdr.Transform(
new A.Offset { X = 0L, Y = 0L },
new A.Extents { Cx = 0L, Cy = 0L }),
new A.Graphic(
new A.GraphicData(
new C.ChartReference { Id = chartRelationshipId })
{ Uri = "http://schemas.openxmlformats.org/drawingml/2006/chart" }));
var anchor = new Xdr.TwoCellAnchor(
new Xdr.FromMarker(
new Xdr.ColumnId("6"),
new Xdr.ColumnOffset("0"),
new Xdr.RowId("1"),
new Xdr.RowOffset("0")),
new Xdr.ToMarker(
new Xdr.ColumnId("15"),
new Xdr.ColumnOffset("0"),
new Xdr.RowId("23"),
new Xdr.RowOffset("0")),
graphicFrame,
new Xdr.ClientData());
drawingsPart.WorksheetDrawing.Append(anchor);
drawingsPart.WorksheetDrawing.Save();
worksheetPart.Worksheet.Save();
}
private static C.ChartSpace CreateChartSpace(int lastRow)
{
const uint xAxisId = 48650112U;
const uint yAxisId = 48672768U;
var scatterChart = new C.ScatterChart(
new C.ScatterStyle { Val = C.ScatterStyleValues.LineMarker },
CreateSeries(0, "正压力(N)", 2, lastRow),
CreateSeries(1, "摩擦力(N)", 3, lastRow),
CreateSeries(2, "位移量(mm)", 4, lastRow),
CreateSeries(3, "摩擦系数", 5, lastRow),
new C.AxisId { Val = xAxisId },
new C.AxisId { Val = yAxisId });
var chart = new C.Chart(
CreateTitle("防滑性能实时曲线"),
new C.PlotArea(
new C.Layout(),
scatterChart,
CreateValueAxis(xAxisId, yAxisId, C.AxisPositionValues.Bottom, "时间(s)"),
CreateValueAxis(yAxisId, xAxisId, C.AxisPositionValues.Left, "载荷 / 摩擦力 / 位移 / 系数")),
new C.Legend(
new C.LegendPosition { Val = C.LegendPositionValues.Bottom },
new C.Layout()),
new C.PlotVisibleOnly { Val = true });
return new C.ChartSpace(
new C.EditingLanguage { Val = "zh-CN" },
chart);
}
private static C.ScatterChartSeries CreateSeries(uint index, string title, int yColumnIndex, int lastRow)
{
var sheet = EscapeSheetName(DataSheetName);
var xFormula = $"'{sheet}'!$A$2:$A${lastRow}";
var yColumn = GetColumnName(yColumnIndex);
var yFormula = $"'{sheet}'!${yColumn}$2:${yColumn}${lastRow}";
var titleFormula = $"'{sheet}'!${yColumn}$1";
return new C.ScatterChartSeries(
new C.Index { Val = index },
new C.Order { Val = index },
new C.SeriesText(new C.StringReference(new C.Formula(titleFormula))),
new C.XValues(new C.NumberReference(new C.Formula(xFormula))),
new C.YValues(new C.NumberReference(new C.Formula(yFormula))),
new C.Smooth { Val = false });
}
private static C.ValueAxis CreateValueAxis(uint axisId, uint crossingAxisId, C.AxisPositionValues position, string title) =>
new(
new C.AxisId { Val = axisId },
new C.Scaling(new C.Orientation { Val = C.OrientationValues.MinMax }),
new C.AxisPosition { Val = position },
CreateTitle(title),
new C.NumberingFormat { FormatCode = "0.00", SourceLinked = false },
new C.MajorGridlines(),
new C.CrossingAxis { Val = crossingAxisId },
new C.Crosses { Val = C.CrossesValues.AutoZero },
new C.TickLabelPosition { Val = C.TickLabelPositionValues.NextTo });
private static C.Title CreateTitle(string text) =>
new(
new C.ChartText(
new C.RichText(
new A.BodyProperties(),
new A.ListStyle(),
new A.Paragraph(
new A.Run(
new A.RunProperties { Language = "zh-CN", FontSize = 1100 },
new A.Text(text))))),
new C.Layout(),
new C.Overlay { Val = false });
private static string MakeSafeFileName(string value)
{
foreach (var invalidChar in Path.GetInvalidFileNameChars())
{
value = value.Replace(invalidChar, '_');
}
return value;
}
private static string EscapeSheetName(string value) => value.Replace("'", "''");
private static string GetColumnName(int columnIndex)
{
var dividend = columnIndex;
var columnName = string.Empty;
while (dividend > 0)
{
var modulo = (dividend - 1) % 26;
columnName = Convert.ToChar('A' + modulo) + columnName;
dividend = (dividend - modulo) / 26;
}
return columnName;
}
private static int ColumnNameToIndex(string columnName)
{
var sum = 0;
foreach (var character in columnName)
{
sum *= 26;
sum += character - 'A' + 1;
}
return sum;
}
}
}

View File

@@ -0,0 +1,390 @@
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
using NModbus;
using NModbus.IO;
using System;
using System.Globalization;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
{
public sealed class SlipResistanceDeviceService : IDisposable
{
private const byte SlaveId = 1;
private const ushort PlcDisplacementRegister = 402;
private const ushort PlcStateCoilStart = 81;
private const ushort StartTestCoil = 80;
private const ushort StopTestCoil = 83;
private const ushort ResetCoil = 90;
private const ushort MoveLeftCoil = 1;
private const ushort MoveRightCoil = 2;
private const ushort LowerCoil = 4;
private const ushort LiftCoil = 5;
private const ushort ManualSpeedRegister = 302;
private const ushort TestSpeedRegister = 310;
private const ushort ManualDisplacementRegister = 320;
private readonly object sync = new();
private readonly ModbusFactory modbusFactory = new();
private CancellationTokenSource? cancellationTokenSource;
private Task? adcTask;
private Task? plcTask;
private SerialPort? plcPort;
private SerialPort? adcPort;
private IModbusSerialMaster? plcMaster;
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 double verticalLoadN;
private double horizontalFrictionN;
private double displacementMm;
private bool isTestRunning;
private bool isResetting;
private bool isConnected;
private string lastError = string.Empty;
public SlipDeviceSnapshot CurrentSnapshot
{
get
{
lock (sync)
{
return snapshot;
}
}
}
public void Start(DeviceSettings deviceSettings)
{
settings = deviceSettings;
Stop();
try
{
plcPort = CreatePort(settings.PlcPortName, settings.BaudRate);
adcPort = CreatePort(settings.AdcPortName, settings.BaudRate);
plcPort.Open();
adcPort.Open();
plcMaster = modbusFactory.CreateRtuMaster(new SerialPortResource(plcPort));
adcMaster = modbusFactory.CreateRtuMaster(new SerialPortResource(adcPort));
plcMaster.Transport.ReadTimeout = 2000;
adcMaster.Transport.ReadTimeout = 2000;
cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
SetConnected(true, string.Empty);
adcTask = Task.Run(() => PollAdc(token), token);
plcTask = Task.Run(() => PollPlc(token), token);
}
catch (Exception ex)
{
SetConnected(false, ex.Message);
Stop();
}
}
public void UpdateSettings(DeviceSettings deviceSettings)
{
settings = deviceSettings;
}
public Task PulseStartTestAsync() => PulseCoilAsync(StartTestCoil);
public Task PulseStopTestAsync() => PulseCoilAsync(StopTestCoil);
public Task PulseResetAsync() => PulseCoilAsync(ResetCoil);
public Task ToggleMoveLeftAsync() => ToggleCoilAsync(MoveLeftCoil);
public Task ToggleMoveRightAsync() => ToggleCoilAsync(MoveRightCoil);
public async Task LiftAsync()
{
await WriteCoilAsync(LowerCoil, false);
await WriteCoilAsync(LiftCoil, true);
}
public async Task LowerAsync()
{
await WriteCoilAsync(LiftCoil, false);
await WriteCoilAsync(LowerCoil, true);
}
public Task WriteManualSpeedAsync(double value) => WriteFloatRegisterAsync(ManualSpeedRegister, value);
public Task WriteTestSpeedAsync(double value) => WriteFloatRegisterAsync(TestSpeedRegister, value);
public Task WriteManualDisplacementAsync(double value) => WriteFloatRegisterAsync(ManualDisplacementRegister, value);
public void Stop()
{
cancellationTokenSource?.Cancel();
try
{
adcTask?.Wait(200);
plcTask?.Wait(200);
}
catch
{
}
cancellationTokenSource?.Dispose();
cancellationTokenSource = null;
adcTask = null;
plcTask = null;
plcMaster?.Dispose();
adcMaster?.Dispose();
plcMaster = null;
adcMaster = null;
ClosePort(plcPort);
ClosePort(adcPort);
plcPort = null;
adcPort = null;
}
public void Dispose()
{
Stop();
}
private static SerialPort CreatePort(string portName, int baudRate) =>
new(portName, baudRate, Parity.None, 8, StopBits.One)
{
ReadTimeout = 2000,
WriteTimeout = 2000
};
private static void ClosePort(SerialPort? port)
{
if (port is null)
{
return;
}
try
{
if (port.IsOpen)
{
port.Close();
}
}
catch
{
}
port.Dispose();
}
private void PollAdc(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
var master = adcMaster;
if (master is null)
{
return;
}
var data = master.ReadHoldingRegisters(SlaveId, 0, 8);
var pressureRaw = UshortToInt(data[0], data[1]);
var friction1Raw = UshortToInt(data[6], data[7]);
var friction2Raw = UshortToInt(data[2], data[3]);
var pressure = ConvertAdc(pressureRaw, settings.NormalPressureZero, settings.NormalPressureCoefficient);
// Keep the old instrument channel wiring: ADC 6/7 uses zero 1 with coefficient 2,
// and ADC 2/3 uses zero 2 with coefficient 1.
var friction1 = ConvertAdc(friction1Raw, settings.FrictionZero1, settings.FrictionCoefficient2);
var friction2 = ConvertAdc(friction2Raw, settings.FrictionZero2, settings.FrictionCoefficient1);
var friction = (friction1 + friction2) * -1.0;
lock (sync)
{
verticalLoadN = pressure;
horizontalFrictionN = friction;
lastError = string.Empty;
isConnected = true;
RefreshSnapshotLocked();
}
Thread.Sleep(10);
}
catch (Exception ex)
{
SetConnected(false, ex.Message);
Thread.Sleep(250);
}
}
}
private void PollPlc(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
var master = plcMaster;
if (master is null)
{
return;
}
var displacementWords = master.ReadHoldingRegisters(SlaveId, PlcDisplacementRegister, 2);
var coils = master.ReadCoils(SlaveId, PlcStateCoilStart, 10);
lock (sync)
{
displacementMm = UshortToFloat(displacementWords[1], displacementWords[0]);
isTestRunning = coils.Length > 0 && coils[0];
isResetting = coils.Length > 9 && coils[9];
lastError = string.Empty;
isConnected = true;
RefreshSnapshotLocked();
}
Thread.Sleep(10);
}
catch (Exception ex)
{
SetConnected(false, ex.Message);
Thread.Sleep(250);
}
}
}
private async Task PulseCoilAsync(ushort coil)
{
await WriteCoilAsync(coil, true);
await Task.Delay(80);
await WriteCoilAsync(coil, false);
}
private Task ToggleCoilAsync(ushort coil) =>
Task.Run(() =>
{
var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接");
var current = master.ReadCoils(SlaveId, coil, 1)[0];
master.WriteSingleCoil(SlaveId, coil, !current);
});
private Task WriteCoilAsync(ushort coil, bool value) =>
Task.Run(() =>
{
var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接");
master.WriteSingleCoil(SlaveId, coil, value);
});
private Task WriteFloatRegisterAsync(ushort register, double value) =>
Task.Run(() =>
{
var master = plcMaster ?? throw new InvalidOperationException("PLC 未连接");
master.WriteMultipleRegisters(SlaveId, register, SplitFloatToUShortArray((float)value));
});
private void SetConnected(bool connected, string error)
{
lock (sync)
{
isConnected = connected;
lastError = error;
RefreshSnapshotLocked();
}
}
private void RefreshSnapshotLocked()
{
snapshot = new SlipDeviceSnapshot(
DateTime.Now,
verticalLoadN,
horizontalFrictionN,
displacementMm,
isTestRunning,
isResetting,
isConnected,
lastError);
}
private static double ConvertAdc(int rawValue, string zeroText, string coefficientText)
{
var zero = ParseDouble(zeroText);
var coefficient = ParseDouble(coefficientText);
if (Math.Abs(coefficient) < 0.0001)
{
return 0;
}
return (rawValue - zero) / coefficient;
}
private static double ParseDouble(string value) =>
double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var invariantValue)
? invariantValue
: double.TryParse(value, NumberStyles.Float, CultureInfo.CurrentCulture, out var localValue)
? localValue
: 0;
private static int UshortToInt(ushort first, ushort second)
{
var bytes = new byte[4];
BitConverter.GetBytes(first).CopyTo(bytes, 0);
BitConverter.GetBytes(second).CopyTo(bytes, 2);
return BitConverter.ToInt32(bytes, 0);
}
private static float UshortToFloat(ushort first, ushort second)
{
var intSign = first / 32768;
var intSignRest = first % 32768;
var intExponent = intSignRest / 128;
var intExponentRest = intSignRest % 128;
var digit = (float)(intExponentRest * 65536 + second) / 8388608;
return (float)Math.Pow(-1, intSign) * (float)Math.Pow(2, intExponent - 127) * (digit + 1);
}
private static ushort[] SplitFloatToUShortArray(float value)
{
var bytes = BitConverter.GetBytes(value);
return
[
BitConverter.ToUInt16(bytes, 0),
BitConverter.ToUInt16(bytes, 2)
];
}
private sealed class SerialPortResource(SerialPort serialPort) : IStreamResource
{
public int InfiniteTimeout => SerialPort.InfiniteTimeout;
public int ReadTimeout
{
get => serialPort.ReadTimeout;
set => serialPort.ReadTimeout = value;
}
public int WriteTimeout
{
get => serialPort.WriteTimeout;
set => serialPort.WriteTimeout = value;
}
public void DiscardInBuffer() => serialPort.DiscardInBuffer();
public int Read(byte[] buffer, int offset, int count) => serialPort.Read(buffer, offset, count);
public void Write(byte[] buffer, int offset, int count) => serialPort.Write(buffer, offset, count);
public void Dispose()
{
}
}
}
}

View File

@@ -1,21 +1,40 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.Json;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModels
{
public partial class MainWindowViewModel : ViewModelBase
public partial class MainWindowViewModel : ViewModelBase, IDisposable
{
private readonly SlipResistanceDeviceService deviceService = new();
private readonly SlipExcelExportService excelExportService = new();
private readonly DispatcherTimer refreshTimer;
private readonly Stopwatch runStopwatch = new();
private readonly List<SlipDataPoint> currentRun = [];
private readonly ObservableCollection<ObservablePoint> verticalLoadPoints = [];
private readonly ObservableCollection<ObservablePoint> horizontalFrictionPoints = [];
private readonly ObservableCollection<ObservablePoint> frictionCoefficientPoints = [];
private readonly ObservableCollection<ObservablePoint> displacementPoints = [];
private bool isLoadingDeviceSettings;
private bool wasRunning;
private List<SlipDataPoint> lastCompletedRun = [];
[ObservableProperty]
private string testNumber = $"SLIP-{DateTime.Now:yyyyMMdd-HHmm}";
@@ -33,10 +52,10 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
private string sampleFeature = "整鞋样品 / 瓷砖接触面 / 水平测试";
[ObservableProperty]
private string currentStatus = "测试停止,请按“测试”键进行下次试验";
private string currentStatus = "设备连接中,等待 PLC 与 ADC 实时数据";
[ObservableProperty]
private int uploadProgress = 68;
private int uploadProgress;
[ObservableProperty]
private string manualDistance = "0";
@@ -56,50 +75,85 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
[ObservableProperty]
private string testSpeed = "0.30";
[ObservableProperty]
private string normalPressureZero = "0";
[ObservableProperty]
private string normalPressureCoefficient = "0.00";
[ObservableProperty]
private string frictionZero1 = "0";
[ObservableProperty]
private string frictionCoefficient1 = "0.00";
[ObservableProperty]
private string frictionZero2 = "0";
[ObservableProperty]
private string frictionCoefficient2 = "0.00";
public string TargetLoadText { get; } = "400 N";
public string ActualLoadText { get; } = "400 N";
[ObservableProperty]
private string plcPortName = "COM7";
[ObservableProperty]
private string adcPortName = "COM8";
[ObservableProperty]
private int baudRate = 115200;
[ObservableProperty]
private int selectedShoeSizeIndex;
[ObservableProperty]
private int selectedLubricantIndex;
[ObservableProperty]
private int selectedModeIndex = 2;
[ObservableProperty]
private int selectedSurfaceIndex;
[ObservableProperty]
private string targetLoadText = "400 N";
[ObservableProperty]
private string actualLoadText = "0.0 N";
[ObservableProperty]
private string deviceStatus = "离线";
[ObservableProperty]
private string activeMode = "水平测试模式";
[ObservableProperty]
private string resultSummary = "等待 3 次有效试验";
[ObservableProperty]
private string staticCoefficient = "0.000";
[ObservableProperty]
private string dynamicCoefficient = "0.000";
[ObservableProperty]
private string verticalPressure = "0.0";
[ObservableProperty]
private string horizontalForce = "0.0";
[ObservableProperty]
private string frictionCoefficient = "0.000";
[ObservableProperty]
private string distance = "0.0";
public string TestSpeedText => $"{TestSpeed} m/s";
public string SampleRateText { get; } = "30 Hz";
public string ActiveMode { get; } = "水平模式";
public string DeviceStatus { get; } = "联机 / 待机";
public string BatchNumber { get; } = DateTime.Now.ToString("yyyy-MM-dd");
public string ResultSummary { get; } = "平均 0.62";
public string BatchNumber { get; } = DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
public string UploadProgressText => $"{UploadProgress}%";
public string StaticCoefficient { get; } = "0.66";
public string VerticalPressure { get; } = "401.6";
public string HorizontalForce { get; } = "248.9";
public string FrictionCoefficient { get; } = "0.62";
public string Distance { get; } = "218.4";
public string StandardReference { get; } = "GB/T 3903.6-2024 7.1-8速度、载荷、三次平均与系数计算";
public string StandardReference { get; } = "GB/T 3903.6-2024 5.1.3、7.3.1.4、8.1-8.3:实时采集、静/动摩擦系数、三次平均与重测判定";
public ObservableCollection<TestSample> Samples { get; } =
[
new(16, "08:46:35", "0.58", "0.57", "有效"),
new(15, "08:46:27", "0.71", "0.61", "有效"),
new(14, "08:46:12", "0.66", "0.66", "有效"),
new(13, "08:46:04", "0.77", "0.69", "有效"),
new(12, "08:45:51", "0.62", "0.62", "有效"),
new(11, "08:45:43", "0.74", "0.64", "有效"),
new(10, "08:45:28", "0.64", "0.64", "有效"),
new(9, "08:45:19", "0.73", "0.66", "有效"),
new(8, "08:45:03", "0.57", "0.56", "有效"),
new(7, "08:44:49", "0.65", "0.57", "有效"),
new(6, "08:44:35", "0.56", "0.54", "有效"),
new(5, "08:44:27", "0.63", "0.56", "有效"),
new(4, "08:44:09", "0.56", "0.54", "有效"),
new(3, "08:44:01", "0.63", "0.56", "有效"),
new(2, "08:43:41", "0.55", "0.52", "有效"),
new(1, "08:43:33", "0.62", "0.55", "有效"),
];
public ObservableCollection<TestSample> Samples { get; } = [];
public DrawMarginFrame ChartFrame { get; } = new()
{
@@ -109,74 +163,15 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
public SolidColorPaint LegendTextPaint { get; } = new(SKColor.Parse("#334155"));
public ISeries[] Series { get; } =
[
new LineSeries<ObservablePoint>
{
Name = "垂直压力(N)",
Values = CreatePressureSeries(),
MiniatureShapeSize = 8,
MiniatureStrokeThickness = 2,
Stroke = new SolidColorPaint(SKColor.Parse("#DC2626")) { StrokeThickness = 3.6f },
Fill = new SolidColorPaint(new SKColor(220, 38, 38, 18)),
GeometryFill = new SolidColorPaint(SKColors.White),
GeometryStroke = new SolidColorPaint(SKColor.Parse("#DC2626")) { StrokeThickness = 2 },
GeometrySize = 5,
LineSmoothness = 0.65,
ScalesYAt = 0
},
new LineSeries<ObservablePoint>
{
Name = "水平拉力(N)",
Values = CreatePullSeries(),
MiniatureShapeSize = 8,
MiniatureStrokeThickness = 2,
Stroke = new SolidColorPaint(SKColor.Parse("#16A34A")) { StrokeThickness = 3.4f },
Fill = new SolidColorPaint(new SKColor(22, 163, 74, 16)),
GeometryFill = new SolidColorPaint(SKColors.White),
GeometryStroke = new SolidColorPaint(SKColor.Parse("#16A34A")) { StrokeThickness = 2 },
GeometrySize = 5,
LineSmoothness = 0.65,
ScalesYAt = 0
},
new LineSeries<ObservablePoint>
{
Name = "摩擦系数",
Values = CreateFrictionSeries(),
MiniatureShapeSize = 8,
MiniatureStrokeThickness = 2,
Stroke = new SolidColorPaint(SKColor.Parse("#C026D3")) { StrokeThickness = 3.4f },
Fill = new SolidColorPaint(new SKColor(192, 38, 211, 16)),
GeometryFill = new SolidColorPaint(SKColors.White),
GeometryStroke = new SolidColorPaint(SKColor.Parse("#C026D3")) { StrokeThickness = 2 },
GeometrySize = 5,
LineSmoothness = 0.65,
ScalesYAt = 1
},
new LineSeries<ObservablePoint>
{
Name = "距离(mm)",
Values = CreateDistanceSeries(),
MiniatureShapeSize = 8,
MiniatureStrokeThickness = 2,
Stroke = new SolidColorPaint(SKColor.Parse("#2563EB")) { StrokeThickness = 3.2f },
Fill = new SolidColorPaint(new SKColor(37, 99, 235, 14)),
GeometryFill = new SolidColorPaint(SKColors.White),
GeometryStroke = new SolidColorPaint(SKColor.Parse("#2563EB")) { StrokeThickness = 2 },
GeometrySize = 5,
LineSmoothness = 0.35,
ScalesYAt = 0
}
];
public ISeries[] Series { get; }
public Axis[] XAxes { get; } =
[
new Axis
{
Name = "时间(s)",
MinLimit = -0.10,
MaxLimit = 0.62,
UnitWidth = 0.05,
MinLimit = 0,
UnitWidth = 0.1,
SeparatorsPaint = new SolidColorPaint(SKColor.Parse("#D7E0EA")) { StrokeThickness = 1 },
SubseparatorsPaint = new SolidColorPaint(SKColor.Parse("#EEF3F8")) { StrokeThickness = 1 },
SubseparatorsCount = 4,
@@ -192,10 +187,8 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
[
new Axis
{
Name = "压力 / 力 / 距离",
Name = "压力 / 摩擦力 / 位移",
MinLimit = 0,
MaxLimit = 800,
UnitWidth = 50,
SeparatorsPaint = new SolidColorPaint(SKColor.Parse("#D7E0EA")) { StrokeThickness = 1 },
SubseparatorsPaint = new SolidColorPaint(SKColor.Parse("#EEF3F8")) { StrokeThickness = 1 },
SubseparatorsCount = 4,
@@ -210,7 +203,6 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
Name = "摩擦系数",
Position = LiveChartsCore.Measure.AxisPosition.End,
MinLimit = 0,
MaxLimit = 1.5,
UnitWidth = 0.1,
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7E22CE")),
NamePaint = new SolidColorPaint(SKColor.Parse("#7E22CE")),
@@ -223,61 +215,98 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
public MainWindowViewModel()
{
Series =
[
CreateLineSeries("垂直压力(N)", verticalLoadPoints, "#DC2626", 0, 0.65),
CreateLineSeries("水平摩擦力(N)", horizontalFrictionPoints, "#16A34A", 0, 0.65),
CreateLineSeries("摩擦系数", frictionCoefficientPoints, "#C026D3", 1, 0.65),
CreateLineSeries("位移(mm)", displacementPoints, "#2563EB", 0, 0.35)
];
LoadDeviceSettings();
UpdateTargetLoad();
deviceService.Start(CurrentSettings());
refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(33) };
refreshTimer.Tick += (_, _) => RefreshFromDevice();
refreshTimer.Start();
}
[RelayCommand]
private void Clear()
private async Task Clear()
{
UploadProgress = 0;
CurrentStatus = "传感器已清零,垂直压力与水平拉力基线重置完成";
await RunDeviceCommand(deviceService.PulseResetAsync(), "已发送复位指令 M90");
}
[RelayCommand]
private void Preload()
{
UploadProgress = 34;
CurrentStatus = "预压中0.2s 内施加目标垂直载荷,等待滑动动作";
CurrentStatus = "请在 0.2 s 内施加目标垂直载荷,载荷达到后启动滑动测试";
}
[RelayCommand]
private void StartTest()
private async Task StartTest()
{
UploadProgress = 86;
CurrentStatus = "测试运行:正在采集峰值静摩擦力与 0.3s-0.6s 动摩擦力";
BeginRun();
await RunDeviceCommand(deviceService.PulseStartTestAsync(), "已发送测试启动指令 M80等待 M81 运行状态");
}
[RelayCommand]
private void StopTest()
private async Task StopTest()
{
UploadProgress = 68;
CurrentStatus = "测试停止,请按“测试”键进行下次试验";
await RunDeviceCommand(deviceService.PulseStopTestAsync(), "已发送测试停止指令 M83");
}
[RelayCommand]
private void ExportReport()
{
UploadProgress = 100;
CurrentStatus = "报告数据已整理:三次平均、静/动摩擦系数与试验条件可导出";
var points = currentRun.Count > 0 ? currentRun.ToList() : lastCompletedRun;
if (points.Count == 0)
{
CurrentStatus = "没有可导出的实时采样数据,请先完成一次测试";
return;
}
try
{
var report = new SlipReportExport(
TestNumber,
OperatorName,
MethodName,
ReportName,
SampleFeature,
CurrentShoeSize(),
CurrentLubricant(),
CurrentMode(),
CurrentSurface(),
TargetLoadText,
ActualLoadText,
TestSpeedText,
StandardReference,
Samples.ToList(),
points);
var path = excelExportService.Export(report);
UploadProgress = 100;
CurrentStatus = $"Excel 已导出:{path}";
}
catch (Exception ex)
{
CurrentStatus = $"Excel 导出失败:{ex.Message}";
}
}
[RelayCommand]
private void LiftFast() => CurrentStatus = "垂直架快速上升";
private async Task Lift() => await RunDeviceCommand(deviceService.LiftAsync(), "垂直架提升指令已发送 M5");
[RelayCommand]
private void Lift() => CurrentStatus = "垂直架提升";
private async Task Lower() => await RunDeviceCommand(deviceService.LowerAsync(), "垂直架下降指令已发送 M4");
[RelayCommand]
private void Lower() => CurrentStatus = "垂直架下降";
private async Task MoveLeft() => await RunDeviceCommand(deviceService.ToggleMoveLeftAsync(), "水平板左移状态已切换 M1");
[RelayCommand]
private void LowerFast() => CurrentStatus = "垂直架快速下降";
[RelayCommand]
private void MoveLeft() => CurrentStatus = "水平板向左移动";
[RelayCommand]
private void MoveRight() => CurrentStatus = "水平板向右移动";
private async Task MoveRight() => await RunDeviceCommand(deviceService.ToggleMoveRightAsync(), "水平板右移状态已切换 M2");
[RelayCommand]
private void DeleteSelectedSample()
@@ -290,6 +319,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
}
Samples.Remove(sample);
UpdateResultSummary();
CurrentStatus = $"已删除序号 {SelectedSampleIndex} 的实验数据";
}
@@ -303,28 +333,263 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
private void CloseSettingsDialog()
{
IsSettingsDialogOpen = false;
deviceService.UpdateSettings(CurrentSettings());
}
partial void OnUploadProgressChanged(int value)
partial void OnUploadProgressChanged(int value) => OnPropertyChanged(nameof(UploadProgressText));
partial void OnManualSpeedChanged(string value)
{
OnPropertyChanged(nameof(UploadProgressText));
SaveAndApplySettings();
WriteNumericSetting(value, deviceService.WriteManualSpeedAsync, "手动速度");
}
partial void OnManualSpeedChanged(string value) => SaveDeviceSettings();
partial void OnManualDisplacementChanged(string value) => SaveDeviceSettings();
partial void OnManualDisplacementChanged(string value)
{
SaveAndApplySettings();
WriteNumericSetting(value, deviceService.WriteManualDisplacementAsync, "手动位移");
}
partial void OnTestSpeedChanged(string value)
{
OnPropertyChanged(nameof(TestSpeedText));
SaveDeviceSettings();
SaveAndApplySettings();
WriteNumericSetting(value, deviceService.WriteTestSpeedAsync, "测试速度");
}
partial void OnNormalPressureCoefficientChanged(string value) => SaveDeviceSettings();
partial void OnNormalPressureZeroChanged(string value) => SaveAndApplySettings();
partial void OnFrictionCoefficient1Changed(string value) => SaveDeviceSettings();
partial void OnNormalPressureCoefficientChanged(string value) => SaveAndApplySettings();
partial void OnFrictionCoefficient2Changed(string value) => SaveDeviceSettings();
partial void OnFrictionZero1Changed(string value) => SaveAndApplySettings();
partial void OnFrictionCoefficient1Changed(string value) => SaveAndApplySettings();
partial void OnFrictionZero2Changed(string value) => SaveAndApplySettings();
partial void OnFrictionCoefficient2Changed(string value) => SaveAndApplySettings();
partial void OnPlcPortNameChanged(string value) => SaveAndApplySettings();
partial void OnAdcPortNameChanged(string value) => SaveAndApplySettings();
partial void OnBaudRateChanged(int value) => SaveAndApplySettings();
partial void OnSelectedShoeSizeIndexChanged(int value) => UpdateTargetLoad();
partial void OnSelectedModeIndexChanged(int value)
{
ActiveMode = CurrentMode();
}
public void Dispose()
{
refreshTimer.Stop();
deviceService.Dispose();
}
private void RefreshFromDevice()
{
var device = deviceService.CurrentSnapshot;
DeviceStatus = device.IsConnected
? device.IsTestRunning ? "联机 / 测试中" : device.IsResetting ? "联机 / 复位中" : "联机 / 待机"
: "离线";
VerticalPressure = device.VerticalLoadN.ToString("F1", CultureInfo.InvariantCulture);
HorizontalForce = device.HorizontalFrictionN.ToString("F1", CultureInfo.InvariantCulture);
FrictionCoefficient = device.FrictionCoefficient.ToString("F3", CultureInfo.InvariantCulture);
Distance = device.DisplacementMm.ToString("F1", CultureInfo.InvariantCulture);
ActualLoadText = $"{VerticalPressure} N";
if (!device.IsConnected && !string.IsNullOrWhiteSpace(device.LastError))
{
CurrentStatus = $"设备离线:{device.LastError}";
}
if (!wasRunning && device.IsTestRunning)
{
BeginRun();
}
if (device.IsTestRunning)
{
RecordPoint(device);
}
if (wasRunning && !device.IsTestRunning)
{
CompleteRun();
}
wasRunning = device.IsTestRunning;
}
private void BeginRun()
{
currentRun.Clear();
verticalLoadPoints.Clear();
horizontalFrictionPoints.Clear();
frictionCoefficientPoints.Clear();
displacementPoints.Clear();
runStopwatch.Restart();
UploadProgress = 0;
CurrentStatus = "测试运行:按标准采集垂直载荷、摩擦力、位移与摩擦系数";
}
private void RecordPoint(SlipDeviceSnapshot device)
{
var time = runStopwatch.Elapsed.TotalSeconds;
if (currentRun.Count > 0 && time - currentRun[^1].TimeSeconds < 1.0 / 30.0)
{
return;
}
var point = new SlipDataPoint(
device.Timestamp,
time,
device.VerticalLoadN,
device.HorizontalFrictionN,
device.DisplacementMm,
device.FrictionCoefficient);
currentRun.Add(point);
verticalLoadPoints.Add(new ObservablePoint(time, point.VerticalLoadN));
horizontalFrictionPoints.Add(new ObservablePoint(time, point.HorizontalFrictionN));
frictionCoefficientPoints.Add(new ObservablePoint(time, point.FrictionCoefficient));
displacementPoints.Add(new ObservablePoint(time, point.DisplacementMm));
UploadProgress = Math.Min(99, currentRun.Count);
}
private void CompleteRun()
{
runStopwatch.Stop();
if (currentRun.Count < 3)
{
CurrentStatus = "测试已停止,但有效采样点不足,未生成结果";
return;
}
lastCompletedRun = currentRun.ToList();
var peak = FindStaticPeak(currentRun);
var dynamicWindow = currentRun
.Where(point => point.TimeSeconds >= 0.3 && point.TimeSeconds <= 0.6)
.ToList();
if (dynamicWindow.Count == 0)
{
dynamicWindow = currentRun.ToList();
}
var staticCoefficientValue = CalculateCoefficient(peak.HorizontalFrictionN, peak.VerticalLoadN);
var dynamicForce = dynamicWindow.Average(point => point.HorizontalFrictionN);
var dynamicLoad = dynamicWindow.Average(point => point.VerticalLoadN);
var dynamicCoefficientValue = CalculateCoefficient(dynamicForce, dynamicLoad);
var verdict = NeedsRetest(staticCoefficientValue, dynamicCoefficientValue) ? "需重测" : "有效";
var nextIndex = Samples.Count == 0 ? 1 : Samples.Max(sample => sample.Index) + 1;
Samples.Insert(0, new TestSample(
nextIndex,
DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
staticCoefficientValue.ToString("F3", CultureInfo.InvariantCulture),
dynamicCoefficientValue.ToString("F3", CultureInfo.InvariantCulture),
verdict,
staticCoefficientValue,
dynamicCoefficientValue));
StaticCoefficient = staticCoefficientValue.ToString("F3", CultureInfo.InvariantCulture);
DynamicCoefficient = dynamicCoefficientValue.ToString("F3", CultureInfo.InvariantCulture);
UpdateResultSummary();
UploadProgress = 100;
CurrentStatus = verdict == "有效"
? "测试完成:已按标准生成静/动摩擦系数"
: "测试完成:最近三次结果差异超过 10%,建议重新测试";
}
private bool NeedsRetest(double staticCoefficientValue, double dynamicCoefficientValue)
{
var latest = Samples.Take(2).ToList();
if (latest.Count < 2)
{
return false;
}
var staticValues = latest.Select(sample => sample.StaticCoefficientValue).Append(staticCoefficientValue).ToArray();
var dynamicValues = latest.Select(sample => sample.DynamicCoefficientValue).Append(dynamicCoefficientValue).ToArray();
return ExceedsTenPercent(staticValues) || ExceedsTenPercent(dynamicValues);
}
private static bool ExceedsTenPercent(double[] values)
{
var average = values.Average();
if (Math.Abs(average) < 0.0001)
{
return false;
}
return (values.Max() - values.Min()) / Math.Abs(average) > 0.10;
}
private void UpdateResultSummary()
{
var latest = Samples.Take(3).ToList();
if (latest.Count == 0)
{
ResultSummary = "等待 3 次有效试验";
return;
}
var staticAverage = latest.Average(sample => sample.StaticCoefficientValue);
var dynamicAverage = latest.Average(sample => sample.DynamicCoefficientValue);
ResultSummary = $"近 {latest.Count} 次平均 静 {staticAverage:F3} / 动 {dynamicAverage:F3}";
}
private static SlipDataPoint FindStaticPeak(IReadOnlyList<SlipDataPoint> points)
{
for (var index = 1; index < points.Count - 1; index++)
{
var previous = points[index - 1].HorizontalFrictionN;
var current = points[index].HorizontalFrictionN;
var next = points[index + 1].HorizontalFrictionN;
if (current >= previous && current >= next && current > 0)
{
return points[index];
}
}
return points.MaxBy(point => point.HorizontalFrictionN) ?? points[0];
}
private static double CalculateCoefficient(double frictionForce, double verticalLoad) =>
Math.Abs(verticalLoad) > 0.0001 ? frictionForce / verticalLoad : 0;
private async Task RunDeviceCommand(Task command, string successMessage)
{
try
{
await command;
CurrentStatus = successMessage;
}
catch (Exception ex)
{
CurrentStatus = $"设备指令失败:{ex.Message}";
}
}
private void WriteNumericSetting(string value, Func<double, Task> writer, string label)
{
if (isLoadingDeviceSettings || !TryParseDouble(value, out var numericValue))
{
return;
}
_ = RunDeviceCommand(writer(numericValue), $"{label}已写入 PLC");
}
private void SaveAndApplySettings()
{
SaveDeviceSettings();
deviceService.UpdateSettings(CurrentSettings());
}
private void LoadDeviceSettings()
{
@@ -346,9 +611,15 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
ManualSpeed = settings.ManualSpeed ?? ManualSpeed;
ManualDisplacement = settings.ManualDisplacement ?? ManualDisplacement;
TestSpeed = settings.TestSpeed ?? TestSpeed;
NormalPressureZero = settings.NormalPressureZero ?? NormalPressureZero;
NormalPressureCoefficient = settings.NormalPressureCoefficient ?? NormalPressureCoefficient;
FrictionZero1 = settings.FrictionZero1 ?? FrictionZero1;
FrictionCoefficient1 = settings.FrictionCoefficient1 ?? FrictionCoefficient1;
FrictionZero2 = settings.FrictionZero2 ?? FrictionZero2;
FrictionCoefficient2 = settings.FrictionCoefficient2 ?? FrictionCoefficient2;
PlcPortName = settings.PlcPortName ?? PlcPortName;
AdcPortName = settings.AdcPortName ?? AdcPortName;
BaudRate = settings.BaudRate > 0 ? settings.BaudRate : BaudRate;
}
catch
{
@@ -369,14 +640,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
try
{
Directory.CreateDirectory(Path.GetDirectoryName(DeviceSettingsPath)!);
var settings = new DeviceSettings(
ManualSpeed,
ManualDisplacement,
TestSpeed,
NormalPressureCoefficient,
FrictionCoefficient1,
FrictionCoefficient2);
var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
var json = JsonSerializer.Serialize(CurrentSettings(), new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(DeviceSettingsPath, json);
}
catch
@@ -384,54 +648,90 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
}
}
private DeviceSettings CurrentSettings() =>
new(
ManualSpeed,
ManualDisplacement,
TestSpeed,
NormalPressureZero,
NormalPressureCoefficient,
FrictionZero1,
FrictionCoefficient1,
FrictionZero2,
FrictionCoefficient2,
PlcPortName,
AdcPortName,
BaudRate);
private void UpdateTargetLoad()
{
TargetLoadText = SelectedShoeSizeIndex switch
{
1 => "350 N",
2 => "160 N",
_ => "400 N"
};
}
private string CurrentShoeSize() => SelectedShoeSizeIndex switch
{
1 => "205-250",
2 => "205以下",
_ => "250(含)以上"
};
private string CurrentLubricant() => SelectedLubricantIndex switch
{
1 => "湿态 - 蒸馏水",
2 => "湿态 - 洗涤剂",
3 => "冰霜",
_ => "干态"
};
private string CurrentMode() => SelectedModeIndex switch
{
0 => "后跟测试模式",
1 => "前掌测试模式",
_ => "水平测试模式"
};
private string CurrentSurface() => SelectedSurfaceIndex switch
{
1 => "木地板",
2 => "石材",
3 => "冰霜表面",
_ => "瓷砖"
};
private static bool TryParseDouble(string value, out double numericValue) =>
double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out numericValue)
|| double.TryParse(value, NumberStyles.Float, CultureInfo.CurrentCulture, out numericValue);
private static LineSeries<ObservablePoint> CreateLineSeries(
string name,
ObservableCollection<ObservablePoint> values,
string color,
int yAxis,
double smoothness) =>
new()
{
Name = name,
Values = values,
MiniatureShapeSize = 8,
MiniatureStrokeThickness = 2,
Stroke = new SolidColorPaint(SKColor.Parse(color)) { StrokeThickness = 3.2f },
Fill = new SolidColorPaint(SKColors.Transparent),
GeometryFill = new SolidColorPaint(SKColors.White),
GeometryStroke = new SolidColorPaint(SKColor.Parse(color)) { StrokeThickness = 2 },
GeometrySize = 4,
LineSmoothness = smoothness,
ScalesYAt = yAxis
};
private static string DeviceSettingsPath =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"FootwearSlipResistance",
"device-settings.json");
private static ObservablePoint[] CreatePressureSeries() =>
[
new(-0.10, 36), new(-0.08, 42), new(-0.06, 50), new(-0.04, 90),
new(-0.02, 400), new(0.00, 405), new(0.05, 407), new(0.10, 409),
new(0.20, 407), new(0.30, 412), new(0.40, 421), new(0.50, 426),
new(0.62, 430)
];
private static ObservablePoint[] CreatePullSeries() =>
[
new(-0.10, 0), new(-0.05, 0), new(0.00, 160), new(0.04, 226),
new(0.08, 232), new(0.16, 236), new(0.26, 241), new(0.36, 248),
new(0.46, 255), new(0.56, 260), new(0.62, 266)
];
private static ObservablePoint[] CreateFrictionSeries() =>
[
new(-0.10, 0), new(-0.02, 0), new(0.00, 0.47), new(0.04, 0.55),
new(0.08, 0.57), new(0.16, 0.56), new(0.26, 0.58), new(0.36, 0.59),
new(0.46, 0.60), new(0.56, 0.61), new(0.62, 0.62)
];
private static ObservablePoint[] CreateDistanceSeries() =>
[
new(-0.10, 0), new(0.00, 0), new(0.06, 20), new(0.12, 42),
new(0.20, 72), new(0.30, 108), new(0.40, 145), new(0.50, 178),
new(0.62, 218)
];
}
public sealed record TestSample(
int Index,
string Time,
string StaticCoefficient,
string DynamicCoefficient,
string Verdict);
internal sealed record DeviceSettings(
string ManualSpeed,
string ManualDisplacement,
string TestSpeed,
string NormalPressureCoefficient,
string FrictionCoefficient1,
string FrictionCoefficient2);
}

View File

@@ -1,6 +1,7 @@
<suki:SukiWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModels"
xmlns:model="using:Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:suki="using:SukiUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -197,13 +198,13 @@
<TextBox Grid.Column="3" Text="{Binding OperatorName}" Watermark="请输入"/>
<TextBlock Grid.Row="1" Text="鞋码区间" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
<ComboBox Grid.Row="1" Grid.Column="1" SelectedIndex="0">
<ComboBox Grid.Row="1" Grid.Column="1" SelectedIndex="{Binding SelectedShoeSizeIndex}">
<ComboBoxItem Content="250(含)以上 - 400N"/>
<ComboBoxItem Content="205-250 - 350N"/>
<ComboBoxItem Content="205以下 - 160N"/>
</ComboBox>
<TextBlock Grid.Row="1" Grid.Column="2" Text="润滑介质" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
<ComboBox Grid.Row="1" Grid.Column="3" SelectedIndex="0">
<ComboBox Grid.Row="1" Grid.Column="3" SelectedIndex="{Binding SelectedLubricantIndex}">
<ComboBoxItem Content="干态"/>
<ComboBoxItem Content="湿态 - 蒸馏水"/>
<ComboBoxItem Content="湿态 - 洗涤剂"/>
@@ -211,7 +212,7 @@
</ComboBox>
<TextBlock Grid.Row="2" Text="测试模式" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
<ComboBox Grid.Row="2" Grid.Column="1" SelectedIndex="2">
<ComboBox Grid.Row="2" Grid.Column="1" SelectedIndex="{Binding SelectedModeIndex}">
<ComboBoxItem Content="后跟测试模式"/>
<ComboBoxItem Content="前掌测试模式"/>
<ComboBoxItem Content="水平测试模式"/>
@@ -222,11 +223,11 @@
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="Auto,*" RowSpacing="10" ColumnSpacing="10">
<TextBlock Text="报告名称" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBox Grid.Column="1" Text="{Binding ReportName}" Watermark="例: 白羊座防滑性能报告"/>
<TextBox Grid.Column="1" Text="{Binding ReportName}" Watermark="例: 防滑性能报告"/>
<TextBlock Grid.Row="1" Text="样品特征" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding SampleFeature}" Watermark="鞋底材料、花纹、尺码"/>
<TextBlock Grid.Row="2" Text="地面材质" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"/>
<ComboBox Grid.Row="2" Grid.Column="1" SelectedIndex="0">
<ComboBox Grid.Row="2" Grid.Column="1" SelectedIndex="{Binding SelectedSurfaceIndex}">
<ComboBoxItem Content="瓷砖"/>
<ComboBoxItem Content="木地板"/>
<ComboBoxItem Content="石材"/>
@@ -280,7 +281,7 @@
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Samples}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:TestSample">
<DataTemplate x:DataType="model:TestSample">
<Border BorderBrush="{StaticResource LineBrush}" BorderThickness="0,0,0,1" Padding="0,6">
<Grid ColumnDefinitions="50,98,72,72">
<TextBlock Text="{Binding Index}" HorizontalAlignment="Center" FontSize="15" Foreground="#334155"/>
@@ -327,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="生成报告" Classes="primary action" Command="{Binding ExportReportCommand}"/>
<Button Grid.Row="1" Grid.Column="1" Content="导出Excel" Classes="primary action" Command="{Binding ExportReportCommand}"/>
</Grid>
<StackPanel Grid.Row="2" Spacing="8">
@@ -346,6 +347,34 @@
</Grid>
</StackPanel>
<ScrollViewer Grid.Row="4"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
ClipToBounds="True">
<StackPanel Spacing="8">
<TextBlock Text="标准数据" FontWeight="SemiBold"/>
<Border Classes="metric">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="*,Auto" RowSpacing="7">
<TextBlock Text="设备状态" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBlock Grid.Column="1" Text="{Binding DeviceStatus}" FontWeight="SemiBold"/>
<TextBlock Grid.Row="1" Text="正压力(N)" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding VerticalPressure}" FontWeight="SemiBold"/>
<TextBlock Grid.Row="2" Text="摩擦力(N)" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding HorizontalForce}" FontWeight="SemiBold"/>
<TextBlock Grid.Row="3" Text="位移(mm)" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding Distance}" FontWeight="SemiBold"/>
<TextBlock Grid.Row="4" Text="实时系数" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBlock Grid.Row="4" Grid.Column="1" Text="{Binding FrictionCoefficient}" FontWeight="SemiBold"/>
<TextBlock Grid.Row="5" Text="静摩擦系数" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBlock Grid.Row="5" Grid.Column="1" Text="{Binding StaticCoefficient}" FontWeight="SemiBold" Foreground="#5B21B6"/>
<TextBlock Grid.Row="6" Text="动摩擦系数" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBlock Grid.Row="6" Grid.Column="1" Text="{Binding DynamicCoefficient}" FontWeight="SemiBold" Foreground="#047857"/>
</Grid>
</Border>
<TextBlock Text="{Binding ResultSummary}" TextWrapping="Wrap" Classes="caption"/>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</Grid>
@@ -353,7 +382,7 @@
<Grid IsVisible="{Binding IsSettingsDialogOpen}"
Background="#800F172A">
<Border Width="680"
<Border Width="760"
Background="{StaticResource PanelBrush}"
BorderBrush="{StaticResource LineBrush}"
BorderThickness="1"
@@ -374,7 +403,7 @@
</Grid>
<Grid Grid.Row="1"
RowDefinitions="Auto,Auto,Auto,8,Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,8,Auto,Auto,Auto"
ColumnDefinitions="Auto,150,Auto,24,Auto,150"
RowSpacing="10"
ColumnSpacing="12">
@@ -390,16 +419,34 @@
<TextBox Grid.Row="2" Grid.Column="1" Classes="setting-value" Text="{Binding TestSpeed, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Row="2" Grid.Column="2" Text="m/s" Classes="setting-unit" VerticalAlignment="Center"/>
<Border Grid.Row="3" Grid.ColumnSpan="6" Height="1" Background="{StaticResource LineBrush}" Margin="0,4"/>
<Label Grid.Row="3" Content="PLC串口" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="3" Grid.Column="1" Classes="setting-value" Text="{Binding PlcPortName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="4" Content="正压力系数" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="4" Grid.Column="1" Classes="setting-value" Text="{Binding NormalPressureCoefficient, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="3" Grid.Column="4" Content="ADC串口" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="3" Grid.Column="5" Classes="setting-value" Text="{Binding AdcPortName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="5" Content="摩擦1系数" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="5" Grid.Column="1" Classes="setting-value" Text="{Binding FrictionCoefficient1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="4" Content="波特率" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="4" Grid.Column="1" Classes="setting-value" Text="{Binding BaudRate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="5" Grid.Column="4" Content="摩擦2系数" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="5" Grid.Column="5" Classes="setting-value" Text="{Binding FrictionCoefficient2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Border Grid.Row="5" Grid.ColumnSpan="6" Height="1" Background="{StaticResource LineBrush}" Margin="0,4"/>
<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}"/>
<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}"/>
<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}"/>
<Label Grid.Row="8" Content="摩擦2零点" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="8" Grid.Column="1" Classes="setting-value" Text="{Binding FrictionZero2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="8" Grid.Column="4" Content="摩擦2系数" Classes="setting-label" VerticalAlignment="Center"/>
<TextBox Grid.Row="8" Grid.Column="5" Classes="setting-value" Text="{Binding FrictionCoefficient2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
<Grid Grid.Row="2"

View File

@@ -1,4 +1,6 @@
using SukiUI.Controls;
using System;
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModels;
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Views
{
@@ -7,6 +9,15 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Views
public MainWindow()
{
InitializeComponent();
Closed += OnClosed;
}
private void OnClosed(object? sender, EventArgs e)
{
if (DataContext is MainWindowViewModel viewModel)
{
viewModel.Dispose();
}
}
}
}