public SwitchboardAction MakeNextActionDecision(NewVectorReceivedArgs vector)
        {
            if (!TryGetSwitchboardInputsFromVector(vector, out var current))
            {
                return(_lastAction); // not connected, we skip this heater and act again when the connection is re-established
            }
            var(hasValidTemperature, temp) = GetOvenTemperatureFromVector(vector);
            // Careful consideration must be taken if changing the order of the below statements.
            // Note that even though we received indication the board is connected above,
            // if the connection is lost after we return the action, the control program can still fail to act on the heater.
            // When it happens, the MustResend* methods will resend the expected action after 5 seconds.
            var vectorTime = vector.GetVectorTime();

            var(executeManualOff, executeManualOn) = MustExecuteManualMode(vectorTime);
            var action =
                executeManualOn ? new SwitchboardAction(true, vectorTime.AddSeconds(10)) :
                MustTurnOff(hasValidTemperature, temp, vectorTime) ? new SwitchboardAction(false, vectorTime) :
                CanTurnOn(hasValidTemperature, temp, vectorTime) ? new SwitchboardAction(true, vectorTime.AddSeconds(10)) :
                // manual off re-enables temp control, so we only turn off if CanTurnOn above didn't decide to turn on
                executeManualOff ? new SwitchboardAction(false, vectorTime) :
                // keep off: retrigger if current is detected
                MustResendOffCommand(temp, current, vectorTime) ? new SwitchboardAction(false, vectorTime) :
                // keep on: re-trigger early to avoid switching
                _lastAction.IsOn && _lastAction.GetRemainingOnSeconds(vectorTime) < 2 ? new SwitchboardAction(true, vectorTime.AddSeconds(10)) :
                _lastAction;

            return(_lastAction = action);
        }
        private async Task BoardLoop(MCUBoard board, List <IOconfOut230Vac> ports, CancellationToken token)
        {
            var lastActions    = new SwitchboardAction[ports.Max(p => p.PortNumber)];
            var boardStateName = board.BoxName + "_state";
            // we use the next 2 booleans to avoid spamming logs/display with an ongoing problem, so we only notify at the beginning and when we resume normal operation.
            // we might still get lots of entries for problems that alternate between normal and failed states, but for now is a good data point to know if that case is happening.
            var waitingBoardReconnect            = false;
            var tryingToRecoverAfterTimeoutWatch = new Stopwatch();

            while (!token.IsCancellationRequested)
            {
                try
                {
                    var vector = await _cmd.When(_ => true, token);

                    if (!CheckConnectedStateInVector(board, boardStateName, ref waitingBoardReconnect, vector))
                    {
                        continue; // no point trying to send commands while there is no connection to the board.
                    }
                    foreach (var port in ports)
                    {
                        await DoPortActions(vector, board, port, lastActions, token);
                    }

                    if (tryingToRecoverAfterTimeoutWatch.IsRunning)
                    {
                        tryingToRecoverAfterTimeoutWatch.Stop();
                        CALog.LogInfoAndConsoleLn(LogID.A, $"wrote to switch board without time outs after {tryingToRecoverAfterTimeoutWatch.Elapsed}, resuming normal action frequency - {board.ToShortDescription()}");
                    }
                }
                catch (TimeoutException)
                {
                    if (!tryingToRecoverAfterTimeoutWatch.IsRunning)
                    {
                        // we only notify of the situation once while this is a different way a disconnect might look like, we have
                        CALog.LogInfoAndConsoleLn(LogID.A, $"timed out writing to switchboard, reducing action frequency until reconnect - {board.ToShortDescription()}");
                        tryingToRecoverAfterTimeoutWatch.Restart();
                    }
                    // forcing reduced acting frequency )
                    try { await Task.Delay(500, token); }
                    catch (TaskCanceledException) { }
                }
                catch (TaskCanceledException ex)
                {
                    if (!token.IsCancellationRequested)
                    {
                        CALog.LogErrorAndConsoleLn(LogID.A, ex.ToString());
                    }
                }
                catch (Exception ex)
                {
                    CALog.LogErrorAndConsoleLn(LogID.A, ex.ToString());
                }
            }

            AllOff(board, ports);
        }
 private static async Task DoPortActions(NewVectorReceivedArgs vector, MCUBoard board, IOconfOut230Vac port, SwitchboardAction[] lastActions, CancellationToken token)
 {
     if (token.IsCancellationRequested)
     {
         return;
     }
     try
     {
         var action = SwitchboardAction.FromVectorSamples(vector, port.Name);
         if (action.Equals(lastActions[port.PortNumber - 1]))
         {
             return; // no action changes has been requested since the last action taken on the heater.
         }
         var onSeconds = action.GetRemainingOnSeconds(vector.GetVectorTime());
         if (onSeconds <= 0)
         {
             await board.SafeWriteLine($"p{port.PortNumber} off", token);
         }
         else if (onSeconds == int.MaxValue)
         {
             await board.SafeWriteLine($"p{port.PortNumber} on", token);
         }
         else
         {
             await board.SafeWriteLine($"p{port.PortNumber} on {onSeconds}", token);
         }
         lastActions[port.PortNumber - 1] = action;
     }
     catch (TimeoutException)
     {
         // we don't want logging at this level as the caller handles this in a way that reduces the amount of noise for failures that last many vectors.
         throw;
     }
     catch (Exception)
     {
         CALog.LogErrorAndConsoleLn(LogID.A, $"Failed executing port action for board {board.BoxName} port {port.Name} - {port.Name}");
         throw; // this will log extra info and avoid extra board actions on this cycle
     }
 }