更新20260527
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using Serilog;
|
||||
|
||||
namespace ConeCalorimeter
|
||||
{
|
||||
@@ -9,6 +10,74 @@ namespace ConeCalorimeter
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
ConfigureLogging();
|
||||
DispatcherUnhandledException += OnDispatcherUnhandledException;
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
||||
|
||||
Log.Information("ConeCalorimeter application starting.");
|
||||
base.OnStartup(e);
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Information("ConeCalorimeter application exiting with code {ExitCode}.", e.ApplicationExitCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
|
||||
base.OnExit(e);
|
||||
}
|
||||
|
||||
private static void ConfigureLogging()
|
||||
{
|
||||
var logDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"ConeCalorimeter",
|
||||
"Logs");
|
||||
Directory.CreateDirectory(logDirectory);
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.Enrich.WithProperty("Application", "ConeCalorimeter")
|
||||
.WriteTo.File(
|
||||
Path.Combine(logDirectory, "app-.log"),
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 30,
|
||||
shared: true,
|
||||
flushToDiskInterval: TimeSpan.FromSeconds(1),
|
||||
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
private static void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
Log.Fatal(e.Exception, "Unhandled WPF dispatcher exception.");
|
||||
}
|
||||
|
||||
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
if (e.ExceptionObject is Exception exception)
|
||||
{
|
||||
Log.Fatal(exception, "Unhandled AppDomain exception. IsTerminating={IsTerminating}", e.IsTerminating);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Fatal(
|
||||
"Unhandled AppDomain exception object: {ExceptionObject}. IsTerminating={IsTerminating}",
|
||||
e.ExceptionObject,
|
||||
e.IsTerminating);
|
||||
}
|
||||
|
||||
private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
Log.Error(e.Exception, "Unobserved task exception.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<PackageReference Include="NPOI" Version="2.7.2" />
|
||||
<PackageReference Include="OxyPlot.Wpf" Version="2.2.0" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="System.IO.Ports" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Windows;
|
||||
using ConeCalorimeter.Services;
|
||||
using ConeCalorimeter.ViewModels;
|
||||
using Serilog;
|
||||
|
||||
namespace ConeCalorimeter
|
||||
{
|
||||
@@ -13,10 +14,15 @@ namespace ConeCalorimeter
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_tcpDeviceConnectionService = new TcpDeviceConnectionService(
|
||||
TcpDeviceConnectionOptions.FromEnvironment());
|
||||
_scaleService = new ModbusRtuScaleService(
|
||||
SerialScaleOptions.FromEnvironment());
|
||||
var tcpOptions = TcpDeviceConnectionOptions.FromEnvironment();
|
||||
var scaleOptions = SerialScaleOptions.FromEnvironment();
|
||||
|
||||
_tcpDeviceConnectionService = new TcpDeviceConnectionService(tcpOptions);
|
||||
_scaleService = new ModbusRtuScaleService(scaleOptions);
|
||||
Log.Information(
|
||||
"Main window initialized. TCP endpoint={Endpoint}, scale port={ScalePort}.",
|
||||
_tcpDeviceConnectionService.Endpoint,
|
||||
scaleOptions.PortName);
|
||||
_ = _tcpDeviceConnectionService.StartAsync();
|
||||
|
||||
var experimentDataService = new ExperimentDataService(
|
||||
@@ -31,6 +37,7 @@ namespace ConeCalorimeter
|
||||
|
||||
protected override async void OnClosed(EventArgs e)
|
||||
{
|
||||
Log.Information("Main window closing; disposing device services.");
|
||||
_scaleService.Dispose();
|
||||
await _tcpDeviceConnectionService.DisposeAsync();
|
||||
base.OnClosed(e);
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Windows.Threading;
|
||||
using ConeCalorimeter.Models;
|
||||
using Serilog;
|
||||
|
||||
namespace ConeCalorimeter.Services;
|
||||
|
||||
@@ -52,11 +53,20 @@ public sealed class ExperimentDataService : IExperimentDataService
|
||||
{
|
||||
_initialMass = CurrentSnapshot.CurrentMass;
|
||||
}
|
||||
|
||||
Log.Information(
|
||||
"Experiment test started. InitialMass={InitialMass}, SnapshotSeconds={TestSeconds}.",
|
||||
_initialMass,
|
||||
CurrentSnapshot.TestSeconds);
|
||||
}
|
||||
|
||||
public void StopTest()
|
||||
{
|
||||
_isTestRunning = false;
|
||||
Log.Information(
|
||||
"Experiment test stopped. Records={RecordCount}, LastRecordedSeconds={LastRecordedSeconds}.",
|
||||
Records.Count,
|
||||
_lastRecordedSeconds);
|
||||
}
|
||||
|
||||
public void ClearRecords()
|
||||
@@ -87,6 +97,7 @@ public sealed class ExperimentDataService : IExperimentDataService
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Realtime snapshot polling failed: {ex.Message}");
|
||||
Log.Warning(ex, "Realtime snapshot polling failed.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using Serilog;
|
||||
|
||||
namespace ConeCalorimeter.Services;
|
||||
|
||||
@@ -20,6 +21,12 @@ public sealed class ModbusRtuScaleService : IScaleService
|
||||
public ModbusRtuScaleService(SerialScaleOptions options)
|
||||
{
|
||||
_options = options;
|
||||
Log.Information(
|
||||
"Scale service created. Port={PortName}, BaudRate={BaudRate}, UnitId={UnitId}, Register=D{RegisterAddress}.",
|
||||
_options.PortName,
|
||||
_options.BaudRate,
|
||||
_options.UnitId,
|
||||
_options.RegisterAddress);
|
||||
}
|
||||
|
||||
public bool TryReadCurrentMass(out double value)
|
||||
@@ -51,6 +58,12 @@ public sealed class ModbusRtuScaleService : IScaleService
|
||||
or ArgumentException)
|
||||
{
|
||||
Debug.WriteLine($"Scale read failed on {_options.PortName}: {ex.Message}");
|
||||
Log.Warning(
|
||||
ex,
|
||||
"Scale read failed. Port={PortName}, UnitId={UnitId}, Register=D{RegisterAddress}.",
|
||||
_options.PortName,
|
||||
_options.UnitId,
|
||||
_options.RegisterAddress);
|
||||
CloseCurrentPort();
|
||||
return false;
|
||||
}
|
||||
@@ -87,6 +100,7 @@ public sealed class ModbusRtuScaleService : IScaleService
|
||||
|
||||
port.Open();
|
||||
_serialPort = port;
|
||||
Log.Information("Scale serial port opened. Port={PortName}.", _options.PortName);
|
||||
return port;
|
||||
}
|
||||
|
||||
@@ -110,6 +124,7 @@ public sealed class ModbusRtuScaleService : IScaleService
|
||||
finally
|
||||
{
|
||||
port.Dispose();
|
||||
Log.Information("Scale serial port closed. Port={PortName}.", _options.PortName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +213,15 @@ public sealed class ModbusRtuScaleService : IScaleService
|
||||
Debug.WriteLine(
|
||||
$"Scale mass out of range raw [{result.RawHex}], ABCD={result.Abcd:G9}, "
|
||||
+ $"CDAB={result.Cdab:G9}, BADC={result.Badc:G9}, DCBA={result.Dcba:G9}.");
|
||||
Log.Warning(
|
||||
"Scale mass out of range. Raw={RawHex}, ABCD={Abcd}, CDAB={Cdab}, BADC={Badc}, DCBA={Dcba}, Range={MinimumMass}..{MaximumMass}.",
|
||||
result.RawHex,
|
||||
result.Abcd,
|
||||
result.Cdab,
|
||||
result.Badc,
|
||||
result.Dcba,
|
||||
MinimumMass,
|
||||
MaximumMass);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Diagnostics;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using Serilog;
|
||||
|
||||
namespace ConeCalorimeter.Services;
|
||||
|
||||
@@ -34,6 +35,10 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
{
|
||||
_options = options;
|
||||
_statusText = $"未连接设备 {_options.Host}:{_options.Port}";
|
||||
Log.Information(
|
||||
"TCP device service created. Endpoint={Endpoint}, UnitId={UnitId}.",
|
||||
Endpoint,
|
||||
_options.UnitId);
|
||||
}
|
||||
|
||||
public bool IsConnected
|
||||
@@ -112,6 +117,7 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
|
||||
cancellation?.Dispose();
|
||||
Debug.WriteLine("TCP device connection service stopped.");
|
||||
Log.Information("TCP device connection service stopped. Endpoint={Endpoint}.", Endpoint);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
@@ -139,6 +145,11 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device register {registerAddress} read failed: {ex.Message}");
|
||||
Log.Warning(
|
||||
ex,
|
||||
"TCP float register read failed. Endpoint={Endpoint}, Register=D{RegisterAddress}.",
|
||||
Endpoint,
|
||||
registerAddress);
|
||||
SetConnectionState(false, $"读取寄存器 {registerAddress} 失败:{ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
@@ -166,6 +177,11 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device register {registerAddress} float values read failed: {ex.Message}");
|
||||
Log.Warning(
|
||||
ex,
|
||||
"TCP float raw register read failed. Endpoint={Endpoint}, Register=D{RegisterAddress}.",
|
||||
Endpoint,
|
||||
registerAddress);
|
||||
SetConnectionState(false, $"读取寄存器 {registerAddress} 失败:{ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
@@ -193,6 +209,11 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device register {registerAddress} read failed: {ex.Message}");
|
||||
Log.Warning(
|
||||
ex,
|
||||
"TCP int16 register read failed. Endpoint={Endpoint}, Register=D{RegisterAddress}.",
|
||||
Endpoint,
|
||||
registerAddress);
|
||||
SetConnectionState(false, $"读取寄存器 {registerAddress} 失败:{ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
@@ -213,11 +234,22 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
try
|
||||
{
|
||||
WriteInt16(_client, registerAddress, value);
|
||||
Log.Information(
|
||||
"TCP int16 register write succeeded. Endpoint={Endpoint}, Register=D{RegisterAddress}, Value={Value}.",
|
||||
Endpoint,
|
||||
registerAddress,
|
||||
value);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device register {registerAddress} write failed: {ex.Message}");
|
||||
Log.Warning(
|
||||
ex,
|
||||
"TCP int16 register write failed. Endpoint={Endpoint}, Register=D{RegisterAddress}, Value={Value}.",
|
||||
Endpoint,
|
||||
registerAddress,
|
||||
value);
|
||||
SetConnectionState(false, $"写入寄存器 {registerAddress} 失败:{ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
@@ -238,11 +270,22 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
try
|
||||
{
|
||||
WriteFloat(_client, registerAddress, value);
|
||||
Log.Information(
|
||||
"TCP float register write succeeded. Endpoint={Endpoint}, Register=D{RegisterAddress}, Value={Value}.",
|
||||
Endpoint,
|
||||
registerAddress,
|
||||
value);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device register {registerAddress} float write failed: {ex.Message}");
|
||||
Log.Warning(
|
||||
ex,
|
||||
"TCP float register write failed. Endpoint={Endpoint}, Register=D{RegisterAddress}, Value={Value}.",
|
||||
Endpoint,
|
||||
registerAddress,
|
||||
value);
|
||||
SetConnectionState(false, $"写入寄存器 {registerAddress} 失败:{ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
@@ -270,6 +313,11 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device coil {coilAddress} read failed: {ex.Message}");
|
||||
Log.Warning(
|
||||
ex,
|
||||
"TCP coil read failed. Endpoint={Endpoint}, Coil=M{CoilAddress}.",
|
||||
Endpoint,
|
||||
coilAddress);
|
||||
SetConnectionState(false, $"读取线圈 {coilAddress} 失败:{ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
@@ -290,11 +338,22 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
try
|
||||
{
|
||||
WriteCoil(_client, coilAddress, value);
|
||||
Log.Information(
|
||||
"TCP coil write succeeded. Endpoint={Endpoint}, Coil=M{CoilAddress}, Value={Value}.",
|
||||
Endpoint,
|
||||
coilAddress,
|
||||
value);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device coil {coilAddress} write failed: {ex.Message}");
|
||||
Log.Warning(
|
||||
ex,
|
||||
"TCP coil write failed. Endpoint={Endpoint}, Coil=M{CoilAddress}, Value={Value}.",
|
||||
Endpoint,
|
||||
coilAddress,
|
||||
value);
|
||||
SetConnectionState(false, $"写入线圈 {coilAddress} 失败:{ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
@@ -318,6 +377,7 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"TCP device connection failed: {ex.Message}");
|
||||
Log.Warning(ex, "TCP device connection loop failed. Endpoint={Endpoint}.", Endpoint);
|
||||
SetConnectionState(false, $"连接设备失败:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
@@ -340,6 +400,7 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
{
|
||||
SetConnectionState(false, $"正在连接设备 {_options.Host}:{_options.Port}...");
|
||||
Debug.WriteLine($"Connecting TCP device at {_options.Host}:{_options.Port}.");
|
||||
Log.Information("Connecting TCP device. Endpoint={Endpoint}, UnitId={UnitId}.", Endpoint, _options.UnitId);
|
||||
|
||||
var client = new TcpClient();
|
||||
var connectTask = client.ConnectAsync(_options.Host, _options.Port, cancellationToken).AsTask();
|
||||
@@ -363,6 +424,7 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
|
||||
SetConnectionState(true, $"已连接设备 {_options.Host}:{_options.Port}");
|
||||
Debug.WriteLine($"TCP device connected at {_options.Host}:{_options.Port}.");
|
||||
Log.Information("TCP device connected. Endpoint={Endpoint}.", Endpoint);
|
||||
}
|
||||
|
||||
private async Task MonitorConnectionAsync(CancellationToken cancellationToken)
|
||||
@@ -378,6 +440,7 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
if (client is null || !IsTcpClientConnected(client))
|
||||
{
|
||||
Debug.WriteLine("TCP device connection lost.");
|
||||
Log.Warning("TCP device connection lost. Endpoint={Endpoint}.", Endpoint);
|
||||
SetConnectionState(false, "设备连接已断开");
|
||||
return;
|
||||
}
|
||||
@@ -598,6 +661,11 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
|
||||
if (changed)
|
||||
{
|
||||
Log.Information(
|
||||
"TCP device state changed. Endpoint={Endpoint}, IsConnected={IsConnected}, Status={StatusText}.",
|
||||
Endpoint,
|
||||
isConnected,
|
||||
statusText);
|
||||
ConnectionStatusChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Windows.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ConeCalorimeter.Services;
|
||||
using Serilog;
|
||||
|
||||
namespace ConeCalorimeter.ViewModels;
|
||||
|
||||
@@ -461,6 +462,34 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
+ $"ABCD={result.Abcd:G9}, CDAB={result.Cdab:G9}, "
|
||||
+ $"BADC={result.Badc:G9}, DCBA={result.Dcba:G9}, "
|
||||
+ $"preferred={preferredText}, selected={selectedText}, matches={matchCount}.");
|
||||
if (selectedByteOrder is null)
|
||||
{
|
||||
Log.Warning(
|
||||
"C value calibration float has no valid decoded value. Label={Label}, Register=D{RegisterAddress}, Raw={RawHex}, ABCD={Abcd}, CDAB={Cdab}, BADC={Badc}, DCBA={Dcba}, Preferred={PreferredByteOrder}, Matches={MatchCount}.",
|
||||
label,
|
||||
registerAddress,
|
||||
result.RawHex,
|
||||
result.Abcd,
|
||||
result.Cdab,
|
||||
result.Badc,
|
||||
result.Dcba,
|
||||
preferredText,
|
||||
matchCount);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Information(
|
||||
"C value calibration float decoded. Label={Label}, Register=D{RegisterAddress}, Raw={RawHex}, ABCD={Abcd}, CDAB={Cdab}, BADC={Badc}, DCBA={Dcba}, Preferred={PreferredByteOrder}, Selected={SelectedByteOrder}, Matches={MatchCount}.",
|
||||
label,
|
||||
registerAddress,
|
||||
result.RawHex,
|
||||
result.Abcd,
|
||||
result.Cdab,
|
||||
result.Badc,
|
||||
result.Dcba,
|
||||
preferredText,
|
||||
selectedText,
|
||||
matchCount);
|
||||
}
|
||||
|
||||
private void LogFloatReadFailure(string label, ushort registerAddress, string statusText)
|
||||
@@ -469,11 +498,21 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
$"CValueCalibration label={label} register=D{registerAddress} endpoint={_tcpDeviceConnectionService.Endpoint} "
|
||||
+ $"status={statusText} connection=\"{_tcpDeviceConnectionService.StatusText}\"";
|
||||
|
||||
WriteFloatDiagnosticToFile(registerAddress, diagnosticMessage);
|
||||
var shouldWriteApplicationLog = WriteFloatDiagnosticToFile(registerAddress, diagnosticMessage);
|
||||
Debug.WriteLine($"C value calibration float {label} register {registerAddress} {statusText}.");
|
||||
if (shouldWriteApplicationLog)
|
||||
{
|
||||
Log.Warning(
|
||||
"C value calibration float read failed. Label={Label}, Register=D{RegisterAddress}, Endpoint={Endpoint}, Status={Status}, ConnectionStatus={ConnectionStatus}.",
|
||||
label,
|
||||
registerAddress,
|
||||
_tcpDeviceConnectionService.Endpoint,
|
||||
statusText,
|
||||
_tcpDeviceConnectionService.StatusText);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteFloatDiagnosticToFile(ushort registerAddress, string message)
|
||||
private bool WriteFloatDiagnosticToFile(ushort registerAddress, string message)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
if (_lastFileDiagnosticByRegister.TryGetValue(registerAddress, out var lastMessage)
|
||||
@@ -481,7 +520,7 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
&& _lastFileDiagnosticAtByRegister.TryGetValue(registerAddress, out var lastLoggedAt)
|
||||
&& now - lastLoggedAt < DiagnosticLogInterval)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
_lastFileDiagnosticByRegister[registerAddress] = message;
|
||||
@@ -498,10 +537,13 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
File.AppendAllText(
|
||||
DiagnosticLogFilePath,
|
||||
$"{now:yyyy-MM-dd HH:mm:ss.fff} {message}{Environment.NewLine}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"C value calibration diagnostic log write failed: {ex.Message}");
|
||||
Log.Warning(ex, "C value calibration diagnostic log write failed. Path={DiagnosticLogFilePath}.", DiagnosticLogFilePath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Windows.Threading;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ConeCalorimeter.Models;
|
||||
using ConeCalorimeter.Services;
|
||||
using Serilog;
|
||||
|
||||
namespace ConeCalorimeter.ViewModels;
|
||||
|
||||
@@ -31,10 +32,20 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
private const int TargetTemperatureWriteConfirmationAttempts = 3;
|
||||
private const int TargetTemperatureWriteConfirmationDelayMilliseconds = 80;
|
||||
private const int ParameterRefreshReleaseDelayMilliseconds = 500;
|
||||
private const int HeatingDiagnosticLogIntervalSeconds = 5;
|
||||
private const int HeatingDiagnosticMaximumDurationMinutes = 30;
|
||||
private const int HeatingNoRiseWarningDelaySeconds = 60;
|
||||
private const double HeatingNoRiseTolerance = 1;
|
||||
private const double HeatingAtTargetTolerance = 2;
|
||||
private const double TargetTemperatureMismatchTolerance = 0.05;
|
||||
private const ushort AlarmCoil = 91;
|
||||
private const ushort CirculatingWaterCoil = 49;
|
||||
private const ushort HeatingCoil = 102;
|
||||
private const ushort StartHeatingCoil = 100;
|
||||
private const ushort StopHeatingCoil = 101;
|
||||
private const string CirculatingWaterAction = "循环水";
|
||||
private const string StartHeatingAction = "开始升温";
|
||||
private const string StopHeatingAction = "停止升温";
|
||||
|
||||
private readonly Action _closeAction;
|
||||
private readonly Action _helpAction;
|
||||
@@ -67,6 +78,11 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
private int _pendingCurrentHeatFluxZeroReadCount;
|
||||
private ModbusFloatByteOrder? _currentHeatFluxByteOrder;
|
||||
private DateTime _parameterRefreshBlockedUntil = DateTime.MinValue;
|
||||
private double? _lastConfirmedTargetTemperature;
|
||||
private short? _lastConfirmedTargetTemperatureRaw;
|
||||
private DateTime? _heatingDiagnosticStartedAtUtc;
|
||||
private DateTime _lastHeatingDiagnosticLoggedAtUtc = DateTime.MinValue;
|
||||
private double? _heatingDiagnosticStartTemperature;
|
||||
|
||||
public ConeRadiationSettingsViewModel(
|
||||
Action closeAction,
|
||||
@@ -192,17 +208,29 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "开始升温")
|
||||
if (action == StartHeatingAction)
|
||||
{
|
||||
Log.Information(
|
||||
"Cone radiation start heating requested. TargetInput={TargetInput}, CurrentTemperatureText={CurrentTemperatureText}, HeatingActive={HeatingActive}, Endpoint={Endpoint}.",
|
||||
TargetTemperatureText,
|
||||
CurrentTemperatureText,
|
||||
HeatingActive,
|
||||
_tcpDeviceConnectionService.Endpoint);
|
||||
if (WriteTargetTemperature())
|
||||
{
|
||||
WriteActionCoil(action);
|
||||
if (WriteActionCoil(action))
|
||||
{
|
||||
BeginHeatingDiagnostic();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
WriteActionCoil(action);
|
||||
if (WriteActionCoil(action) && action == StopHeatingAction)
|
||||
{
|
||||
EndHeatingDiagnostic("operator stopped heating");
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshDeviceValues()
|
||||
@@ -237,6 +265,7 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
|
||||
AlarmActive = _tcpDeviceConnectionService.TryReadCoil(AlarmCoil, out var alarmActive) && alarmActive;
|
||||
HeatingActive = _tcpDeviceConnectionService.TryReadCoil(HeatingCoil, out var heatingActive) && heatingActive;
|
||||
LogHeatingDiagnosticIfNeeded(HeatingActive);
|
||||
if (_tcpDeviceConnectionService.TryReadCoil(CirculatingWaterCoil, out var circulatingWaterActive))
|
||||
{
|
||||
CirculatingWaterActive = circulatingWaterActive;
|
||||
@@ -461,6 +490,7 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
{
|
||||
LastAction = "辐射温度输入无效";
|
||||
Debug.WriteLine($"Invalid cone radiation target temperature: {TargetTemperatureText}");
|
||||
Log.Warning("Invalid cone radiation target temperature input. Input={Input}.", TargetTemperatureText);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -469,6 +499,10 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
{
|
||||
LastAction = "辐射温度输入超出范围";
|
||||
Debug.WriteLine($"Cone radiation target temperature out of range: {TargetTemperatureText}");
|
||||
Log.Warning(
|
||||
"Cone radiation target temperature out of int16 range. Input={Input}, ScaledValue={ScaledValue}.",
|
||||
TargetTemperatureText,
|
||||
scaledValue);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -476,13 +510,25 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
if (TryWriteAndConfirmTargetTemperature(registerValue))
|
||||
{
|
||||
_parameterRefreshBlockedUntil = DateTime.UtcNow.AddMilliseconds(ParameterRefreshReleaseDelayMilliseconds);
|
||||
_lastConfirmedTargetTemperature = value;
|
||||
_lastConfirmedTargetTemperatureRaw = registerValue;
|
||||
TargetTemperatureText = value.ToString("0.#", CultureInfo.InvariantCulture);
|
||||
LastAction = "辐射温度设置成功";
|
||||
Log.Information(
|
||||
"Cone radiation target temperature confirmed. DisplayValue={DisplayValue}, RawRegisterValue={RawRegisterValue}, Register=D{RegisterAddress}.",
|
||||
value,
|
||||
registerValue,
|
||||
TargetTemperatureRegister);
|
||||
return true;
|
||||
}
|
||||
|
||||
LastAction = "辐射温度写入未生效";
|
||||
Debug.WriteLine($"Cone radiation target temperature write was not confirmed. Expected raw D{TargetTemperatureRegister}={registerValue}.");
|
||||
Log.Warning(
|
||||
"Cone radiation target temperature write was not confirmed. DisplayValue={DisplayValue}, ExpectedRawValue={ExpectedRawValue}, Register=D{RegisterAddress}.",
|
||||
value,
|
||||
registerValue,
|
||||
TargetTemperatureRegister);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -491,10 +537,24 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
for (var attempt = 1; attempt <= TargetTemperatureWriteConfirmationAttempts; attempt++)
|
||||
{
|
||||
if (_tcpDeviceConnectionService.TryWriteInt16(TargetTemperatureRegister, registerValue)
|
||||
&& _tcpDeviceConnectionService.TryReadInt16(TargetTemperatureRegister, out var readBackValue)
|
||||
&& readBackValue == registerValue)
|
||||
&& _tcpDeviceConnectionService.TryReadInt16(TargetTemperatureRegister, out var readBackValue))
|
||||
{
|
||||
return true;
|
||||
if (readBackValue == registerValue)
|
||||
{
|
||||
Log.Information(
|
||||
"Cone radiation target temperature write confirmed. Attempt={Attempt}, Register=D{RegisterAddress}, RawRegisterValue={RawRegisterValue}.",
|
||||
attempt,
|
||||
TargetTemperatureRegister,
|
||||
registerValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.Warning(
|
||||
"Cone radiation target temperature readback mismatch. Attempt={Attempt}, Register=D{RegisterAddress}, ExpectedRawValue={ExpectedRawValue}, ActualRawValue={ActualRawValue}.",
|
||||
attempt,
|
||||
TargetTemperatureRegister,
|
||||
registerValue,
|
||||
readBackValue);
|
||||
}
|
||||
|
||||
if (attempt < TargetTemperatureWriteConfirmationAttempts)
|
||||
@@ -506,6 +566,125 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
return false;
|
||||
}
|
||||
|
||||
private void BeginHeatingDiagnostic()
|
||||
{
|
||||
_heatingDiagnosticStartedAtUtc = DateTime.UtcNow;
|
||||
_lastHeatingDiagnosticLoggedAtUtc = DateTime.MinValue;
|
||||
_heatingDiagnosticStartTemperature = _lastStableCurrentTemperature;
|
||||
|
||||
Log.Information(
|
||||
"Cone radiation heating diagnostic started. TargetTemperature={TargetTemperature}, RawRegisterValue={RawRegisterValue}, StartTemperature={StartTemperature}, StartCoil=M{StartCoil}, HeatingStateCoil=M{HeatingStateCoil}.",
|
||||
_lastConfirmedTargetTemperature,
|
||||
_lastConfirmedTargetTemperatureRaw,
|
||||
_heatingDiagnosticStartTemperature,
|
||||
StartHeatingCoil,
|
||||
HeatingCoil);
|
||||
}
|
||||
|
||||
private void EndHeatingDiagnostic(string reason)
|
||||
{
|
||||
if (!_heatingDiagnosticStartedAtUtc.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Information(
|
||||
"Cone radiation heating diagnostic ended. Reason={Reason}, TargetTemperature={TargetTemperature}, CurrentTemperature={CurrentTemperature}, HeatingActive={HeatingActive}.",
|
||||
reason,
|
||||
_lastConfirmedTargetTemperature,
|
||||
_lastStableCurrentTemperature,
|
||||
HeatingActive);
|
||||
_heatingDiagnosticStartedAtUtc = null;
|
||||
_heatingDiagnosticStartTemperature = null;
|
||||
_lastHeatingDiagnosticLoggedAtUtc = DateTime.MinValue;
|
||||
}
|
||||
|
||||
private void LogHeatingDiagnosticIfNeeded(bool heatingActive)
|
||||
{
|
||||
if (!_heatingDiagnosticStartedAtUtc.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var elapsed = now - _heatingDiagnosticStartedAtUtc.Value;
|
||||
if (elapsed > TimeSpan.FromMinutes(HeatingDiagnosticMaximumDurationMinutes))
|
||||
{
|
||||
EndHeatingDiagnostic("diagnostic time window expired");
|
||||
return;
|
||||
}
|
||||
|
||||
if (now - _lastHeatingDiagnosticLoggedAtUtc < TimeSpan.FromSeconds(HeatingDiagnosticLogIntervalSeconds))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastHeatingDiagnosticLoggedAtUtc = now;
|
||||
|
||||
var targetReadSucceeded = _tcpDeviceConnectionService.TryReadInt16(TargetTemperatureRegister, out var rawTargetReadback);
|
||||
double? targetReadback = targetReadSucceeded ? rawTargetReadback / TargetTemperatureScale : null;
|
||||
var currentTemperature = _lastStableCurrentTemperature;
|
||||
var targetTemperature = _lastConfirmedTargetTemperature;
|
||||
var deviation = currentTemperature.HasValue && targetTemperature.HasValue
|
||||
? currentTemperature.Value - targetTemperature.Value
|
||||
: (double?)null;
|
||||
var targetMismatch = targetReadSucceeded
|
||||
&& targetTemperature.HasValue
|
||||
&& Math.Abs(targetReadback!.Value - targetTemperature.Value) > TargetTemperatureMismatchTolerance;
|
||||
var hasNotRisen = elapsed >= TimeSpan.FromSeconds(HeatingNoRiseWarningDelaySeconds)
|
||||
&& targetTemperature.HasValue
|
||||
&& _heatingDiagnosticStartTemperature.HasValue
|
||||
&& currentTemperature.HasValue
|
||||
&& targetTemperature.Value > _heatingDiagnosticStartTemperature.Value + HeatingNoRiseTolerance
|
||||
&& currentTemperature.Value < _heatingDiagnosticStartTemperature.Value + HeatingNoRiseTolerance;
|
||||
var reachedTarget = currentTemperature.HasValue
|
||||
&& targetTemperature.HasValue
|
||||
&& Math.Abs(currentTemperature.Value - targetTemperature.Value) <= HeatingAtTargetTolerance;
|
||||
|
||||
if (!targetReadSucceeded)
|
||||
{
|
||||
Log.Warning(
|
||||
"Cone radiation heating diagnostic: target readback failed. ElapsedSeconds={ElapsedSeconds}, TargetTemperature={TargetTemperature}, CurrentTemperature={CurrentTemperature}, HeatingActive={HeatingActive}.",
|
||||
elapsed.TotalSeconds,
|
||||
targetTemperature,
|
||||
currentTemperature,
|
||||
heatingActive);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!heatingActive || targetMismatch || hasNotRisen)
|
||||
{
|
||||
Log.Warning(
|
||||
"Cone radiation heating diagnostic warning. ElapsedSeconds={ElapsedSeconds}, TargetTemperature={TargetTemperature}, RawTargetReadback={RawTargetReadback}, TargetReadback={TargetReadback}, CurrentTemperature={CurrentTemperature}, Deviation={Deviation}, HeatingActive={HeatingActive}, TargetMismatch={TargetMismatch}, HasNotRisen={HasNotRisen}.",
|
||||
elapsed.TotalSeconds,
|
||||
targetTemperature,
|
||||
rawTargetReadback,
|
||||
targetReadback,
|
||||
currentTemperature,
|
||||
deviation,
|
||||
heatingActive,
|
||||
targetMismatch,
|
||||
hasNotRisen);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information(
|
||||
"Cone radiation heating diagnostic sample. ElapsedSeconds={ElapsedSeconds}, TargetTemperature={TargetTemperature}, RawTargetReadback={RawTargetReadback}, TargetReadback={TargetReadback}, CurrentTemperature={CurrentTemperature}, Deviation={Deviation}, HeatingActive={HeatingActive}.",
|
||||
elapsed.TotalSeconds,
|
||||
targetTemperature,
|
||||
rawTargetReadback,
|
||||
targetReadback,
|
||||
currentTemperature,
|
||||
deviation,
|
||||
heatingActive);
|
||||
}
|
||||
|
||||
if (reachedTarget)
|
||||
{
|
||||
EndHeatingDiagnostic("target temperature reached");
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveParameters()
|
||||
{
|
||||
if (!float.TryParse(HeatTransferText, NumberStyles.Float, CultureInfo.InvariantCulture, out var heatTransfer))
|
||||
@@ -593,29 +772,40 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
Debug.WriteLine("Cone radiation circulating water write failed.");
|
||||
}
|
||||
|
||||
private void WriteActionCoil(string action)
|
||||
private bool WriteActionCoil(string action)
|
||||
{
|
||||
if (!TryGetActionCoil(action, out var coilAddress))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
|
||||
{
|
||||
LastAction = $"{action}失败";
|
||||
Debug.WriteLine($"Cone radiation action '{action}' write failed.");
|
||||
Log.Warning(
|
||||
"Cone radiation action coil write failed. Action={Action}, Coil=M{CoilAddress}.",
|
||||
action,
|
||||
coilAddress);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.Information(
|
||||
"Cone radiation action coil write succeeded. Action={Action}, Coil=M{CoilAddress}.",
|
||||
action,
|
||||
coilAddress);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetActionCoil(string action, out ushort coilAddress)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "开始升温":
|
||||
coilAddress = 100;
|
||||
case StartHeatingAction:
|
||||
coilAddress = StartHeatingCoil;
|
||||
return true;
|
||||
case "停止升温":
|
||||
coilAddress = 101;
|
||||
case StopHeatingAction:
|
||||
coilAddress = StopHeatingCoil;
|
||||
return true;
|
||||
default:
|
||||
coilAddress = 0;
|
||||
|
||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ConeCalorimeter.Services;
|
||||
using Microsoft.Win32;
|
||||
using Serilog;
|
||||
|
||||
namespace ConeCalorimeter.ViewModels;
|
||||
|
||||
@@ -88,6 +89,7 @@ public sealed class RealtimeDataViewModel : PageViewModel
|
||||
if (records.Count == 0)
|
||||
{
|
||||
StatusText = "没有可导出的数据";
|
||||
Log.Warning("Realtime data export blocked because there are no records.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,10 +113,12 @@ public sealed class RealtimeDataViewModel : PageViewModel
|
||||
{
|
||||
_realtimeDataExportService.Export(dialog.FileName, records);
|
||||
StatusText = $"已导出:{dialog.FileName}";
|
||||
Log.Information("Realtime data export succeeded. Path={Path}, Records={RecordCount}.", dialog.FileName, records.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"导出失败:{ex.Message}";
|
||||
Log.Error(ex, "Realtime data export failed. Path={Path}, Records={RecordCount}.", dialog.FileName, records.Count);
|
||||
MessageBox.Show(StatusText, "导出实时数据", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using ConeCalorimeter.Models;
|
||||
using ConeCalorimeter.Services;
|
||||
using Microsoft.Win32;
|
||||
using Serilog;
|
||||
|
||||
namespace ConeCalorimeter.ViewModels;
|
||||
|
||||
@@ -155,6 +156,7 @@ public sealed class ReportPageViewModel : PageViewModel
|
||||
if (!records.Any(IsValidExperimentRecord))
|
||||
{
|
||||
StatusText = "请先点击“测试开始”,产生有效实验数据后再导出报表";
|
||||
Log.Warning("Report export blocked because no valid experiment records were available. Records={RecordCount}.", records.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,10 +182,16 @@ public sealed class ReportPageViewModel : PageViewModel
|
||||
{
|
||||
_reportExportService.Export(dialog.FileName, input, records);
|
||||
StatusText = $"已导出:{dialog.FileName}";
|
||||
Log.Information(
|
||||
"Report export succeeded. Path={Path}, Records={RecordCount}, SampleName={SampleName}.",
|
||||
dialog.FileName,
|
||||
records.Count,
|
||||
input.SampleName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"导出失败:{ex.Message}";
|
||||
Log.Error(ex, "Report export failed. Path={Path}, Records={RecordCount}.", dialog.FileName, records.Count);
|
||||
MessageBox.Show(StatusText, "导出报表", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user