<Feat>: Add per-phase retries for failed preadjustment slots, increase revision 3.9.3146.112

Cause:

- Failed meters were excluded without retrying the current phase.
- The procedure Temperature column was redundant when using T1/T2 sensors.

Solution:

1. Added configurable retries
   - Replace the Temperature column with Retries on failure.
   - Interpret the value as additional attempts; default to zero.
   - Preserve legacy Temperature XML compatibility.

2. Added failed-slot retry execution
   - Retry only failed slots, in parallel, within the current phase.
   - Preserve successful slots and their captured results.
   - Restore the complete GCI batch after each attempt.
   - Exclude unsuccessful slots after retries are exhausted.

3. Improved attempt diagnostics
   - Log attempt numbers and outcomes to slot windows and files.
   - Keep successful slot tabs visible during retries.

4. Preserved execution behavior
   - Keep existing measurement algorithms, sensor readings and port handling.
   - Process STOP during retries and hold once after all attempts finish.
   - Prevent further retries after cancellation or streaming cleanup failure.

5. Increase revision 3.9.3146.112
This commit is contained in:
2026-09-15 00:01:53 +02:00
parent 2660c564a6
commit 82c2b17a1f
8 changed files with 360 additions and 34 deletions
@@ -1656,6 +1656,7 @@ namespace GenesisCordonelInterface.API
CancellationToken token)
{
const string operation = nameof(Preadjustment_Initialization);
VerifyPreadjustmentAttemptAccess();
try
{
@@ -1731,6 +1732,7 @@ namespace GenesisCordonelInterface.API
CancellationToken token = default)
{
const string operation = nameof(Preadjustment_Detect);
VerifyPreadjustmentAttemptAccess();
try
{
@@ -1996,6 +1998,7 @@ namespace GenesisCordonelInterface.API
CancellationToken token = default)
{
const string operation = nameof(PreAdjustment_Preparation);
VerifyPreadjustmentAttemptAccess();
try
{
@@ -2100,6 +2103,7 @@ namespace GenesisCordonelInterface.API
CancellationToken token = default)
{
const string operation = nameof(PreAdjustment_AmplitudeTest);
VerifyPreadjustmentAttemptAccess();
try
{
@@ -2217,6 +2221,7 @@ namespace GenesisCordonelInterface.API
int slot, CancellationToken token, CalibrationTemperatureInput temperature)
{
const string operation = nameof(PreAdjustment_TemperatureCalibration);
VerifyPreadjustmentAttemptAccess();
BaseProcess process = null;
int[] participatingSlots = new int[0];
try
@@ -2353,6 +2358,7 @@ namespace GenesisCordonelInterface.API
CancellationToken token = default)
{
const string operation = nameof(PreAdjustment_OffsetTest);
VerifyPreadjustmentAttemptAccess();
try
{
@@ -2456,6 +2462,7 @@ namespace GenesisCordonelInterface.API
CancellationToken token = default)
{
const string operation = nameof(PreAdjustment_Completion);
VerifyPreadjustmentAttemptAccess();
try
{
@@ -2572,6 +2579,101 @@ namespace GenesisCordonelInterface.API
#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)
{
if (retry)
{
// Retry only this phase's failed slots; keep meter calibration data.
control.Failed = false;
control.Ok = false;
control.SetChecked(true);
control.IsEnabled = true;
}
}
if (retry && _progressProcess != null) _progressProcess.Aborted = false;
foreach (var control in original)
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;
@@ -2591,6 +2693,7 @@ namespace GenesisCordonelInterface.API
CancellationTokenSource runSource;
lock (preadjustmentProcessGate)
{
VerifyPreadjustmentAttemptAccess();
if (activePreadjustmentCancellation != null || pp.IsBusy)
throw new InvalidOperationException("A preadjustment process is still running.");
token.ThrowIfCancellationRequested();
@@ -2626,7 +2729,9 @@ namespace GenesisCordonelInterface.API
// a modal MessageBox/PDF UI and would leave the batch waiting forever.
// The interactive PreAdjustmentControl retains responsibility for reports.
pp.DebugMessage(
process.ProcessName + " failed; slot excluded from remaining preadjustment phases.",
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);