| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716 | #include "mmu2_protocol_logic.h"#include "mmu2_log.h"#include "mmu2_fsensor.h"#include "system_timer.h"#include <string.h>namespace MMU2 {static const uint8_t supportedMmuFWVersion[3] PROGMEM = { 2, 1, 1 };void ProtocolLogic::CheckAndReportAsyncEvents() {    // even when waiting for a query period, we need to report a change in filament sensor's state    // - it is vital for a precise synchronization of moves of the printer and the MMU    uint8_t fs = (uint8_t)WhereIsFilament();    if (fs != lastFSensor) {        SendAndUpdateFilamentSensor();    }}void ProtocolLogic::SendQuery() {    SendMsg(RequestMsg(RequestMsgCodes::Query, 0));    scopeState = ScopeState::QuerySent;}void ProtocolLogic::SendFINDAQuery() {    SendMsg(RequestMsg(RequestMsgCodes::Finda, 0));    scopeState = ScopeState::FINDAReqSent;}void ProtocolLogic::SendAndUpdateFilamentSensor() {    SendMsg(RequestMsg(RequestMsgCodes::FilamentSensor, lastFSensor = (uint8_t)WhereIsFilament()));    scopeState = ScopeState::FilamentSensorStateSent;}void ProtocolLogic::SendButton(uint8_t btn) {    SendMsg(RequestMsg(RequestMsgCodes::Button, btn));    scopeState = ScopeState::ButtonSent;}void ProtocolLogic::SendVersion(uint8_t stage) {    SendMsg(RequestMsg(RequestMsgCodes::Version, stage));    scopeState = (ScopeState)((uint_fast8_t)ScopeState::S0Sent + stage);}void ProtocolLogic::SendReadRegister(uint8_t index, ScopeState nextState) {    SendMsg(RequestMsg(RequestMsgCodes::Read, index));    scopeState = nextState;}// searches for "ok\n" in the incoming serial data (that's the usual response of the old MMU FW)struct OldMMUFWDetector {    uint8_t ok;    inline constexpr OldMMUFWDetector():ok(0) { }        enum class State : uint8_t { MatchingPart, SomethingElse, Matched };        /// @returns true when "ok\n" gets detected    State Detect(uint8_t c){        // consume old MMU FW's data if any -> avoid confusion of protocol decoder        if(ok == 0 && c == 'o'){            ++ok;            return State::MatchingPart;        } else if(ok == 1 && c == 'k'){            ++ok;            return State::MatchingPart;        } else if(ok == 2 && c == '\n'){            return State::Matched;        }        return State::SomethingElse;    }};StepStatus ProtocolLogic::ExpectingMessage() {    int bytesConsumed = 0;    int c = -1;        OldMMUFWDetector oldMMUh4x0r; // old MMU FW hacker ;)            // try to consume as many rx bytes as possible (until a message has been completed)    while ((c = uart->read()) >= 0) {        ++bytesConsumed;        RecordReceivedByte(c);        switch (protocol.DecodeResponse(c)) {        case DecodeStatus::MessageCompleted:            rsp = protocol.GetResponseMsg();            LogResponse();            RecordUARTActivity(); // something has happened on the UART, update the timeout record            return MessageReady;        case DecodeStatus::NeedMoreData:            break;        case DecodeStatus::Error:{            // consume old MMU FW's data if any -> avoid confusion of protocol decoder            auto old = oldMMUh4x0r.Detect(c);            if( old == OldMMUFWDetector::State::Matched ){                // hack bad FW version - BEWARE - we silently assume that the first query is an "S0"                // The old MMU FW responds with "ok\n" and we fake the response to a bad FW version at this spot                rsp = ResponseMsg(RequestMsg(RequestMsgCodes::Version, 0), ResponseMsgParamCodes::Accepted, 0);                return MessageReady;            } else if( old == OldMMUFWDetector::State::MatchingPart ){                break;            }            }            [[fallthrough]]; // otherwise        default:            RecordUARTActivity(); // something has happened on the UART, update the timeout record            return ProtocolError;        }    }    if (bytesConsumed != 0) {        RecordUARTActivity(); // something has happened on the UART, update the timeout record        return Processing;    // consumed some bytes, but message still not ready    } else if (Elapsed(linkLayerTimeout)) {        return CommunicationTimeout;    }    return Processing;}void ProtocolLogic::SendMsg(RequestMsg rq) {    uint8_t txbuff[Protocol::MaxRequestSize()];    uint8_t len = Protocol::EncodeRequest(rq, txbuff);    uart->write(txbuff, len);    LogRequestMsg(txbuff, len);    RecordUARTActivity();}void ProtocolLogic::StartSeqRestart() {    retries = maxRetries;    SendVersion(0);}void ProtocolLogic::DelayedRestartRestart() {    scopeState = ScopeState::RecoveringProtocolError;}void ProtocolLogic::CommandRestart() {    scopeState = ScopeState::CommandSent;    SendMsg(rq);}void ProtocolLogic::IdleRestart() {    scopeState = ScopeState::Ready;}StepStatus ProtocolLogic::ProcessVersionResponse(uint8_t stage) {    if (rsp.request.code != RequestMsgCodes::Version || rsp.request.value != stage) {        // got a response to something else - protocol corruption probably, repeat the query OR restart the comm by issuing S0?        SendVersion(stage);    } else {        mmuFwVersion[stage] = rsp.paramValue;        if (mmuFwVersion[stage] != pgm_read_byte(supportedMmuFWVersion + stage)) {            if (--retries == 0) {                return VersionMismatch;            } else {                SendVersion(stage);            }        } else {            dataTO.Reset(); // got a meaningful response from the MMU, stop data layer timeout tracking            SendVersion(stage + 1);        }    }    return Processing;}StepStatus ProtocolLogic::ScopeStep() {    if ( ! ExpectsResponse() ) {        // we are waiting for something        switch (currentScope) {        case Scope::DelayedRestart:            return DelayedRestartWait();        case Scope::Idle:            return IdleWait();        case Scope::Command:            return CommandWait();        case Scope::Stopped:            return StoppedStep();        default:            break;        }    } else {        // we are expecting a message        if (auto expmsg = ExpectingMessage(); expmsg != MessageReady) // this whole statement takes 12B            return expmsg;        // process message        switch (currentScope) {        case Scope::StartSeq:            return StartSeqStep(); // ~270B        case Scope::Idle:            return IdleStep(); // ~300B        case Scope::Command:            return CommandStep(); // ~430B        case Scope::Stopped:            return StoppedStep();        default:            break;        }    }    return Finished;}StepStatus ProtocolLogic::StartSeqStep() {    // solve initial handshake    switch (scopeState) {    case ScopeState::S0Sent: // received response to S0 - major    case ScopeState::S1Sent: // received response to S1 - minor    case ScopeState::S2Sent: // received response to S2 - minor        return ProcessVersionResponse((uint8_t)scopeState - (uint8_t)ScopeState::S0Sent);    case ScopeState::S3Sent: // received response to S3 - revision        if (rsp.request.code != RequestMsgCodes::Version || rsp.request.value != 3) {            // got a response to something else - protocol corruption probably, repeat the query OR restart the comm by issuing S0?            SendVersion(3);        } else {            mmuFwVersionBuild = rsp.paramValue; // just register the build number            // Start General Interrogation after line up.            // For now we just send the state of the filament sensor, but we may request            // data point states from the MMU as well. TBD in the future, especially with another protocol            SendAndUpdateFilamentSensor();        }        return Processing;    case ScopeState::FilamentSensorStateSent:        SwitchFromStartToIdle();        return Processing; // Returning Finished is not a good idea in case of a fast error recovery        // - it tells the printer, that the command which experienced a protocol error and recovered successfully actually terminated.        // In such a case we must return "Processing" in order to keep the MMU state machine running and prevent the printer from executing next G-codes.        break;    default:        return VersionMismatch;    }    return Finished;}StepStatus ProtocolLogic::DelayedRestartWait() {    if (Elapsed(heartBeatPeriod)) { // this basically means, that we are waiting until there is some traffic on        while (uart->read() != -1)            ; // clear the input buffer        // switch to StartSeq        Start();    }    return Processing;}StepStatus ProtocolLogic::CommandWait() {    if (Elapsed(heartBeatPeriod)) {        SendQuery();    } else {        // even when waiting for a query period, we need to report a change in filament sensor's state        // - it is vital for a precise synchronization of moves of the printer and the MMU        CheckAndReportAsyncEvents();    }    return Processing;}StepStatus ProtocolLogic::ProcessCommandQueryResponse() {    switch (rsp.paramCode) {    case ResponseMsgParamCodes::Processing:        progressCode = static_cast<ProgressCode>(rsp.paramValue);        errorCode = ErrorCode::OK;        SendAndUpdateFilamentSensor(); // keep on reporting the state of fsensor regularly        return Processing;    case ResponseMsgParamCodes::Error:        // in case of an error the progress code remains as it has been before        errorCode = static_cast<ErrorCode>(rsp.paramValue);        // keep on reporting the state of fsensor regularly even in command error state        // - the MMU checks FINDA and fsensor even while recovering from errors        SendAndUpdateFilamentSensor();        return CommandError;    case ResponseMsgParamCodes::Button:        // The user pushed a button on the MMU. Save it, do what we need to do        // to prepare, then pass it back to the MMU so it can work its magic.        buttonCode = static_cast<Buttons>(rsp.paramValue);        SendAndUpdateFilamentSensor();        return ButtonPushed;    case ResponseMsgParamCodes::Finished:        progressCode = ProgressCode::OK;        scopeState = ScopeState::Ready;        return Finished;    default:        return ProtocolError;    }}StepStatus ProtocolLogic::CommandStep() {    switch (scopeState) {    case ScopeState::CommandSent: {        switch (rsp.paramCode) { // the response should be either accepted or rejected        case ResponseMsgParamCodes::Accepted:            progressCode = ProgressCode::OK;            errorCode = ErrorCode::RUNNING;            scopeState = ScopeState::Wait;            break;        case ResponseMsgParamCodes::Rejected:            // rejected - should normally not happen, but report the error up            progressCode = ProgressCode::OK;            errorCode = ErrorCode::PROTOCOL_ERROR;            return CommandRejected;        default:            return ProtocolError;        }    } break;    case ScopeState::QuerySent:        return ProcessCommandQueryResponse();    case ScopeState::FilamentSensorStateSent:        SendFINDAQuery();        return Processing;    case ScopeState::FINDAReqSent:        SendReadRegister(4, ScopeState::StatisticsSent);        scopeState = ScopeState::StatisticsSent;        return Processing;    case ScopeState::StatisticsSent:        scopeState = ScopeState::Wait;        return Processing;    case ScopeState::ButtonSent:        if (rsp.paramCode == ResponseMsgParamCodes::Accepted) {            // Button was accepted, decrement the retry.            mmu2.DecrementRetryAttempts();        }        SendAndUpdateFilamentSensor();        break;    default:        return ProtocolError;    }    return Processing;}StepStatus ProtocolLogic::IdleWait() {    if (scopeState == ScopeState::Ready) { // check timeout        if (Elapsed(heartBeatPeriod)) {            SendQuery();            return Processing;        }    }    return Finished;}StepStatus ProtocolLogic::IdleStep() {    switch (scopeState) {    case ScopeState::QuerySent: // check UART        // If we are accidentally in Idle and we receive something like "T0 P1" - that means the communication dropped out while a command was in progress.        // That causes no issues here, we just need to switch to Command processing and continue there from now on.        // The usual response in this case should be some command and "F" - finished - that confirms we are in an Idle state even on the MMU side.        switch (rsp.request.code) {        case RequestMsgCodes::Cut:        case RequestMsgCodes::Eject:        case RequestMsgCodes::Load:        case RequestMsgCodes::Mode:        case RequestMsgCodes::Tool:        case RequestMsgCodes::Unload:            if (rsp.paramCode != ResponseMsgParamCodes::Finished) {                return SwitchFromIdleToCommand();            }            break;        case RequestMsgCodes::Reset:            // this one is kind of special            // we do not transfer to any "running" command (i.e. we stay in Idle),            // but in case there is an error reported we must make sure it gets propagated            switch (rsp.paramCode) {            case ResponseMsgParamCodes::Button:                // The user pushed a button on the MMU. Save it, do what we need to do                // to prepare, then pass it back to the MMU so it can work its magic.                buttonCode = static_cast<Buttons>(rsp.paramValue);                SendFINDAQuery();                return ButtonPushed;            case ResponseMsgParamCodes::Processing:                // @@TODO we may actually use this branch to report progress of manual operation on the MMU                // The MMU sends e.g. X0 P27 after its restart when the user presses an MMU button to move the Selector                // For now let's behave just like "finished"            case ResponseMsgParamCodes::Finished:                errorCode = ErrorCode::OK;                break;            default:                errorCode = static_cast<ErrorCode>(rsp.paramValue);                SendFINDAQuery(); // continue Idle state without restarting the communication                return CommandError;            }            break;        default:            return ProtocolError;        }        SendFINDAQuery();        return Processing;    case ScopeState::FINDAReqSent:        SendReadRegister(4, ScopeState::StatisticsSent);        scopeState = ScopeState::StatisticsSent;        return Processing;    case ScopeState::StatisticsSent:        failStatistics = rsp.paramValue;        scopeState = ScopeState::Ready;        return Finished;    case ScopeState::ButtonSent:        if (rsp.paramCode == ResponseMsgParamCodes::Accepted) {            // Button was accepted, decrement the retry.            mmu2.DecrementRetryAttempts();        }        SendFINDAQuery();        return Processing;    default:        return ProtocolError;    }    // The "return Finished" in this state machine requires a bit of explanation:    // The Idle state either did nothing (still waiting for the heartbeat timeout)    // or just successfully received the answer to Q0, whatever that was.    // In both cases, it is ready to hand over work to a command or something else,    // therefore we are returning Finished (also to exit mmu_loop() and unblock Marlin's loop!).    // If there is no work, we'll end up in the Idle state again    // and we'll send the heartbeat message after the specified timeout.    return Finished;}ProtocolLogic::ProtocolLogic(MMU2Serial *uart)    : currentScope(Scope::Stopped)    , scopeState(ScopeState::Ready)    , plannedRq(RequestMsgCodes::unknown, 0)    , lastUARTActivityMs(0)    , dataTO()    , rsp(RequestMsg(RequestMsgCodes::unknown, 0), ResponseMsgParamCodes::unknown, 0)    , state(State::Stopped)    , lrb(0)    , uart(uart)    , errorCode(ErrorCode::OK)    , progressCode(ProgressCode::OK)    , buttonCode(NoButton)    , lastFSensor((uint8_t)WhereIsFilament())    , findaPressed(false)    , failStatistics(0)    , mmuFwVersion { 0, 0, 0 }{}void ProtocolLogic::Start() {    state = State::InitSequence;    currentScope = Scope::StartSeq;    protocol.ResetResponseDecoder(); // important - finished delayed restart relies on this    StartSeqRestart();}void ProtocolLogic::Stop() {    state = State::Stopped;    currentScope = Scope::Stopped;}void ProtocolLogic::ToolChange(uint8_t slot) {    PlanGenericRequest(RequestMsg(RequestMsgCodes::Tool, slot));}void ProtocolLogic::Statistics() {    PlanGenericRequest(RequestMsg(RequestMsgCodes::Version, 3));}void ProtocolLogic::UnloadFilament() {    PlanGenericRequest(RequestMsg(RequestMsgCodes::Unload, 0));}void ProtocolLogic::LoadFilament(uint8_t slot) {    PlanGenericRequest(RequestMsg(RequestMsgCodes::Load, slot));}void ProtocolLogic::EjectFilament(uint8_t slot) {    PlanGenericRequest(RequestMsg(RequestMsgCodes::Eject, slot));}void ProtocolLogic::CutFilament(uint8_t slot) {    PlanGenericRequest(RequestMsg(RequestMsgCodes::Cut, slot));}void ProtocolLogic::ResetMMU() {    PlanGenericRequest(RequestMsg(RequestMsgCodes::Reset, 0));}void ProtocolLogic::Button(uint8_t index) {    PlanGenericRequest(RequestMsg(RequestMsgCodes::Button, index));}void ProtocolLogic::Home(uint8_t mode) {    PlanGenericRequest(RequestMsg(RequestMsgCodes::Home, mode));}void ProtocolLogic::PlanGenericRequest(RequestMsg rq) {    plannedRq = rq;    if (!ExpectsResponse()) {        ActivatePlannedRequest();    } // otherwise wait for an empty window to activate the request}bool ProtocolLogic::ActivatePlannedRequest() {    if (plannedRq.code == RequestMsgCodes::Button) {        // only issue the button to the MMU and do not restart the state machines        SendButton(plannedRq.value);        plannedRq = RequestMsg(RequestMsgCodes::unknown, 0);        return true;    } else if (plannedRq.code != RequestMsgCodes::unknown) {        currentScope = Scope::Command;        SetRequestMsg(plannedRq);        plannedRq = RequestMsg(RequestMsgCodes::unknown, 0);        CommandRestart();        return true;    }    return false;}StepStatus ProtocolLogic::SwitchFromIdleToCommand() {    currentScope = Scope::Command;    SetRequestMsg(rsp.request);    // we are recovering from a communication drop out, the command is already running    // and we have just received a response to a Q0 message about a command progress    return ProcessCommandQueryResponse();}void ProtocolLogic::SwitchToIdle() {    state = State::Running;    currentScope = Scope::Idle;    IdleRestart();}void ProtocolLogic::SwitchFromStartToIdle() {    state = State::Running;    currentScope = Scope::Idle;    IdleRestart();    SendQuery(); // force sending Q0 immediately}bool ProtocolLogic::Elapsed(uint32_t timeout) const {    return _millis() >= (lastUARTActivityMs + timeout);}void ProtocolLogic::RecordUARTActivity() {    lastUARTActivityMs = _millis();}void ProtocolLogic::RecordReceivedByte(uint8_t c) {    lastReceivedBytes[lrb] = c;    lrb = (lrb + 1) % lastReceivedBytes.size();}constexpr char NibbleToChar(uint8_t c) {    switch (c) {    case 0:    case 1:    case 2:    case 3:    case 4:    case 5:    case 6:    case 7:    case 8:    case 9:        return c + '0';    case 10:    case 11:    case 12:    case 13:    case 14:    case 15:        return (c - 10) + 'a';    default:        return 0;    }}void ProtocolLogic::FormatLastReceivedBytes(char *dst) {    for (uint8_t i = 0; i < lastReceivedBytes.size(); ++i) {        uint8_t b = lastReceivedBytes[(lrb - i - 1) % lastReceivedBytes.size()];        dst[i * 3] = NibbleToChar(b >> 4);        dst[i * 3 + 1] = NibbleToChar(b & 0xf);        dst[i * 3 + 2] = ' ';    }    dst[(lastReceivedBytes.size() - 1) * 3 + 2] = 0; // terminate properly}void ProtocolLogic::FormatLastResponseMsgAndClearLRB(char *dst) {    *dst++ = '<';    for (uint8_t i = 0; i < lrb; ++i) {        uint8_t b = lastReceivedBytes[i];        if (b < 32)            b = '.';        if (b > 127)            b = '.';        *dst++ = b;    }    *dst = 0; // terminate properly    lrb = 0;  // reset the input buffer index in case of a clean message}void ProtocolLogic::LogRequestMsg(const uint8_t *txbuff, uint8_t size) {    constexpr uint_fast8_t rqs = modules::protocol::Protocol::MaxRequestSize() + 2;    char tmp[rqs] = ">";    static char lastMsg[rqs] = "";    for (uint8_t i = 0; i < size; ++i) {        uint8_t b = txbuff[i];        if (b < 32)            b = '.';        if (b > 127)            b = '.';        tmp[i + 1] = b;    }    tmp[size + 1] = '\n';    tmp[size + 2] = 0;    if (!strncmp_P(tmp, PSTR(">S0*99.\n"), rqs) && !strncmp(lastMsg, tmp, rqs)) {        // @@TODO we skip the repeated request msgs for now        // to avoid spoiling the whole log just with ">S0" messages        // especially when the MMU is not connected.        // We'll lose the ability to see if the printer is actually        // trying to find the MMU, but since it has been reliable in the past        // we can live without it for now.    } else {        MMU2_ECHO_MSG(tmp);    }    memcpy(lastMsg, tmp, rqs);}void ProtocolLogic::LogError(const char *reason_P) {    char lrb[lastReceivedBytes.size() * 3];    FormatLastReceivedBytes(lrb);    MMU2_ERROR_MSGRPGM(reason_P);    SERIAL_ECHOPGM(", last bytes: ");    SERIAL_ECHOLN(lrb);}void ProtocolLogic::LogResponse() {    char lrb[lastReceivedBytes.size()];    FormatLastResponseMsgAndClearLRB(lrb);    MMU2_ECHO_MSG(lrb);    SERIAL_ECHOLN();}StepStatus ProtocolLogic::SuppressShortDropOuts(const char *msg_P, StepStatus ss) {    if (dataTO.Record(ss)) {        LogError(msg_P);        return dataTO.InitialCause();    } else {        return Processing; // suppress short drop outs of communication    }}StepStatus ProtocolLogic::HandleCommunicationTimeout() {    uart->flush(); // clear the output buffer    protocol.ResetResponseDecoder();    Start();    return SuppressShortDropOuts(PSTR("Communication timeout"), CommunicationTimeout);}StepStatus ProtocolLogic::HandleProtocolError() {    uart->flush(); // clear the output buffer    state = State::InitSequence;    currentScope = Scope::DelayedRestart;    DelayedRestartRestart();    return SuppressShortDropOuts(PSTR("Protocol Error"), ProtocolError);}StepStatus ProtocolLogic::Step() {    if (!ExpectsResponse()) { // if not waiting for a response, activate a planned request immediately        ActivatePlannedRequest();    }    auto currentStatus = ScopeStep();    switch (currentStatus) {    case Processing:        // we are ok, the state machine continues correctly        break;    case Finished: {        // We are ok, switching to Idle if there is no potential next request planned.        // But the trouble is we must report a finished command if the previous command has just been finished        // i.e. only try to find some planned command if we just finished the Idle cycle        bool previousCommandFinished = currentScope == Scope::Command; // @@TODO this is a nasty hack :(        if (!ActivatePlannedRequest()) {                               // if nothing is planned, switch to Idle            SwitchToIdle();        } else {            // if the previous cycle was Idle and now we have planned a new command -> avoid returning Finished            if (!previousCommandFinished && currentScope == Scope::Command) {                currentStatus = Processing;            }        }    } break;    case CommandRejected:        // we have to repeat it - that's the only thing we can do        // no change in state        // @@TODO wait until Q0 returns command in progress finished, then we can send this one        LogError(PSTR("Command rejected"));        CommandRestart();        break;    case CommandError:        LogError(PSTR("Command Error"));        // we shall probably transfer into the Idle state and await further instructions from the upper layer        // Idle state may solve the problem of keeping up the heart beat running        break;    case VersionMismatch:        LogError(PSTR("Version mismatch"));        Stop(); // cannot continue        break;    case ProtocolError:        currentStatus = HandleProtocolError();        break;    case CommunicationTimeout:        currentStatus = HandleCommunicationTimeout();        break;    default:        break;    }    return currentStatus;}uint8_t ProtocolLogic::CommandInProgress() const {    if (currentScope != Scope::Command)        return 0;    return (uint8_t)ReqMsg().code;}bool DropOutFilter::Record(StepStatus ss) {    if (occurrences == maxOccurrences) {        cause = ss;    }    --occurrences;    return occurrences == 0;}} // namespace MMU2
 |