diff --git a/ConeCalorimeter/App.xaml.cs b/ConeCalorimeter/App.xaml.cs
index 00acf76..507d5ae 100644
--- a/ConeCalorimeter/App.xaml.cs
+++ b/ConeCalorimeter/App.xaml.cs
@@ -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
///
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.");
+ }
+ }
}
diff --git a/ConeCalorimeter/ConeCalorimeter.csproj b/ConeCalorimeter/ConeCalorimeter.csproj
index e576896..9344091 100644
--- a/ConeCalorimeter/ConeCalorimeter.csproj
+++ b/ConeCalorimeter/ConeCalorimeter.csproj
@@ -13,6 +13,7 @@
+
diff --git a/ConeCalorimeter/MainWindow.xaml.cs b/ConeCalorimeter/MainWindow.xaml.cs
index f57158f..c6ae0d4 100644
--- a/ConeCalorimeter/MainWindow.xaml.cs
+++ b/ConeCalorimeter/MainWindow.xaml.cs
@@ -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);
diff --git a/ConeCalorimeter/Services/ExperimentDataService.cs b/ConeCalorimeter/Services/ExperimentDataService.cs
index ea471fc..7329e28 100644
--- a/ConeCalorimeter/Services/ExperimentDataService.cs
+++ b/ConeCalorimeter/Services/ExperimentDataService.cs
@@ -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
{
diff --git a/ConeCalorimeter/Services/ModbusRtuScaleService.cs b/ConeCalorimeter/Services/ModbusRtuScaleService.cs
index 0ff97e5..5e09758 100644
--- a/ConeCalorimeter/Services/ModbusRtuScaleService.cs
+++ b/ConeCalorimeter/Services/ModbusRtuScaleService.cs
@@ -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;
}
diff --git a/ConeCalorimeter/Services/TcpDeviceConnectionService.cs b/ConeCalorimeter/Services/TcpDeviceConnectionService.cs
index d146486..7115f65 100644
--- a/ConeCalorimeter/Services/TcpDeviceConnectionService.cs
+++ b/ConeCalorimeter/Services/TcpDeviceConnectionService.cs
@@ -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);
}
}
diff --git a/ConeCalorimeter/ViewModels/CValueCalibrationViewModel.cs b/ConeCalorimeter/ViewModels/CValueCalibrationViewModel.cs
index 1b16d35..951b053 100644
--- a/ConeCalorimeter/ViewModels/CValueCalibrationViewModel.cs
+++ b/ConeCalorimeter/ViewModels/CValueCalibrationViewModel.cs
@@ -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;
}
}
diff --git a/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs b/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs
index eaed351..369ce8a 100644
--- a/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs
+++ b/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs
@@ -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;
diff --git a/ConeCalorimeter/ViewModels/RealtimeDataViewModel.cs b/ConeCalorimeter/ViewModels/RealtimeDataViewModel.cs
index ec28c07..4ee0263 100644
--- a/ConeCalorimeter/ViewModels/RealtimeDataViewModel.cs
+++ b/ConeCalorimeter/ViewModels/RealtimeDataViewModel.cs
@@ -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);
}
}
diff --git a/ConeCalorimeter/ViewModels/ReportPageViewModel.cs b/ConeCalorimeter/ViewModels/ReportPageViewModel.cs
index 2b539e9..f77d865 100644
--- a/ConeCalorimeter/ViewModels/ReportPageViewModel.cs
+++ b/ConeCalorimeter/ViewModels/ReportPageViewModel.cs
@@ -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);
}
}