更新数据接口

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

@@ -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()
{
}
}
}
}