更新20260527

This commit is contained in:
GukSang.Jin
2026-05-27 12:53:06 +08:00
parent 1d087c00e8
commit 57ce28a1ea
10 changed files with 446 additions and 22 deletions

View File

@@ -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.");
}
}
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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
{

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);
}
}