Files
tbf/GenesisCordonelInterface/API/InterfaceGCIToLaatzen.cs
T

2762 lines
101 KiB
C#

using CordonelPreadjustmentUi;
using CordonelPreadjustmentUi.Processes;
using CordonelPreadjustmentUi.Processes.Actions;
using CordonelPreadjustmentUi.Processes.Itinerary;
using GenesisCordonelInterface.Core.Threading;
using GenesisCordonelInterface.UI.LaatzenAPI_CordonelPreadjustmentUI;
using NLog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Configuration;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Linq;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore.EventArguments;
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore;
using Xylem.Common.Hardware.WaterMeter.Genesis.Registers;
using Xylem.Common.Hardware.WaterMeter.WaterMeterCore;
using Xylem.Common.Hardware.WaterMeter.WaterMeterCore.Consts;
using Xylem.Common.Hardware.WaterMeter.WaterMeterRegisters;
using Xylem.Common.Ui.CordonelPreadjustmentUi;
using static GenesisCordonelInterface.API.PublicModels;
using static Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.GenesisMeter;
using static Xylem.Common.Hardware.WaterMeter.Genesis.Registers.Register;
namespace GenesisCordonelInterface.API
{
public class InterfaceGCIToLaatzen
{
#region Fields
//private static readonly Lazy<ILogger> Logger = new Lazy<ILogger>(() => LogManager.GetLogger("GCI"));
private static readonly NLog.ILogger Logger = NLog.LogManager.GetLogger("GenesisCordonelInterface");
//GenesisToolBox
private readonly MeterBatch _meterBatch = new MeterBatch();
//Preadjustment
public ProcessProgress _progressProcess = new ProcessProgress();
public List<MeterStateControl> _meterControls = new List<MeterStateControl>();
public List<MeterStateControl> _tempMeterControls = new List<MeterStateControl>();
// Protects all access to _meterBatch.ListOfMeters
private readonly object _meterBatchLock = new object();
private static readonly object _setupGenesisMeterLock = new object();
private readonly ConcurrentDictionary<int, ApiWorker> _workers = new ConcurrentDictionary<int, ApiWorker>();
private readonly ConcurrentDictionary<int, bool> _selectedSlots = new ConcurrentDictionary<int, bool>();
private readonly ConcurrentDictionary<int, IDisposable> _streamingLeases = new ConcurrentDictionary<int, IDisposable>();
public InterfaceGCIToLaatzen()
{
}
#endregion
#region ================================== Worker ==================================
private ApiWorker GetWorker(int slot)
{
if (slot <= 0)
throw new ArgumentOutOfRangeException(nameof(slot));
return _workers.GetOrAdd(slot, s => new ApiWorker($"GCI Worker Slot {s}"));
}
#endregion
#region ================================== Worker Debug ==================================
public List<PublicModels.WorkerDebugStatus> GetWorkerDebugStatuses()
{
return _workers
.OrderBy(x => x.Key)
.Select(x => new PublicModels.WorkerDebugStatus
{
Slot = x.Key,
Name = x.Value.Name,
QueueLength = /*x.Value.IsDisposed ? 0 : */x.Value.QueueLength,
IsBusy = !x.Value.IsDisposed && x.Value.IsBusy,
CurrentOperation = x.Value.IsDisposed ? null : x.Value.CurrentOperation,
LastError = x.Value.LastError,
LastActivity = x.Value.LastActivity
})
.ToList();
}
#endregion
#region ================================== MeterBatch Debug ==================================
/// <summary>
///
/// </summary>
/// <returns></returns>
public List<MeterBatchDebugStatus> GetMeterBatchDebugStatuses()
{
List<ZeroFlowGenesisMeter> meters;
//just snapshot of list under lock
lock (_meterBatchLock)
{
meters = _meterBatch.ListOfMeters
.OfType<ZeroFlowGenesisMeter>()
.ToList();
}
return meters
.Select(m => new MeterBatchDebugStatus
{
Slot = m.Slot,
Selected = IsSlotSelected(m.Slot),
PcbId = m.PcbId,
IsConnected = m.IsConnected,
IsLoggedOn = m.IsLoggedOn,
RequestPort = m.requestPortConfig.HasValue
? m.requestPortConfig.Value.PortName
: "",
StreamingPort = m.streamingPortConfig.HasValue
? m.streamingPortConfig.Value.PortName
: "",
RequestPortType = m.requestPortConfig.HasValue
? m.requestPortConfig.Value.Type
: "",
FwVersion = m.FwVersion,
InterfaceVersion = m.InterfaceInfo?.InterfaceVersion
})
.OrderBy(x => x.Slot)
.ToList();
}
public void SetSlotSelected(int slot, bool selected)
{
if (slot <= 0)
throw new ArgumentOutOfRangeException(nameof(slot));
Logger.Debug(
"[{0}] SetSlotSelected: slot={1}, selected={2}",
InterfaceName,
slot,
selected);
_selectedSlots[slot] = selected;
}
public bool IsSlotSelected(int slot)
{
return _selectedSlots.TryGetValue(slot, out bool selected) && selected;
}
public List<int> GetSelectedSlots()
{
return _selectedSlots
.Where(x => x.Value)
.Select(x => x.Key)
.OrderBy(x => x)
.ToList();
}
#endregion
#region ================================== Helpers ==================================
private GenesisMeter GetMeterThreadSafe(int slot)
{
lock (_meterBatchLock)
{
return _meterBatch.ListOfMeters
.OfType<ZeroFlowGenesisMeter>()
.FirstOrDefault(m => m.Slot == slot);
}
}
private ZeroFlowGenesisMeter GetMeterThreadSafe2(int slot)
{
lock (_meterBatchLock)
{
return _meterBatch.ListOfMeters
.OfType<ZeroFlowGenesisMeter>()
.FirstOrDefault(m => m.Slot == slot);
}
}
private bool RemoveMeterThreadSafe(GenesisMeter meter)
{
if (meter == null)
return false;
lock (_meterBatchLock)
{
return _meterBatch.ListOfMeters.Remove(meter);
}
}
private void RemoveMetersThreadSafe()
{
lock (_meterBatchLock)
{
_meterBatch.ListOfMeters.Clear();
}
}
private void RemoveWorkersThreadSafe()
{
foreach (var pair in _workers.ToList())
{
ApiWorker worker;
if (_workers.TryRemove(pair.Key, out worker))
{
worker.Dispose();
}
}
}
private void DisposeMetersThreadSafe()
{
lock (_meterBatchLock)
{
foreach (var meter in _meterBatch.ListOfMeters.OfType<ZeroFlowGenesisMeter>())
{
meter.DisposeMeter();
}
}
}
private void EnsureConnected(GenesisMeter meter)
{
if (!meter.IsLoggedOn)
throw new InvalidOperationException("Meter is not connected.");
}
private string ToHex(byte[] data)
{
return data == null ? "" : BitConverter.ToString(data).Replace("-", " ");
}
private const string InterfaceName = "InterfaceGCIToLaatzen";
private void LogInfo(string operation, string message)
{
Logger.Info("[{0}] {1}: {2}", InterfaceName, operation, message);
}
private void LogError(string operation, Exception ex)
{
Logger.Error(ex, "[{0}] {1} failed: {2}", InterfaceName, operation, ex.Message);
}
private string SafePort(string port)
{
return string.IsNullOrWhiteSpace(port) ? "<empty>" : port;
}
public ConcurrentDictionary<RegisterDefinition, Byte[]> GetRegistersDicForSlot(int slot)
{
var meter = GetMeterThreadSafe(slot);
if (meter == null)
throw new Exception($"Slot {slot} not initialized.");
return meter.GetRegistersDic();
}
#endregion
#region ================================== INIT/UPDATE ==================================
public Task<PublicModels.GciInitSlotResult> InitSlotAsync(
int slot,
ConfigSource cfg,
PasswordSource pwd,
PortConfig? req,
PortConfig? str,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => InitSlot(slot, cfg, pwd, req, str), token, nameof(InitSlot));
}
public PublicModels.GciInitSlotResult InitSlot(
int slot,
ConfigSource cfg,
PasswordSource pwd,
PortConfig? req,
PortConfig? str)
{
const string operation = nameof(InitSlot);
try
{
LogInfo(operation,
string.Format(
"Start. Slot={0}, ConfigSource={1}, PasswordSource={2}, RequestPort={3}, StreamingPort={4}",
slot,
cfg,
pwd,
req.HasValue ? req.Value.PortName : "NA",
str.HasValue ? str.Value.PortName : "NA"));
var existingMeter = GetMeterThreadSafe(slot);
if (existingMeter != null)
{
UpdateMeterForSlot(existingMeter, slot, cfg, pwd, req, str);
return SlotAlreadyExists(slot, existingMeter);
}
var meter = CreateMeterForSlot(slot, cfg, pwd, req, str);
//AddMeter(slot, meter);
return SlotCreated(slot, meter);
}
catch (Exception ex)
{
LogError(operation, ex);
return SlotFailed(slot, ex.Message);
}
}
public Task<PublicModels.GciInitSlotResult> UpdateSlotAsync(
int slot,
ConfigSource cfg,
PasswordSource pwd,
PortConfig? req,
PortConfig? str,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => UpdateSlot(slot, cfg, pwd, req, str), token, nameof(UpdateSlot));
}
public PublicModels.GciInitSlotResult UpdateSlot(
int slot,
ConfigSource cfg,
PasswordSource pwd,
PortConfig? req,
PortConfig? str)
{
const string operation = nameof(UpdateSlot);
try
{
LogInfo(operation,
string.Format(
"Start. Slot={0}, ConfigSource={1}, PasswordSource={2}, RequestPort={3}, StreamingPort={4}",
slot,
cfg,
pwd,
req.HasValue ? req.Value.PortName : "NA",
str.HasValue ? str.Value.PortName : "NA"));
var meter = GetMeterThreadSafe(slot);
if (meter == null)
{
return SlotFailed(slot, "Meter does not exist for this slot.");
}
if (meter.IsConnected)
{
return SlotFailed(slot, "Slot update is not allowed while meter is connected. Disconnect the meter first.");
}
UpdateMeterForSlot(meter, slot, cfg, pwd, req, str);
return SlotUpdated(slot, meter);
}
catch (Exception ex)
{
LogError(operation, ex);
return SlotFailed(slot, ex.Message);
}
}
private GenesisMeter CreateMeterForSlot(
int slot,
ConfigSource cfg,
PasswordSource pwd,
PortConfig? req,
PortConfig? str)
{
ZeroFlowGenesisMeter meter = new ZeroFlowGenesisMeter(0, 3, !(_meterControls is TempMeterStateControl), useForPreadjustmentUI: false);
meter.Slot = slot;
meter.useConfigSource = cfg;
meter.usePasswordSource = pwd;
meter.requestPortConfig = req;
meter.streamingPortConfig = str;
meter.onlineOperation = false;
lock (_meterBatchLock)
{
_meterBatch.AddMeter2(meter);
}
return meter;
}
private void UpdateMeterForSlot(
GenesisMeter meter,
int slot,
ConfigSource cfg,
PasswordSource pwd,
PortConfig? req,
PortConfig? str)
{
if (meter == null)
throw new ArgumentNullException(nameof(meter));
meter.Slot = slot;
meter.useConfigSource = cfg;
meter.usePasswordSource = pwd;
meter.requestPortConfig = req;
meter.streamingPortConfig = str;
//meter.SetupGenesisMeter(slot, req, str, true);
}
private PublicModels.GciInitSlotResult SlotCreated(int slot, GenesisMeter meter)
{
return new PublicModels.GciInitSlotResult
{
SlotId = slot,
Success = true,
Message = "Meter initialized.",
PcbId = meter != null ? meter.SerialNumber : null,
Created = true,
Updated = false,
AlreadyExists = false
};
}
private PublicModels.GciInitSlotResult SlotUpdated(int slot, GenesisMeter meter)
{
return new PublicModels.GciInitSlotResult
{
SlotId = slot,
Success = true,
Message = "Meter updated.",
PcbId = meter != null ? meter.SerialNumber : null,
Created = false,
Updated = true,
AlreadyExists = false
};
}
private PublicModels.GciInitSlotResult SlotAlreadyExists(int slot, GenesisMeter meter)
{
return new PublicModels.GciInitSlotResult
{
SlotId = slot,
Success = true,
Message = "Meter already exists. Meter config was only updated",
PcbId = meter != null ? meter.SerialNumber : null,
Created = false,
Updated = false,
AlreadyExists = true
};
}
private PublicModels.GciInitSlotResult SlotFailed(int slot, string message)
{
return new PublicModels.GciInitSlotResult
{
SlotId = slot,
Success = false,
Message = message,
PcbId = null,
Created = false,
Updated = false,
AlreadyExists = false
};
}
#endregion
#region ================================== Slot INFO ==================================
public Task<PublicModels.GciSlotInfo> GetOneMeterInfo(
int slot,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => GetOneMeterInfo(slot), token, nameof(GetOneMeterInfo));
}
public PublicModels.GciSlotInfo GetOneMeterInfo(int slot)
{
const string operation = nameof(GetOneMeterInfo);
try
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeterThreadSafe(slot);
if (meter == null)
{
return new PublicModels.GciSlotInfo
{
SlotId = slot,
Success = true,
Exists = false,
IsConnected = false,
IsLoggedOn = false,
Message = "Slot is empty."
};
}
return new PublicModels.GciSlotInfo
{
SlotId = slot,
Success = true,
Exists = true,
IsConnected = meter.IsConnected,
IsLoggedOn = meter.IsLoggedOn,
Message = "Slot found.",
PcbId = meter.PcbId,
Password = meter.Password,
ConfigSource = ModelsMapping.MapConfigSourceBack(meter.useConfigSource),
PasswordSource = ModelsMapping.MapPasswordSourceBack(meter.usePasswordSource),
RequestPort = ModelsMapping.MapPortBack(meter.requestPortConfig),
StreamingPort = ModelsMapping.MapPortBack(meter.streamingPortConfig)
};
}
catch (Exception ex)
{
LogError(operation, ex);
return new PublicModels.GciSlotInfo
{
SlotId = slot,
Success = false,
Exists = false,
IsConnected = false,
IsLoggedOn = false,
Message = ex.Message
};
}
}
public Task<PublicModels.GciAllSlotsInfo> GetAllMetersInfo(
CancellationToken token = default)
{
return Task.Run(() => GetAllMetersInfo(), token);
}
public PublicModels.GciAllSlotsInfo GetAllMetersInfo()
{
const string operation = nameof(GetAllMetersInfo);
try
{
LogInfo(operation, "Start.");
lock (_meterBatchLock)
{
var result = new PublicModels.GciAllSlotsInfo
{
Success = true,
Message = "Slots read.",
Slots = _meterBatch.ListOfMeters
.OfType<GenesisMeter>()
.Select(meter => new PublicModels.GciSlotInfo
{
SlotId = meter.Slot,
Success = true,
Exists = true,
Message = "Slot found.",
PcbId = meter.PcbId,
ConfigSource = ModelsMapping.MapConfigSourceBack(meter.useConfigSource),
PasswordSource = ModelsMapping.MapPasswordSourceBack(meter.usePasswordSource),
RequestPort = ModelsMapping.MapPortBack(meter.requestPortConfig),
StreamingPort = ModelsMapping.MapPortBack(meter.streamingPortConfig)
})
.ToList()
};
return result;
}
}
catch (Exception ex)
{
LogError(operation, ex);
return new PublicModels.GciAllSlotsInfo
{
Success = false,
Message = ex.Message,
Slots = new List<PublicModels.GciSlotInfo>()
};
}
}
public Task<PublicModels.GciCleanSlotResult> CleanSlotAsync(
int slot,
CancellationToken token = default)
{
return Task.Run(() => CleanSlot(slot), token);
}
#endregion
#region ================================== CLEAN ==================================
public PublicModels.GciCleanSlotResult CleanSlot(int slot)
{
const string operation = nameof(CleanSlot);
try
{
LogInfo(operation, string.Format("Start. Slot={0}", slot));
var meter = GetMeterThreadSafe(slot);
if (meter != null)
{
meter.DisposeMeter();
RemoveMeterThreadSafe(meter);
}
ApiWorker worker;
if (_workers.TryRemove(slot, out worker))
{
worker.Dispose();
}
return new PublicModels.GciCleanSlotResult
{
SlotId = slot,
Success = true,
Message = string.Format("Slot {0} cleaned.", slot)
};
}
catch (Exception ex)
{
LogError(operation, ex);
return new PublicModels.GciCleanSlotResult
{
SlotId = slot,
Success = false,
Message = ex.Message
};
}
}
public Task<PublicModels.GciCleanAllSlotsResult> CleanAllSlotsAsync(
CancellationToken token = default)
{
return Task.Run(() => CleanAllSlots(), token);
}
public PublicModels.GciCleanAllSlotsResult CleanAllSlots()
{
const string operation = nameof(CleanAllSlots);
try
{
LogInfo(operation, "Start.");
DisposeMetersThreadSafe();
RemoveMetersThreadSafe();
RemoveWorkersThreadSafe();
return new PublicModels.GciCleanAllSlotsResult
{
Success = true,
Message = "Slots cleaned."
};
}
catch (Exception ex)
{
LogError(operation, ex);
return new PublicModels.GciCleanAllSlotsResult
{
Success = false,
Message = ex.Message
};
}
}
#endregion
#region ================================== PORT DETECTION ==================================
public Task<PortDetectionResult> DetectStreamingPortAsync(
int slot,
CancellationToken token = default(CancellationToken))
{
return GetWorker(slot).RunAsync(() => DetectStreamingPort(slot), token, nameof(DetectStreamingPort));
}
public PortDetectionResult DetectStreamingPort(int slot)
{
const string operation = nameof(DetectStreamingPort);
if (slot <= 0)
throw new ArgumentOutOfRangeException(nameof(slot), "Slot number must be greater than zero.");
try
{
LogInfo(operation, $"Start. Slot={slot}");
using (var mb = new MeterBatch())
using (var meter = new GenesisMeter())
{
mb.AddMeter(meter);
var rawData = new ConcurrentBag<string>();
meter.StreamingPort.OnRawRecordReceived += (o, rawMsg) =>
{
var data = (string)rawMsg.GetData();
rawData.Add(data);
};
Thread.Sleep(500);
bool success = rawData.Any();
string portName = meter.StreamingPort.GetPortName();
LogInfo(operation,
$"Finish. Slot={slot}, Success={success}, Port={SafePort(portName)}, RawRecords={rawData.Count}");
return new PortDetectionResult
{
Success = success,
Slot = slot,
PortName = portName,
ErrorMessage = success ? null : "No streaming data received."
};
}
}
catch (Exception ex)
{
LogError(operation, ex);
throw;
}
}
public Task<PortDetectionResult> DetectRequestPortAsync(
int slot,
CancellationToken token = default(CancellationToken))
{
return GetWorker(slot).RunAsync(() => DetectRequestPort(slot), token, nameof(DetectRequestPort));
}
public PortDetectionResult DetectRequestPort(int slot)
{
const string operation = nameof(DetectRequestPort);
if (slot <= 0)
throw new ArgumentOutOfRangeException(nameof(slot), "Slot number must be greater than zero.");
try
{
LogInfo(operation, $"Start. Slot={slot}");
using (var mb = new MeterBatch())
using (var meter = new GenesisMeter())
{
mb.AddMeter(meter);
meter.Logout();
string pcbId = meter.GetPcbId();
bool success = !string.IsNullOrEmpty(pcbId);
string portName = meter.RequestPort.GetPortName();
LogInfo(operation,
$"Finish. Slot={slot}, Success={success}, Port={SafePort(portName)}, PcbId={pcbId ?? "<empty>"}");
return new PortDetectionResult
{
Success = success,
Slot = slot,
PortName = portName,
PcbId = pcbId,
ErrorMessage = success ? null : "PCB ID was empty."
};
}
}
catch (Exception ex)
{
LogError(operation, ex);
throw;
}
}
#endregion
#region ================================== LOGIN ==================================
public Task<PublicModels.GciLoginResult> LoginOneSlotAsync(int slot, CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => LoginOneSlot(slot), token, nameof(LoginOneSlot));
}
public PublicModels.GciLoginResult LoginOneSlot(int slot)
{
const string operation = nameof(LoginOneSlot);
try
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeterThreadSafe(slot);
if (!meter.IsLoggedOn)
{
meter.Login();
if (meter.IsLoggedOn)
{
LogInfo(operation, $"New Slot {slot} login success.");
return new PublicModels.GciLoginResult
{
Success = true,
SlotId = slot,
IsLoggedOn = true,
Message = "Logged on now"
};
}
LogInfo(operation, $"Failed Slot {slot} login.");
return new PublicModels.GciLoginResult
{
Success = false,
SlotId = slot,
IsLoggedOn = false,
Message = string.IsNullOrWhiteSpace(meter.LastLoginError)
? "Login failed"
: meter.LastLoginError
};
}
var result = new PublicModels.GciLoginResult
{
Success = true,
SlotId = slot,
IsLoggedOn = true,
Message = "Already logged on"
};
LogInfo(operation, $"Slot={slot} already logged on.");
return result;
}
catch (Exception ex)
{
LogError(operation, ex);
return new PublicModels.GciLoginResult
{
Success = false,
SlotId = slot,
IsLoggedOn = false,
Message = ex.Message
};
}
}
#endregion
#region ================================== CONNECT ==================================
/// <summary>
/// Borrows the streaming port for one externally controlled preadjustment phase.
/// The caller must stop its reader first and dispose the lease after all phase tasks finish.
/// </summary>
public Task<IDisposable> AcquirePreadjustmentStreamingPortAsync(
int slot, PortConfig streamingPort, CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() =>
{
token.ThrowIfCancellationRequested();
var meter = GetMeterThreadSafe(slot) as ZeroFlowGenesisMeter;
if (meter == null || !meter.requestPortConfig.HasValue)
throw new InvalidOperationException($"Slot {slot} must be initialized before borrowing its streaming port.");
if (string.IsNullOrWhiteSpace(streamingPort.PortName) ||
string.Equals(streamingPort.PortName, "NA", StringComparison.OrdinalIgnoreCase) ||
string.Equals(streamingPort.PortName, meter.requestPortConfig.Value.PortName, StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("A separate, valid streaming COM port is required.", nameof(streamingPort));
var lease = new StreamingPortLease(() =>
GetWorker(slot).RunAsync(() =>
{
ReleasePreadjustmentStreamingPort(slot, meter);
return true;
}, CancellationToken.None, "ReleasePreadjustmentStreamingPort").GetAwaiter().GetResult());
if (!_streamingLeases.TryAdd(slot, lease))
throw new InvalidOperationException($"Slot {slot} already has an active streaming-port lease.");
try
{
// Borrow only streaming resources. Keep the existing request protocol,
// authentication and process settings throughout the handoff.
meter.ConnectStreamingPort(streamingPort);
if (meter.StreamingPort == null || !meter.StreamingPort.IsOpen())
throw new InvalidOperationException($"GCI could not open {streamingPort.PortName} for slot {slot}.");
token.ThrowIfCancellationRequested();
// Preparation originally installed this handler for the entire sequence.
// Each newly created protocol needs its own subscription and decoder mode.
meter.StartRecordData();
LogInfo("AcquirePreadjustmentStreamingPort", $"Slot={slot}, Port={streamingPort.PortName}, Owner=GCI, LoggedOn={meter.IsLoggedOn}, RecordingStarted=True");
return (IDisposable)lease;
}
catch (Exception acquisitionError)
{
// Cleanup is streaming-only, including cancellation during acquisition.
try
{
meter.DisconnectStreamingPort();
}
catch (Exception restoreError)
{
throw new AggregateException("Streaming acquisition and streaming cleanup failed.", acquisitionError, restoreError);
}
finally
{
_streamingLeases.TryRemove(slot, out _);
}
throw;
}
}, token, nameof(AcquirePreadjustmentStreamingPortAsync));
}
private void ReleasePreadjustmentStreamingPort(int slot, ZeroFlowGenesisMeter meter)
{
string portName = meter.StreamingPort == null ? "NA" : meter.StreamingPort.GetPortName();
try
{
meter.StopRecordData();
}
catch (Exception ex)
{
// A failed decoder stop must never prevent physical port release.
LogError("StopRecordDataBeforeStreamingRelease", ex);
}
meter.DisconnectStreamingPort();
_streamingLeases.TryRemove(slot, out _);
LogInfo("ReleasePreadjustmentStreamingPort", $"Slot={slot}, Port={portName}, Owner=Reader, RequestConnected={meter.IsConnected}");
}
private sealed class StreamingPortLease : IDisposable
{
private readonly object gate = new object();
private Action release;
public StreamingPortLease(Action releaseAction) { release = releaseAction; }
public void Dispose()
{
lock (gate)
{
if (release == null) return;
release();
release = null;
}
}
}
public Task<PublicModels.GciConnectResult> ConnectOneSlotAsync(int slot, CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => ConnectOneSlot(slot), token, nameof(ConnectOneSlot));
}
public PublicModels.GciConnectResult ConnectOneSlot(int slot)
{
const string operation = nameof(ConnectOneSlot);
try
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeterThreadSafe(slot);
if (!meter.IsConnected)
{
meter.SetupGenesisMeter(slot, meter.requestPortConfig, meter.streamingPortConfig, true);
meter.ConnectMeter();
LogInfo(operation, $"New Slot {slot} connection success.");
return new PublicModels.GciConnectResult
{
Success = true,
SlotId = slot,
IsConnected = meter.IsConnected,
Message = meter.IsConnected
? "Connected now"
: meter.LastConnectStatus
};
}
var result = new PublicModels.GciConnectResult
{
Success = true,
SlotId = slot,
IsConnected = true,
Message = "Already connected"
};
LogInfo(operation, $"Slot={slot} already connected.");
return result;
}
catch (Exception ex)
{
LogError(operation, ex);
return new PublicModels.GciConnectResult
{
Success = false,
SlotId = slot,
IsConnected = false,
Message = ex.Message
};
}
}
private List<PublicModels.GciRegisterSnapshot> BuildRegisters(GenesisMeter meter)
{
var list = new List<GciRegisterSnapshot>();
foreach (var item in meter.GetRegistersDic())
{
var from = item.Key.RegisterDetail.Version.First?.ToString() ?? "-";
var to = item.Key.RegisterDetail.Version.Last?.ToString() ?? "-";
list.Add(new GciRegisterSnapshot
{
Name = item.Key.GetIdent(),
Type = item.Key.DataType.Name,
RawValue = BitConverter.ToString(item.Value).Replace("-", " "),
Min = item.Key.Minimum?.ToString(),
Max = item.Key.Maximum?.ToString(),
Description = item.Key.RegisterDetail.Description,
Version = $"from {from} to {to}",
IsAvailable = item.Key.IsAvailable.ToString(),
Privilege = item.Key.RegisterDetail.Privilege.Lvl8.ToString()
});
}
return list;
}
#endregion
#region ================================== DISCONNECT ==================================
public Task<PublicModels.GciDisconnectResult> DisconnectAsync(
int slot,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => Disconnect(slot), token, nameof(Disconnect));
}
public PublicModels.GciDisconnectResult Disconnect(int slot)
{
const string operation = nameof(Disconnect);
try
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeterThreadSafe(slot);
bool logoutSuccess = meter.Logout();
meter.Disconnect();
if (meter.IsLoggedOn)
{
meter.MarkLoggedOut();
meter.LastLogoutStatus = logoutSuccess
? "Successfully logged out"
: "Logout failed, but connection was closed";
}
bool success = !meter.IsConnected && !meter.IsLoggedOn;
if (success)
{
LogInfo(operation, $"Success. Slot={slot}");
}
else
{
LogInfo(
operation,
$"Disconnect incomplete. Slot={slot}, IsConnected={meter.IsConnected}, IsLoggedOn={meter.IsLoggedOn}");
}
//if (!meter.IsConnected && meter.IsLoggedOn)
// meter.ConnectMeter(); // repair of this situation
return new PublicModels.GciDisconnectResult
{
SlotId = slot,
Success = success,
Message = string.Join(
" | ",
new[]
{
meter.LastLogoutStatus,
meter.LastDisposeStatus,
success
? null
: $"Disconnect state invalid. IsConnected={meter.IsConnected}, IsLoggedOn={meter.IsLoggedOn}"
}
.Where(x => !string.IsNullOrWhiteSpace(x)))
};
}
catch (Exception ex)
{
LogError(operation, ex);
return new PublicModels.GciDisconnectResult
{
SlotId = slot,
Success = false,
Message = ex.Message
};
}
finally
{
if (_workers.TryRemove(slot, out var worker))
{
worker.Dispose();
LogInfo(operation, $"Worker disposed. Slot={slot}");
}
}
}
#endregion
#region ================================== PCB ==================================
public Task<PublicModels.GciGetPcbIdResult> GetPcbIdAsync(
int slot,
CancellationToken token = default(CancellationToken))
{
return GetWorker(slot).RunAsync(() => GetPcbId(slot), token, nameof(GetPcbId));
}
public PublicModels.GciGetPcbIdResult GetPcbId(int slot)
{
const string operation = nameof(GetPcbId);
try
{
LogInfo(operation, $"Start. Slot={slot}");
var meter = GetMeterThreadSafe(slot);
//meter.Logout();
string pcbId = meter.GetPcbId();
bool isValid =
!string.IsNullOrWhiteSpace(pcbId) &&
pcbId.Length == 9;
LogInfo(operation, $"Finish. Slot={slot}, PcbId={pcbId ?? "<empty>"}");
return new PublicModels.GciGetPcbIdResult
{
SlotId = slot,
Success = isValid,
PcbId = pcbId,
Message = isValid
? "PCB ID read successfully."
: "Invalid length"
};
}
catch (Exception ex)
{
LogError(operation, ex);
return new PublicModels.GciGetPcbIdResult
{
SlotId = slot,
Success = false,
PcbId = null,
Message = ex.Message
};
}
}
#endregion
#region ================================== REGISTER READ ==================================
public Task<RegisterReadResult> ReadRegisterAsync(int slot, string name, CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => ReadRegister(slot, name), token, nameof(ReadRegister));
}
public RegisterReadResult ReadRegister(int slot, string name)
{
const string operation = nameof(ReadRegister);
try
{
LogInfo(operation, $"Start. Slot={slot}, Register={name}");
var meter = GetMeterThreadSafe(slot);
EnsureConnected(meter);
var raw = meter.ReadRegister(name);
bool success = raw != null && raw.Length > 0;
var result = new RegisterReadResult
{
Success = success,
RegisterName = name,
RawHex = ToHex(raw)
};
LogInfo(operation, $"Success. Slot={slot}, Register={name}, RawHex={result.RawHex}");
return result;
}
catch (Exception ex)
{
LogError(operation, ex);
return new RegisterReadResult
{
Success = false,
RegisterName = name,
ErrorMessage = ex.Message
};
}
}
#endregion
#region ================================== REGISTER WRITE ==================================
public Task<RegisterWriteResult> WriteRegisterAsync(
int slot,
string registerName,
object value,
bool storeToDevice = false,
bool refreshSystemState = false,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => WriteRegister(slot, registerName, value, storeToDevice, refreshSystemState), token, nameof(WriteRegister));
}
public RegisterWriteResult WriteRegister(
int slot,
string registerName,
object value,
bool storeToDevice = false,
bool refreshSystemState = false)
{
const string operation = nameof(WriteRegister);
try
{
LogInfo(operation,
$"Start. Slot={slot}, Register={registerName}, Value={value}, " +
$"StoreToDevice={storeToDevice}, RefreshSystemState={refreshSystemState}");
var meter = GetMeterThreadSafe(slot);
EnsureConnected(meter);
if (string.IsNullOrWhiteSpace(registerName))
throw new ArgumentException("Register name cannot be empty.", nameof(registerName));
bool writeOk = meter.WriteRegister(registerName, value);
if (!writeOk)
{
LogInfo(operation, $"Failed. Slot={slot}, Register={registerName}, Reason=Write operation failed.");
return new RegisterWriteResult
{
Success = false,
RegisterName = registerName,
WrittenValue = value,
ErrorMessage = "Write operation failed."
};
}
if (storeToDevice)
{
if (!meter.StoreAllConfigurations())
{
LogInfo(operation, $"Failed. Slot={slot}, Register={registerName}, Reason=StoreAllConfigurations failed.");
return new RegisterWriteResult
{
Success = false,
RegisterName = registerName,
WrittenValue = value,
StoreToDevice = true,
RefreshSystemState = refreshSystemState,
ErrorMessage = "StoreAllConfigurations failed."
};
}
}
if (refreshSystemState)
{
if (!meter.WriteRegister("SENSUSRADIO_SYSTEMSTATE", 0xFF, true, false))
{
LogInfo(operation, $"Failed. Slot={slot}, Register={registerName}, Reason=System state refresh failed.");
return new RegisterWriteResult
{
Success = false,
RegisterName = registerName,
WrittenValue = value,
StoreToDevice = storeToDevice,
RefreshSystemState = true,
ErrorMessage = "System state refresh failed."
};
}
}
LogInfo(operation, $"Success. Slot={slot}, Register={registerName}");
return new RegisterWriteResult
{
Success = true,
RegisterName = registerName,
WrittenValue = value,
StoreToDevice = storeToDevice,
RefreshSystemState = refreshSystemState
};
}
catch (Exception ex)
{
LogError(operation, ex);
return new RegisterWriteResult
{
Success = false,
RegisterName = registerName,
WrittenValue = value,
StoreToDevice = storeToDevice,
RefreshSystemState = refreshSystemState,
ErrorMessage = ex.Message
};
}
}
#endregion
#region ================================== PASSWORD ==================================
public Task<PublicModels.GciSetPasswordResult> SetMeterPasswordAsync(
int slot,
string password,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(() => SetMeterPassword(slot, password),
token,
nameof(SetMeterPassword));
}
public PublicModels.GciSetPasswordResult SetMeterPassword(
int slot,
string password)
{
const string operation = nameof(SetMeterPassword);
try
{
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password cannot be empty.", nameof(password));
LogInfo(operation, $"Start. Slot={slot}");
GenesisMeter meter = GetMeterThreadSafe(slot);
meter?.SetPassword(password);//SetupFromExternConfig();
var result = new PublicModels.GciSetPasswordResult
{
SlotId = slot,
Success = true,
Password = meter.GetPassword_out(),
Message = "Password set successfully."
};
LogInfo(operation, $"Finish. Slot={slot}, Success={result.Success}");
return result;
}
catch (Exception ex)
{
LogError(operation, ex);
return new PublicModels.GciSetPasswordResult
{
SlotId = slot,
Success = false,
Message = ex.Message
};
}
}
#endregion
#region ================================== METER BATCH SETUP ==================================
public void ReloadSlotSetup()
{
const string operation = nameof(ReloadSlotSetup);
try
{
LogInfo(operation, "Start.");
_meterBatch.ListOfMeters.Clear();
_selectedSlots.Clear();
// TODO:
// Load meter batch setup from persistent storage.
// Example:
// _meterBatch.SetupFromConfigFile();
LogInfo(operation, "Success. Meter batch and selected slots cleared.");
}
catch (Exception ex)
{
LogError(operation, ex);
throw;
}
}
public void SaveSlotSetup(List<MeterBatchDebugStatus> data)
{
const string operation = nameof(SaveSlotSetup);
try
{
LogInfo(operation, $"Start. Rows={data?.Count ?? 0}");
if (data == null)
throw new ArgumentNullException(nameof(data));
_meterBatch.ListOfMeters.Clear();
_selectedSlots.Clear();
foreach (var item in data)
{
var meter = new GenesisMeter();
PortConfig? req = string.IsNullOrWhiteSpace(item.RequestPort)
? (PortConfig?)null
: new PortConfig { PortName = item.RequestPort, Type = "Serial" };
PortConfig? str = string.IsNullOrWhiteSpace(item.StreamingPort)
? (PortConfig?)null
: new PortConfig { PortName = item.StreamingPort, Type = "Serial" };
meter.useConfigSource = ConfigSource.InterfaceInputConfig;
meter.SetupFromExternConfig(item.Slot, req, str, true);
_meterBatch.AddMeter(meter);
_selectedSlots[item.Slot] = item.Selected;
LogInfo(
operation,
$"Saved row. Slot={item.Slot}, Selected={item.Selected}, " +
$"RequestPort={SafePort(item.RequestPort)}, StreamingPort={SafePort(item.StreamingPort)}");
}
LogInfo(operation, $"Success. MeterBatchCount={_meterBatch.ListOfMeters.Count}, SelectedSlots={_selectedSlots.Count}");
}
catch (Exception ex)
{
LogError(operation, ex);
throw;
}
}
private List<string> _cachedRegisters;
public List<string> GetAllRegisterNames()
{
if (_cachedRegisters != null)
return _cachedRegisters;
_cachedRegisters = typeof(Register)
.GetNestedTypes(BindingFlags.Public)
.SelectMany(t => t.GetFields(BindingFlags.Public | BindingFlags.Static))
.Where(f => f.FieldType == typeof(string))
.Select(f => f.GetValue(null)?.ToString())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.OrderBy(x => x)
.ToList();
return _cachedRegisters;
}
#endregion
// GCI to PreadjustmentUI - MANUAL - Laatzen GUI
#region ================================== PreAdjustmentUI form call ==================================
private FrmCordonelPreadjustmentUI _preadjustmentForm;
private readonly object _formLock = new object();
public event EventHandler PreAdjustmentFormClosedByUser;
/// <summary>
/// Shows singleton instance of preadjustment form.
///
/// Behavior:
///
/// ShowPreadjustmentForm()
/// ↓
/// create form instance if necessary
/// ↓
/// user works with form
/// ↓
/// user clicks X
/// ↓
/// FormClosing
/// ↓
/// Cancel closing
/// ↓
/// Hide()
/// ↓
/// PreadjustmentFormClosedByUser
/// ↓
/// external workflow continues
///
/// Notes:
/// - form instance is reused
/// - form is hidden instead of disposed
/// - event notification is non-blocking
/// - repeated calls bring existing form to front
/// - actual disposal happens only during application shutdown
///
/// Typical usage:
///
/// _bridge.PreadjustmentFormClosedByUser += (s,e)=>
/// {
/// ContinueWorkflow();
/// };
///
/// _bridge.ShowPreadjustmentForm(this);
///
/// </summary>
public void ShowPreAdjustmentForm(IWin32Window owner)
{
lock (_formLock)
{
// Create form only if it does not exist
// or has already been disposed
if (_preadjustmentForm == null || _preadjustmentForm.IsDisposed)
{
_preadjustmentForm = new FrmCordonelPreadjustmentUI(_meterBatch);
_preadjustmentForm.FormClosing += (s, e) =>
{
// Hide the form instead of destroying it
// when user clicks the close button
if (e.CloseReason == CloseReason.UserClosing)
{
e.Cancel = true;
_preadjustmentForm.Hide();
// Notify outside code that the form was closed by user
PreAdjustmentFormClosedByUser?.Invoke(
this,
EventArgs.Empty);
}
};
}
// If already visible, bring it to front
if (_preadjustmentForm.Visible)
{
_preadjustmentForm.Activate();
_preadjustmentForm.BringToFront();
return;
}
// Show existing form instance
_preadjustmentForm.Show(owner);
}
}
#endregion
#region ================================== Create context for PreAdjustmentUI ==================================
/*public interface IGciToolContext
{
IReadOnlyList<int> GetEnabledSlots();
string GetPcbId(int slot);
string GetSerialNumber(int slot);
bool Login(int slot);
bool Logout(int slot);
bool ReadRegister(int slot, int address, out string value);
bool WriteRegister(int slot, int address, string value);
void LogInfo(string message);
void LogError(string message);
event EventHandler<GciMeterChangedEventArgs> MeterChanged;
}*/
#endregion
// GCI to PreadjustmentUI - AUTOMATIC - STANDALONE - Laatzen GUI
#region ================================== PreAdjustmentUI INIT ==================================
/// <summary>
/// Initializes PreAdjustment environment.
/// </summary>
public PreAdjustmentInitializationResult Preadjustment_Initialization(
ProcessProgress pp,
List<MeterStateControl> mc)
{
return Preadjustment_Initialization(pp, mc, CancellationToken.None);
}
public PreAdjustmentInitializationResult Preadjustment_Initialization(
ProcessProgress pp,
List<MeterStateControl> mc,
CancellationToken token)
{
const string operation = nameof(Preadjustment_Initialization);
VerifyPreadjustmentAttemptAccess();
try
{
LogInfo(operation, "Start.");
token.ThrowIfCancellationRequested();
lock (preadjustmentProcessGate)
{
if (activePreadjustmentCancellation != null || (_progressProcess != null && _progressProcess.IsBusy))
throw new InvalidOperationException("Cannot initialize while a preadjustment process is running.");
_progressProcess = pp;
_meterControls = mc;
// A new attempt must not inherit the previous selection/failure flags.
foreach (var control in mc)
{
control.Failed = false;
control.Ok = false;
SetSlotSelected(control.Slot, control.IsEnabled);
}
}
var initializedSlots = _meterBatch.ListOfMeters
.Select(m => m.Slot)
.OrderBy(s => s)
.ToList();
var result =
new PreAdjustmentInitializationResult
{
Success = initializedSlots.Any(),
Slots = string.Join(",", initializedSlots),
ErrorMessage = initializedSlots.Any()
? null
: "No initialized meters found."
};
LogInfo(operation, result.ToString());
return result;
}
catch (Exception ex)
{
LogError(operation, ex);
return new PreAdjustmentInitializationResult
{
Success = false,
ErrorMessage = ex.Message
};
}
}
#endregion
#region ================================== PreAdjustmentUI DETECT process ==================================
public Task<PreadjustmentDetectResult> PreAdjustment_DetectAsync(
IEnumerable<PublicModels.MeterBatchDebugStatus> selectedSlots,
CancellationToken token = default)
{
return Task.Run(() => Preadjustment_Detect(selectedSlots, token), token);
}
public PreadjustmentDetectResult PreAdjustment_DetectDirect(
IEnumerable<PublicModels.MeterBatchDebugStatus> selectedSlots,
CancellationToken token = default)
{
return Preadjustment_Detect(selectedSlots, token);
}
private PreadjustmentDetectResult Preadjustment_Detect(
IEnumerable<PublicModels.MeterBatchDebugStatus> selectedSlots,
CancellationToken token = default)
{
const string operation = nameof(Preadjustment_Detect);
VerifyPreadjustmentAttemptAccess();
try
{
LogInfo(operation, "Start.");
MeterBatch globalMeterBatch = new MeterBatch();
MeterBatch thermoMeterBatch = new MeterBatch();
if (_progressProcess.Setting == null)
throw new ArgumentNullException(nameof(_progressProcess.Setting));
if (_meterControls == null)
throw new ArgumentNullException(nameof(_meterControls));
if (_tempMeterControls == null)
_tempMeterControls = new List<MeterStateControl>();
if (globalMeterBatch.ListOfMeters.Any())
globalMeterBatch.RemoveAllMeters();
if (thermoMeterBatch.ListOfMeters.Any())
thermoMeterBatch.RemoveAllMeters();
globalMeterBatch = _meterBatch;
_meterControls = CreateMeterControls(selectedSlots);
if (!_progressProcess.Setting.GetTempUseTempFlansh())
_tempMeterControls.Clear();
var allMeterControls = new List<MeterStateControl>();
allMeterControls.AddRange(_meterControls);
allMeterControls.AddRange(_tempMeterControls);
CreateZeroFlowMeters(
globalMeterBatch,
thermoMeterBatch,
allMeterControls,
token);
SetEnableOpeningState(allMeterControls);
if (!_meterControls.Any(a => a.EnableOpening))
{
return new PreadjustmentDetectResult
{
Success = false,
DetectedMeterCount = 0,
DetectedThermometerCount = _tempMeterControls.Count(t => t.IsEnabled),
ErrorMessage = "No enabled meter found."
};
}
SetUnknownStatus(allMeterControls);
CheckNormalMeters(_meterControls, token);
CheckTemperatureMeters(
_progressProcess.Setting,
_tempMeterControls,
token);
List<MeterStateControl> failedMeters = _meterControls.Where(m => m.IsEnabled && !m.Ok).ToList();
foreach (var failed in failedMeters)
{
failed.Failed = true;
failed.IsEnabled = false;
}
return new PreadjustmentDetectResult
{
Success = failedMeters.Count == 0,
DetectedMeterCount = _meterControls.Count(m => m.IsEnabled && m.Ok),
DetectedThermometerCount = _tempMeterControls.Count(t => t.IsEnabled && t.Ok),
ErrorMessage = failedMeters.Count == 0 ? null : $"Request or streaming port check failed for slots: {string.Join(",", failedMeters.Select(m => m.Slot))}."
};
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
// A batch-level detection error cannot reuse an earlier successful status.
if (_meterControls != null)
foreach (var control in _meterControls)
{
control.Ok = false;
control.Failed = true;
control.IsEnabled = false;
}
return new PreadjustmentDetectResult
{
Success = false,
ErrorMessage = ex.Message
};
}
}
private List<MeterStateControl> CreateMeterControls(IEnumerable<PublicModels.MeterBatchDebugStatus> slots)
{
var controls =
new List<MeterStateControl>();
foreach (var slot in slots)
{
var ctl = new MeterStateControl(slot.Slot);
ctl.IsEnabled = true;
controls.Add(ctl);
}
return controls;
}
private void CreateZeroFlowMeters(
MeterBatch globalMeterBatch,
MeterBatch thermoMeterBatch,
List<MeterStateControl> allMeterControls,
CancellationToken token)
{
foreach (var meterStateCtl in allMeterControls)
{
token.ThrowIfCancellationRequested();
if (!(meterStateCtl.IsEnabled ||
meterStateCtl is TempMeterStateControl))
{
continue;
}
if (meterStateCtl.Slot == -1)
continue;
/*ZeroFlowGenesisMeter currentMeter = new ZeroFlowGenesisMeter(meterStateCtl.Slot, 3, !(meterStateCtl is TempMeterStateControl));
var convertedMeter = GetMeterThreadSafe(meterStateCtl.Slot);
if (convertedMeter != null)
{
convertedMeter.CopySafeStateTo(currentMeter);
}
currentMeter.LogOnEnable = true;
currentMeter.LoginFailed = false;
currentMeter.PreparationFailed = false;
currentMeter.AmplitudeFailed = false;
currentMeter.ZeroFlowOffsetFailed = false;
currentMeter.CompletionFailed = false;
currentMeter.Ok = false;
currentMeter.EmptyPipeCheckEnable = false;
currentMeter.EmptyPipeCheckFailed = false;
if (meterStateCtl is TempMeterStateControl)
{
thermoMeterBatch.AddMeter(currentMeter);
meterStateCtl.IsEnabled = true;
}
else
{
if (currentMeter.useConfigSource != ConfigSource.InterfaceInputConfig)
globalMeterBatch.AddMeter(currentMeter);
else
globalMeterBatch.AddMeter2(currentMeter);
}
meterStateCtl.Meter = currentMeter;*/
meterStateCtl.Meter = GetMeterThreadSafe2(meterStateCtl.Slot);
}
}
private void SetEnableOpeningState(
List<MeterStateControl> allMeterControls)
{
foreach (var meterState in allMeterControls)
{
meterState.EnableOpening =
meterState.IsEnabled;
}
}
private void SetUnknownStatus(
List<MeterStateControl> allMeterControls)
{
foreach (var meterState in allMeterControls)
{
meterState.SetToUnknownStatus =
!meterState.EnableOpening;
}
}
private void CheckNormalMeters(
List<MeterStateControl> meterControls,
CancellationToken token)
{
var checks = meterControls.Where(m => m != null && m.IsEnabled).Select(meterCtl =>
GetWorker(meterCtl.Slot).RunAsync(() =>
{
token.ThrowIfCancellationRequested();
try
{
meterCtl.Ok = meterCtl.Meter != null &&
meterCtl.Meter.CheckRequestPort() &&
meterCtl.Meter.CheckStreamingPort();
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
meterCtl.Ok = false;
LogError($"DetectSlot{meterCtl.Slot}", ex);
}
return meterCtl.Ok;
}, token, "PreadjustmentDetectSlot")).ToArray();
Task.WhenAll(checks).GetAwaiter().GetResult();
}
private void CheckTemperatureMeters(
PreAdjustmentSettingsContainer settings,
List<MeterStateControl> tempMeterControls,
CancellationToken token)
{
if (settings.GetTempUseManualInput())
return;
foreach (var meterCtl in tempMeterControls)
{
token.ThrowIfCancellationRequested();
if (meterCtl != null && meterCtl.IsEnabled)
{
meterCtl.Ok =
meterCtl.Meter.CheckStreamingPort();
}
}
}
#endregion
#region ================================== PreAdjustmentUI PREPARATION process ==================================
public Task<PreAdjustmentProcessResult> PreAdjustment_PreparationAsync(
int slot,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(
() => PreAdjustment_Preparation(slot, token),
token,
nameof(PreAdjustment_Preparation));
}
public PreAdjustmentProcessResult PreAdjustment_PreparationDirect()
{
return PreAdjustment_PreparationDirect(CancellationToken.None);
}
public PreAdjustmentProcessResult PreAdjustment_PreparationDirect(CancellationToken token)
{
return PreAdjustment_Preparation(-1, token);
}
/// <summary>
/// Executes standalone preparation process.
/// </summary>
public PreAdjustmentProcessResult PreAdjustment_Preparation(
int slot = -1,
CancellationToken token = default)
{
const string operation = nameof(PreAdjustment_Preparation);
VerifyPreadjustmentAttemptAccess();
try
{
LogInfo(operation, $"Start. Slot={slot}");
BaseProcess process = CreatePreparationProcess(_progressProcess);
bool success =
ExecuteProcess(process, _progressProcess, token);
var result =
new PreAdjustmentProcessResult
{
Success = success,
Slot = slot,
ProcessName = process.ProcessName,
ErrorMessage = success
? null
: process.FailedMessage
};
LogInfo(operation, result.ToString());
return result;
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
LogError(operation, ex);
return new PreAdjustmentProcessResult
{
Success = false,
Slot = slot,
ProcessName = "Preparation",
ErrorMessage = ex.Message
};
}
}
/// <summary>
/// Creates preparation process instance based on current configuration.
/// </summary>
/// <param name="pp">Current process progress context</param>
/// <returns>Configured preparation process</returns>
private BaseProcess CreatePreparationProcess(ProcessProgress pp)
{
if (pp.Setting.NumberOfPaths == 1)
{
return new SPPreparationProcess(
"Preparation Single",
PreAdjustmentControl.StatusPanelItems.Prepare,
PreAdjustmentControl.PredefinedMessages.WaitUntilPreparationFinished(pp.Setting.Culture),
PreAdjustmentControl.PredefinedMessages.PreparationFailed(pp.Setting.Culture),
60);
}
return new PreparationProcess(
"Preparation",
PreAdjustmentControl.StatusPanelItems.Prepare,
PreAdjustmentControl.PredefinedMessages.WaitUntilPreparationFinished(pp.Setting.Culture),
PreAdjustmentControl.PredefinedMessages.PreparationFailed(pp.Setting.Culture),
60);
}
public bool PreAdjustment_PushCalibrationParams(double temperature)
{
if (_progressProcess != null)
{
_progressProcess.PushedTestBenchTemp = temperature;
_progressProcess.TempretureSelected = true;
return true;
}
return false;
}
#endregion
#region ================================== PreAdjustmentUI AMPLITUDE TEST process ==================================
public Task<PreAdjustmentProcessResult> PreAdjustment_AmplitudeTestAsync(
int slot,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(
() => PreAdjustment_AmplitudeTest(slot, token),
token,
nameof(PreAdjustment_AmplitudeTest));
}
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTestDirect()
{
return PreAdjustment_AmplitudeTestDirect(CancellationToken.None);
}
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTestDirect(CancellationToken token)
{
return PreAdjustment_AmplitudeTest(-1, token);
}
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTest(
int slot = -1,
CancellationToken token = default)
{
const string operation = nameof(PreAdjustment_AmplitudeTest);
VerifyPreadjustmentAttemptAccess();
try
{
LogInfo(operation, $"Start. Slot={slot}");
if (_progressProcess.Setting.TempOnly)
{
return new PreAdjustmentProcessResult
{
Success = true,
Slot = slot,
ProcessName = "Amplitude Test"
};
}
BaseProcess process = CreateAmplitudeTestProcess(_progressProcess);
bool success = ExecuteProcess(process, _progressProcess, token);
LogInfo(operation, $"Finished. Slot={slot}, Success={success}");
return new PreAdjustmentProcessResult
{
Success = success,
Slot = slot,
ProcessName = process.ProcessName,
ErrorMessage = success ? null : process.FailedMessage
};
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
LogError(operation, ex);
return new PreAdjustmentProcessResult
{
Success = false,
Slot = slot,
ProcessName = "Amplitude Test",
ErrorMessage = ex.Message
};
}
}
/// <summary>
/// Creates amplitude test process instance based on current configuration.
/// </summary>
/// <param name="pp">Current process progress context</param>
/// <returns>Configured amplitude test process</returns>
private BaseProcess CreateAmplitudeTestProcess(ProcessProgress pp)
{
if (pp.Setting.NumberOfPaths == 1)
{
return new SPAmplitudeTestProcess(
"Amplitude Test Single",
PreAdjustmentControl.StatusPanelItems.Amplitude,
PreAdjustmentControl.PredefinedMessages.WaitUntilAmplitudeTestFinished(pp.Setting.Culture),
PreAdjustmentControl.PredefinedMessages.AmplitudeFailed(pp.Setting.Culture),
4 * 60);
}
return new AmplitudeTestProcess(
"Amplitude Test",
PreAdjustmentControl.StatusPanelItems.Amplitude,
PreAdjustmentControl.PredefinedMessages.WaitUntilAmplitudeTestFinished(pp.Setting.Culture),
PreAdjustmentControl.PredefinedMessages.AmplitudeFailed(pp.Setting.Culture),
4 * 60);
}
#endregion
#region ================================== PreAdjustmentUI TEMPERATURE CALIBRATION process ==================================
public Task<PreAdjustmentProcessResult> PreAdjustment_TemperatureCalibrationAsync(
int slot,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(
() => PreAdjustment_TemperatureCalibration(slot, token),
token,
nameof(PreAdjustment_TemperatureCalibration));
}
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect()
{
return PreAdjustment_TemperatureCalibrationDirect(CancellationToken.None);
}
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect(CancellationToken token)
{
return PreAdjustment_TemperatureCalibration(-1, token);
}
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect(
CalibrationTemperatureInput temperature, CancellationToken token)
{
return PreAdjustment_TemperatureCalibration(-1, token, temperature);
}
public Task<PreAdjustmentProcessResult> PreAdjustment_TemperatureCalibrationAsync(
int slot, CalibrationTemperatureInput temperature, CancellationToken token)
{
return GetWorker(slot).RunAsync(
() => PreAdjustment_TemperatureCalibration(slot, token, temperature),
token, nameof(PreAdjustment_TemperatureCalibration));
}
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibration(
int slot = -1, CancellationToken token = default)
{
return PreAdjustment_TemperatureCalibration(slot, token, null);
}
private PreAdjustmentProcessResult PreAdjustment_TemperatureCalibration(
int slot, CancellationToken token, CalibrationTemperatureInput temperature)
{
const string operation = nameof(PreAdjustment_TemperatureCalibration);
VerifyPreadjustmentAttemptAccess();
BaseProcess process = null;
int[] participatingSlots = new int[0];
try
{
LogInfo(operation, "Start.");
participatingSlots = _meterControls.Where(m => m.IsEnabled).Select(m => m.Slot).ToArray();
process = CreateTemperatureCalibrationProcess(_progressProcess, temperature);
bool success = ExecuteProcess(process, _progressProcess, token);
var result = BuildTemperatureCalibrationResult(process, participatingSlots, slot,
success, success ? null : process.FailedMessage);
LogInfo(operation, result.ToString());
return result;
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
LogError(operation, ex);
return BuildTemperatureCalibrationResult(process, participatingSlots, slot, false, ex.Message);
}
}
private TemperatureCalibrationResult BuildTemperatureCalibrationResult(
BaseProcess process, int[] participatingSlots, int slot, bool success, string error)
{
var source = process as ITemperatureCalibrationMeasurements;
var measurements = source == null
? new Dictionary<int, CalibrationTemperatureMeasurement>()
: source.GetTemperatureMeasurements().ToDictionary(m => m.Slot);
var result = new TemperatureCalibrationResult
{
Success = success, Slot = slot, ProcessName = process?.ProcessName ?? "Temperature Calibration",
ErrorMessage = error
};
foreach (int meterSlot in participatingSlots)
{
CalibrationTemperatureMeasurement measurement;
measurements.TryGetValue(meterSlot, out measurement);
var control = _meterControls.FirstOrDefault(m => m.Slot == meterSlot);
bool completed = measurement != null && measurement.CalibrationCompleted &&
control != null && !control.Failed;
result.Slots.Add(new TemperatureCalibrationSlotResult
{
Slot = meterSlot, ProcessName = result.ProcessName, Success = completed,
ReferenceTemperatureCelsius = measurement?.ReferenceTemperatureCelsius,
MeterTemperatureCelsius = measurement?.MeterTemperatureCelsius,
AbsoluteDifferenceCelsius = measurement?.AbsoluteDifferenceCelsius,
DeviationLimitCelsius = measurement?.DeviationLimitCelsius,
SampleCount = measurement?.SampleCount ?? 0,
TemperatureCheckPassed = measurement?.TemperatureCheckPassed,
ErrorMessage = completed ? null :
(measurement?.ErrorMessage ?? error ?? "Temperature calibration did not complete.")
});
}
return result;
}
public TemperatureCalibrationResult PreAdjustment_TemperatureCalibrationDetailedDirect(
CalibrationTemperatureInput temperature, CancellationToken token)
{
return TemperatureCalibrationResult.FromProcessResult(
PreAdjustment_TemperatureCalibration(-1, token, temperature));
}
public async Task<TemperatureCalibrationResult> PreAdjustment_TemperatureCalibrationDetailedAsync(
int slot, CalibrationTemperatureInput temperature, CancellationToken token)
{
var result = await PreAdjustment_TemperatureCalibrationAsync(slot, temperature, token).ConfigureAwait(false);
return TemperatureCalibrationResult.FromProcessResult(result);
}
public bool PreAdjustment_PushTemperature(double temperature)
{
if (_progressProcess != null)
{
_progressProcess.PushedTestBenchTemp = temperature;
_progressProcess.TempretureSelected = true;
return true;
}
return false;
}
/// <summary>
/// Creates temperature calibration process instance based on current configuration.
/// </summary>
/// <param name="pp">Current process progress context</param>
/// <returns>Configured temperature calibration process</returns>
private BaseProcess CreateTemperatureCalibrationProcess(ProcessProgress pp, CalibrationTemperatureInput temperature)
{
if (pp.Setting.NumberOfPaths == 1)
{
return new SPTemperatureCalibrationProcess(
"Temperature Calibration Single",
PreAdjustmentControl.StatusPanelItems.TempCal,
PreAdjustmentControl.PredefinedMessages.WaitUntilTemperatureCalibrationFinished(pp.Setting.Culture),
PreAdjustmentControl.PredefinedMessages.TempCalFailed(pp.Setting.Culture),
2 * 60) { DirectTemperature = temperature };
}
return new TemperatureCalibrationProcess(
"Temperature Calibration",
PreAdjustmentControl.StatusPanelItems.TempCal,
PreAdjustmentControl.PredefinedMessages.WaitUntilTemperatureCalibrationFinished(pp.Setting.Culture),
PreAdjustmentControl.PredefinedMessages.TempCalFailed(pp.Setting.Culture),
2 * 60) { DirectTemperature = temperature };
}
#endregion
#region ================================== PreAdjustmentUI OFFSET TEST process ==================================
public Task<PreAdjustmentProcessResult> PreAdjustment_OffsetTestAsync(
int slot,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(
() => PreAdjustment_OffsetTest(slot, token),
token,
nameof(PreAdjustment_OffsetTest));
}
public PreAdjustmentProcessResult PreAdjustment_OffsetTestDirect()
{
return PreAdjustment_OffsetTestDirect(CancellationToken.None);
}
public PreAdjustmentProcessResult PreAdjustment_OffsetTestDirect(CancellationToken token)
{
return PreAdjustment_OffsetTest(-1, token);
}
public PreAdjustmentProcessResult PreAdjustment_OffsetTest(
int slot = -1,
CancellationToken token = default)
{
const string operation = nameof(PreAdjustment_OffsetTest);
VerifyPreadjustmentAttemptAccess();
try
{
LogInfo(operation, $"Start. Slot={slot}");
if (_progressProcess.Setting.TempOnly)
{
return new PreAdjustmentProcessResult
{
Success = true,
Slot = slot,
ProcessName = "Offset Test"
};
}
BaseProcess process = CreateOffsetTestProcess(_progressProcess);
bool success = ExecuteProcess(process, _progressProcess, token);
var result =
new PreAdjustmentProcessResult
{
Success = success,
Slot = slot,
ProcessName = process.ProcessName,
ErrorMessage = success
? null
: process.FailedMessage
};
LogInfo(operation, result.ToString());
return result;
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
LogError(operation, ex);
return new PreAdjustmentProcessResult
{
Success = false,
Slot = slot,
ProcessName = "Offset Test",
ErrorMessage = ex.Message
};
}
}
/// <summary>
/// Creates offset test process instance based on current configuration.
/// </summary>
/// <param name="pp">Current process progress context</param>
/// <returns>Configured offset test process</returns>
private BaseProcess CreateOffsetTestProcess(ProcessProgress pp)
{
if (pp.Setting.NumberOfPaths == 1)
{
return new SPOffsetTestProcess(
"Offset Test Single",
PreAdjustmentControl.StatusPanelItems.Offset,
PreAdjustmentControl.PredefinedMessages.WaitUntilZeroflowOffsetTestFinished(pp.Setting.Culture),
PreAdjustmentControl.PredefinedMessages.ZeroflowOffsetTestFailed(pp.Setting.Culture),
18 * 60);
}
return new OffsetTestProcess(
"Offset Test",
PreAdjustmentControl.StatusPanelItems.Offset,
PreAdjustmentControl.PredefinedMessages.WaitUntilZeroflowOffsetTestFinished(pp.Setting.Culture),
PreAdjustmentControl.PredefinedMessages.ZeroflowOffsetTestFailed(pp.Setting.Culture),
18 * 60);
}
#endregion
#region ================================== PreAdjustmentUI COMPLETION process ==================================
public Task<PreAdjustmentProcessResult> PreAdjustment_CompletionAsync(
int slot,
CancellationToken token = default)
{
return GetWorker(slot).RunAsync(
() => PreAdjustment_Completion(slot, token),
token,
nameof(PreAdjustment_Completion));
}
public PreAdjustmentProcessResult PreAdjustment_CompletionDirect()
{
return PreAdjustment_CompletionDirect(CancellationToken.None);
}
public PreAdjustmentProcessResult PreAdjustment_CompletionDirect(CancellationToken token)
{
return PreAdjustment_Completion(-1, token);
}
public PreAdjustmentProcessResult PreAdjustment_Completion(
int slot = -1,
CancellationToken token = default)
{
const string operation = nameof(PreAdjustment_Completion);
VerifyPreadjustmentAttemptAccess();
try
{
LogInfo(operation, $"Start.");
BaseProcess process = CreateCompletionProcess(_progressProcess);
bool success = ExecuteProcess(process, _progressProcess, token);
var result =
new CompletionProcessResult
{
Success = success,
Slot = slot,
ProcessName = process.ProcessName,
ErrorMessage = success
? null
: process.FailedMessage
};
CaptureCompletionDatabaseRecords(process, result);
LogInfo(operation, result.ToString());
return result;
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
LogError(operation, ex);
return new PreAdjustmentProcessResult
{
Success = false,
Slot = slot,
ProcessName = "Completion",
ErrorMessage = ex.Message
};
}
}
private void CaptureCompletionDatabaseRecords(BaseProcess process, CompletionProcessResult result)
{
var completion = process as CompletionProcess;
if (completion == null)
{
// Single-path Completion has a different readout contract.
foreach (var control in _meterControls.Where(m => m.IsEnabled || m.Failed))
_progressProcess.DebugMessage("SQL Completion export is unavailable for this process type.", control.Slot, "DB");
return;
}
var snapshots = completion.GetDatabaseSnapshots();
foreach (var snapshot in snapshots)
{
int pcbId;
int.TryParse(snapshot.PcbId, out pcbId);
// The writer reports invalid identity without sending it to SQL.
result.DatabaseRecords.Add(new PreadjustmentCompletionRecord
{
Slot = snapshot.Slot, PcbId = pcbId, MeterSize = snapshot.MeterSize,
ZeroOffset1 = snapshot.ZeroOffset1, ZeroOffset2 = snapshot.ZeroOffset2,
ZeroOffset3 = snapshot.ZeroOffset3,
FirstHitUpdatePeriod = snapshot.FirstHitUpdatePeriod,
FirstHitShift = snapshot.FirstHitShift,
FirstHitPercent1 = snapshot.FirstHitPercent1,
FirstHitPercent2 = snapshot.FirstHitPercent2,
FirstHitPercent3 = snapshot.FirstHitPercent3,
ToFTempOffset1 = snapshot.ToFTempOffset1,
ToFTempOffset2 = snapshot.ToFTempOffset2,
ToFTempOffset3 = snapshot.ToFTempOffset3,
Date = snapshot.Date, Successful = snapshot.Successful
});
}
foreach (var control in _meterControls.Where(m => m.IsEnabled || m.Failed))
{
if (snapshots.Any(s => s.Slot == control.Slot)) continue;
_progressProcess.DebugMessage("SQL write skipped: Completion produced no verified register readout.", control.Slot, "DB");
result.DatabaseWrites.Add(new CompletionDatabaseWriteResult
{
Slot = control.Slot,
Result = new CalibrationWriteResult
{
Status = CalibrationWriteStatus.Failed,
ErrorMessage = "Completion produced no verified register readout."
}
});
}
}
/// <summary>
/// Creates completion process instance based on current configuration.
/// </summary>
/// <param name="pp">Current process progress context</param>
/// <returns>Configured completion process</returns>
private BaseProcess CreateCompletionProcess(ProcessProgress pp)
{
if (pp.Setting.NumberOfPaths == 1)
{
return new SPCompletionProcess(
"Completion Single",
PreAdjustmentControl.StatusPanelItems.Completion,
PreAdjustmentControl.PredefinedMessages.WaitUntilCompletionFinished(pp.Setting.Culture),
PreAdjustmentControl.PredefinedMessages.CompletionFailed(pp.Setting.Culture),
1 * 60);
}
return new CompletionProcess(
"Completion",
PreAdjustmentControl.StatusPanelItems.Completion,
PreAdjustmentControl.PredefinedMessages.WaitUntilCompletionFinished(pp.Setting.Culture),
PreAdjustmentControl.PredefinedMessages.CompletionFailed(pp.Setting.Culture),
1 * 60);
}
#endregion
#region ================================== PreAdjustmentUI ExecuteProcess ==================================
private readonly AsyncLocal<object> preadjustmentAttemptContext = new AsyncLocal<object>();
private object activePreadjustmentAttempt;
private void VerifyPreadjustmentAttemptAccess()
{
lock (preadjustmentProcessGate)
{
if (activePreadjustmentAttempt != null &&
!ReferenceEquals(activePreadjustmentAttempt, preadjustmentAttemptContext.Value))
throw new InvalidOperationException("Another preadjustment attempt owns the slot selection.");
}
}
/// <summary>
/// Executes one synchronous phase attempt with a temporary list of participating slots.
/// The callback must return only after all phase workers and streaming leases are drained.
/// This changes process participation only; it does not connect, disconnect or log in.
/// </summary>
public T ExecutePreadjustmentAttempt<T>(IEnumerable<int> slots, bool retry,
bool initialization, CancellationToken token, Func<T> action)
{
if (slots == null) throw new ArgumentNullException(nameof(slots));
if (action == null) throw new ArgumentNullException(nameof(action));
var selected = new HashSet<int>(slots);
if (selected.Count == 0 || selected.Any(s => s <= 0))
throw new ArgumentException("A phase attempt requires valid slots.", nameof(slots));
List<MeterStateControl> original;
Dictionary<int, bool> originalSelection;
object previousContext = preadjustmentAttemptContext.Value;
var owner = new object();
lock (preadjustmentProcessGate)
{
token.ThrowIfCancellationRequested();
if (activePreadjustmentAttempt != null || activePreadjustmentCancellation != null ||
(_progressProcess != null && _progressProcess.IsBusy))
throw new InvalidOperationException("A preadjustment process is still running.");
original = (_meterControls ?? new List<MeterStateControl>()).ToList();
if (!initialization && selected.Any(s => !original.Any(c => c.Slot == s)))
throw new InvalidOperationException("Initialize preadjustment before retrying a phase.");
originalSelection = original.ToDictionary(c => c.Slot, c => IsSlotSelected(c.Slot));
activePreadjustmentAttempt = owner;
preadjustmentAttemptContext.Value = owner;
}
try
{
if (!initialization)
{
_meterControls = original.Where(c => selected.Contains(c.Slot)).ToList();
foreach (var control in _meterControls)
{
token.ThrowIfCancellationRequested();
if (retry)
{
// Restore execution state only. Do not call SetChecked here:
// its UI dispatch may block on the thread that created the control.
// IsEnabled controls phase eligibility; keep meter calibration data.
control.Failed = false;
control.Ok = false;
control.IsEnabled = true;
}
}
if (retry && _progressProcess != null) _progressProcess.Aborted = false;
foreach (var control in original)
{
token.ThrowIfCancellationRequested();
SetSlotSelected(control.Slot, selected.Contains(control.Slot) && control.IsEnabled);
}
}
token.ThrowIfCancellationRequested();
return action();
}
finally
{
try
{
// Detect can create replacement controls. Merge these back by slot so successful
// meters from an earlier attempt remain available for subsequent phases.
if (!initialization)
{
var updated = (_meterControls ?? new List<MeterStateControl>())
.Where(c => selected.Contains(c.Slot)).ToDictionary(c => c.Slot);
_meterControls = original.Select(c => updated.ContainsKey(c.Slot) ? updated[c.Slot] : c).ToList();
foreach (var control in _meterControls)
SetSlotSelected(control.Slot, selected.Contains(control.Slot)
? control.IsEnabled && !control.Failed : originalSelection[control.Slot]);
}
}
finally
{
lock (preadjustmentProcessGate)
{
activePreadjustmentAttempt = null;
preadjustmentAttemptContext.Value = previousContext;
}
}
}
}
private readonly object preadjustmentProcessGate = new object();
private CancellationTokenSource activePreadjustmentCancellation;
public void CancelPreadjustment()
{
CancellationTokenSource cancellation;
lock (preadjustmentProcessGate)
cancellation = activePreadjustmentCancellation;
if (cancellation == null) return;
try { cancellation.Cancel(); }
catch (ObjectDisposedException) { /* The process has already completed. */ }
}
private bool ExecuteProcess(BaseProcess process, ProcessProgress pp, CancellationToken token)
{
CancellationTokenSource previousSource;
CancellationTokenSource runSource;
lock (preadjustmentProcessGate)
{
VerifyPreadjustmentAttemptAccess();
if (activePreadjustmentCancellation != null || pp.IsBusy)
throw new InvalidOperationException("A preadjustment process is still running.");
token.ThrowIfCancellationRequested();
previousSource = pp.CancellationSource;
// A cancelled, completed run must not poison the next explicit run.
if (previousSource == null || previousSource.IsCancellationRequested)
previousSource = new CancellationTokenSource();
runSource = CancellationTokenSource.CreateLinkedTokenSource(token, previousSource.Token);
activePreadjustmentCancellation = runSource;
lock (pp) pp.CancellationSource = runSource;
pp.StopSequence = false;
pp.IsBusy = true;
}
try
{
if (_meterControls == null || !_meterControls.Any(m => m.IsEnabled && !m.Failed))
throw new InvalidOperationException("No eligible meters remain; preadjustment process was not executed. Run Initialization and Detect for a new attempt.");
foreach (var meterCtrl in _meterControls) meterCtrl.Ok = false;
process.StartProcess(pp, _meterControls, _tempMeterControls);
// Do not return when cancellation is requested: wait for every owned task.
process.Completion.GetAwaiter().GetResult();
runSource.Token.ThrowIfCancellationRequested();
foreach (var meterCtrl in _meterControls)
{
// Set only after all phase tasks completed; failed/absent slots stay false.
meterCtrl.Ok = meterCtrl.IsEnabled && !meterCtrl.Failed;
if (meterCtrl.IsEnabled && meterCtrl.Failed)
{
runSource.Token.ThrowIfCancellationRequested();
// This API runs in a background batch. Return-note generation can show
// a modal MessageBox/PDF UI and would leave the batch waiting forever.
// The interactive PreAdjustmentControl retains responsibility for reports.
pp.DebugMessage(
process.ProcessName + (activePreadjustmentAttempt != null
? " failed in this attempt."
: " failed; slot excluded from remaining preadjustment phases."),
meterCtrl.Slot,
"GCI",
meterCtrl.Meter == null ? string.Empty : meterCtrl.Meter.PcbId);
meterCtrl.IsEnabled = false;
}
}
return _meterControls.Any(m => m.IsEnabled && !m.Failed);
}
finally
{
// Completion has drained workers before the shared progress can be reused.
lock (preadjustmentProcessGate)
{
lock (pp) pp.CancellationSource = previousSource;
pp.IsBusy = false;
activePreadjustmentCancellation = null;
}
runSource.Dispose();
}
}
#endregion
}
}