diff --git a/CMakeLists.txt b/CMakeLists.txt index 63758b3..cd70138 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,7 +186,7 @@ target_compile_options(firmware PRIVATE -Wdouble-promotion) # target_link_libraries( firmware PRIVATE A3idesHeaders ) -target_sources(firmware PRIVATE src/main.cpp src/hal/avr/cpu.cpp) +target_sources(firmware PRIVATE src/main.cpp src/hal/avr/cpu.cpp src/modules/protocol.cpp) set_property( SOURCE src/version.c diff --git a/src/modules/protocol.cpp b/src/modules/protocol.cpp new file mode 100644 index 0000000..048eb38 --- /dev/null +++ b/src/modules/protocol.cpp @@ -0,0 +1,221 @@ +#include "protocol.h" + +// protocol definition +// command: Q0 +// meaning: query operation status +// Query/command: query +// Expected reply from the MMU: +// any of the running operation statuses: OID: [T|L|U|E|C|W|K][0-4] +// P[0-9] : command being processed i.e. operation running, may contain a state number +// E[0-9][0-9] : error 1-9 while doing a tool change +// F : operation finished - will be repeated to "Q" messages until a new command is issued + +namespace modules { + +// decoding automaton +// states: input -> transition into state +// Code QTLMUXPSBEWK -> msgcode +// \n ->start +// * ->error +// error \n ->start +// * ->error +// msgcode 0-9 ->msgvalue +// * ->error +// msgvalue 0-9 ->msgvalue +// \n ->start successfully accepted command + +Protocol::DecodeStatus Protocol::DecodeRequest(uint8_t c) { + switch (rqState) { + case RequestStates::Code: + switch (c) { + case 'Q': + case 'T': + case 'L': + case 'M': + case 'U': + case 'X': + case 'P': + case 'S': + case 'B': + case 'E': + case 'W': + case 'K': + requestMsg.code = (RequestMsgCodes)c; + requestMsg.value = 0; + rqState = RequestStates::Value; + return DecodeStatus::NeedMoreData; + default: + rqState = RequestStates::Error; + return DecodeStatus::Error; + } + case RequestStates::Value: + if (c >= '0' && c <= '9') { + requestMsg.value *= 10; + requestMsg.value += c - '0'; + return DecodeStatus::NeedMoreData; + } else if (c == '\n') { + rqState = RequestStates::Code; + return DecodeStatus::MessageCompleted; + } else { + rqState = RequestStates::Error; + return DecodeStatus::Error; + } + default: //case error: + if (c == '\n') { + rqState = RequestStates::Code; + return DecodeStatus::MessageCompleted; + } else { + rqState = RequestStates::Error; + return DecodeStatus::Error; + } + } +} + +uint8_t Protocol::EncodeRequest(const RequestMsg &msg, uint8_t *txbuff) { + txbuff[0] = (uint8_t)msg.code; + txbuff[1] = msg.value + '0'; + txbuff[2] = '\n'; + return 3; +} + +Protocol::DecodeStatus Protocol::DecodeResponse(uint8_t c) { + switch (rspState) { + case ResponseStates::RequestCode: + switch (c) { + case 'Q': + case 'T': + case 'L': + case 'M': + case 'U': + case 'X': + case 'P': + case 'S': + case 'B': + case 'E': + case 'W': + case 'K': + responseMsg.request.code = (RequestMsgCodes)c; + responseMsg.request.value = 0; + rspState = ResponseStates::RequestValue; + return DecodeStatus::NeedMoreData; + default: + rspState = ResponseStates::Error; + return DecodeStatus::Error; + } + case ResponseStates::RequestValue: + if (c >= '0' && c <= '9') { + responseMsg.request.value *= 10; + responseMsg.request.value += c - '0'; + return DecodeStatus::NeedMoreData; + } else if (c == ' ') { + rspState = ResponseStates::Space; + return DecodeStatus::NeedMoreData; + } else { + rspState = ResponseStates::Error; + return DecodeStatus::Error; + } + case ResponseStates::Space: + if (c == ' ') { + rspState = ResponseStates::ParamCode; + return DecodeStatus::NeedMoreData; + } else { + return DecodeStatus::Error; + } + case ResponseStates::ParamCode: + switch (c) { + case 'P': + case 'E': + case 'F': + case 'A': + case 'R': + rspState = ResponseStates::ParamValue; + responseMsg.params.value = 0; + return DecodeStatus::NeedMoreData; + default: + rspState = ResponseStates::Error; + return DecodeStatus::Error; + } + case ResponseStates::ParamValue: + if (c >= '0' && c <= '9') { + responseMsg.params.value *= 10; + responseMsg.params.value += c - '0'; + return DecodeStatus::NeedMoreData; + } else if (c == '\n') { + rspState = ResponseStates::RequestCode; + return DecodeStatus::MessageCompleted; + } else { + rspState = ResponseStates::Error; + return DecodeStatus::Error; + } + default: //case error: + if (c == '\n') { + rspState = ResponseStates::RequestCode; + return DecodeStatus::MessageCompleted; + } else { + return DecodeStatus::Error; + } + } +} + +uint8_t Protocol::EncodeResponseCmdAR(const RequestMsg &msg, ResponseMsgParamCodes ar, uint8_t *txbuff) { + txbuff[0] = (uint8_t)msg.code; + txbuff[1] = msg.value + '0'; + txbuff[2] = ' '; + txbuff[3] = (uint8_t)ar; + txbuff[4] = '\n'; + return 5; +} + +uint8_t Protocol::EncodeResponseReadFINDA(const RequestMsg &msg, uint8_t findaValue, uint8_t *txbuff) { + txbuff[0] = (uint8_t)msg.code; + txbuff[1] = msg.value + '0'; + txbuff[2] = ' '; + txbuff[3] = (uint8_t)ResponseMsgParamCodes::Accepted; + txbuff[4] = findaValue + '0'; + txbuff[5] = '\n'; + return 6; +} + +uint8_t Protocol::EncodeResponseVersion(const RequestMsg &msg, uint8_t value, uint8_t *txbuff) { + txbuff[0] = (uint8_t)msg.code; + txbuff[1] = msg.value + '0'; + txbuff[2] = ' '; + txbuff[3] = (uint8_t)ResponseMsgParamCodes::Accepted; + uint8_t *dst = txbuff + 4; + if (value < 10) { + *dst++ = value + '0'; + } else if (value < 100) { + *dst++ = value / 10 + '0'; + *dst++ = value % 10 + '0'; + } else { + *dst++ = value / 100 + '0'; + *dst++ = (value / 10) % 10 + '0'; + *dst++ = value % 10 + '0'; + } + *dst = '\n'; + return dst - txbuff + 1; +} + +uint8_t Protocol::EncodeResponseQueryOperation(const RequestMsg &msg, ResponseMsgParamCodes code, uint8_t value, uint8_t *txbuff) { + txbuff[0] = (uint8_t)msg.code; + txbuff[1] = msg.value + '0'; + txbuff[2] = ' '; + txbuff[3] = (uint8_t)code; + uint8_t *dst = txbuff + 4; + if (code != ResponseMsgParamCodes::Finished) { + if (value < 10) { + *dst++ = value + '0'; + } else if (value < 100) { + *dst++ = value / 10 + '0'; + *dst++ = value % 10 + '0'; + } else { + *dst++ = value / 100 + '0'; + *dst++ = (value / 10) % 10 + '0'; + *dst++ = value % 10 + '0'; + } + } + *dst = '\n'; + return dst - txbuff + 1; +} + +} // namespace modules diff --git a/src/modules/protocol.h b/src/modules/protocol.h index 9a9ed6e..d8ef63d 100644 --- a/src/modules/protocol.h +++ b/src/modules/protocol.h @@ -1,27 +1,139 @@ #pragma once +#include + /// MMU protocol implementation /// See description of the new protocol in the MMU 2021 doc /// @@TODO possibly add some checksum to verify the correctness of messages namespace modules { -/// @@TODO define/improve this simple message structure that fits our domain +enum class RequestMsgCodes : uint8_t { + unknown = 0, + Query = 'Q', + Tool = 'T', + Load = 'L', + Mode = 'M', + Unload = 'U', + Reset = 'X', + Finda = 'P', + Version = 'S', + Button = 'B', + Eject = 'E', + Wait = 'W', + Cut = 'K' +}; + +enum class ResponseMsgParamCodes : uint8_t { + unknown = 0, + Processing = 'P', + Error = 'E', + Finished = 'F', + Accepted = 'A', + Rejected = 'R' +}; + +/// A generic request message struct Msg { - uint8_t code; + RequestMsgCodes code; uint8_t value; + inline Msg(RequestMsgCodes code, uint8_t value) + : code(code) + , value(value) {} +}; + +/// A request message +/// Requests are being sent by the printer into the MMU +/// It is the same structure as the generic Msg +using RequestMsg = Msg; + +/// A response message +/// Responses are being sent from the MMU into the printer as a response to a request message +struct ResponseMsg { + RequestMsg request; ///< response is always preceeded by the request message + Msg params; ///< parameters of reply + inline ResponseMsg(RequestMsg request, ResponseMsgParamCodes paramCode, uint8_t paramValue) + : request(request) + , params((RequestMsgCodes)paramCode, paramValue) {} }; /// Protocol class is responsible for creating/decoding messages in Rx/Tx buffer +/// Beware - in the decoding more, it is meant to be a statefull instance which works through public methods +/// processing one input byte per call class Protocol { - public: - /// Decodes message in rxbuff - /// @returns decoded message structure - Msg Decode(const uint8_t *rxbuff); - /// Encodes message msg into txbuff memory + /// Message decoding return value + enum class DecodeStatus : uint8_t { + MessageCompleted, ///< message completed and successfully lexed + NeedMoreData, ///< message incomplete yet, waiting for another byte to come + Error, ///< input character broke message decoding + }; + + inline Protocol() + : rqState(RequestStates::Code) + , requestMsg(RequestMsgCodes::unknown, 0) + , rspState(ResponseStates::RequestCode) + , responseMsg(RequestMsg(RequestMsgCodes::unknown, 0), ResponseMsgParamCodes::unknown, 0) { + } + + /// Takes the input byte c and steps one step through the state machine + /// @returns state of the message being decoded + DecodeStatus DecodeRequest(uint8_t c); + + /// Decodes response message in rxbuff + /// @returns decoded response message structure + DecodeStatus DecodeResponse(uint8_t c); + + /// Encodes request message msg into txbuff memory /// It is expected the txbuff is large enough to fit the message - void Encode(uint8_t *txbuff, const Msg &msg); + /// @returns number of bytes written into txbuff + static uint8_t EncodeRequest(const RequestMsg &msg, uint8_t *txbuff); + + /// Encode generic response Command Accepted or Rejected + /// @param msg source request message for this response + /// @returns number of bytes written into txbuff + static uint8_t EncodeResponseCmdAR(const RequestMsg &msg, ResponseMsgParamCodes ar, uint8_t *txbuff); + + /// Encode response to Read FINDA query + /// @param msg source request message for this response + /// @param findaValue 1/0 (on/off) status of FINDA + /// @returns number of bytes written into txbuff + static uint8_t EncodeResponseReadFINDA(const RequestMsg &msg, uint8_t findaValue, uint8_t *txbuff); + + /// Encode response to Version query + /// @param msg source request message for this response + /// @param value version number (0-255) + /// @returns number of bytes written into txbuff + static uint8_t EncodeResponseVersion(const RequestMsg &msg, uint8_t value, uint8_t *txbuff); + + /// Encode response to Query operation status + /// @param msg source request message for this response + /// @param code status of operation (Processing, Error, Finished) + /// @param value related to status of operation(e.g. error code or progress) + /// @returns number of bytes written into txbuff + static uint8_t EncodeResponseQueryOperation(const RequestMsg &msg, ResponseMsgParamCodes code, uint8_t value, uint8_t *txbuff); + +private: + enum class RequestStates : uint8_t { + Code, ///< starting state - expects message code + Value, ///< expecting code value + Error ///< automaton in error state + }; + + RequestStates rqState; + RequestMsg requestMsg; + + enum class ResponseStates : uint8_t { + RequestCode, ///< starting state - expects message code + RequestValue, ///< expecting code value + Space, ///< expecting space + ParamCode, ///< expecting param code + ParamValue, ///< expecting param value + Error ///< automaton in error state + }; + + ResponseStates rspState; + ResponseMsg responseMsg; }; } // namespace modules diff --git a/tests/unit/modules/protocol/CMakeLists.txt b/tests/unit/modules/protocol/CMakeLists.txt index fbaaf53..75344ba 100644 --- a/tests/unit/modules/protocol/CMakeLists.txt +++ b/tests/unit/modules/protocol/CMakeLists.txt @@ -1,5 +1,5 @@ # define the test executable -add_executable(protocol_tests test_protocol.cpp) +add_executable(protocol_tests ../../../../src/modules/protocol.cpp test_protocol.cpp) # define required search paths target_include_directories(protocol_tests PUBLIC ${CMAKE_SOURCE_DIR}/src/modules) diff --git a/tests/unit/modules/protocol/test_protocol.cpp b/tests/unit/modules/protocol/test_protocol.cpp index df686bd..3da6aaa 100644 --- a/tests/unit/modules/protocol/test_protocol.cpp +++ b/tests/unit/modules/protocol/test_protocol.cpp @@ -3,6 +3,200 @@ using Catch::Matchers::Equals; -TEST_CASE("protocol::1", "[protocol]") { - CHECK(true); +TEST_CASE("protocol::EncodeRequests", "[protocol]") { + using namespace modules; + + RequestMsgCodes code; + uint8_t value; + std::tie(code, value) = GENERATE( + std::make_tuple(RequestMsgCodes::Button, 0), + std::make_tuple(RequestMsgCodes::Button, 1), + std::make_tuple(RequestMsgCodes::Button, 2), + std::make_tuple(RequestMsgCodes::Cut, 0), + std::make_tuple(RequestMsgCodes::Eject, 0), + std::make_tuple(RequestMsgCodes::Finda, 0), + std::make_tuple(RequestMsgCodes::Load, 0), + std::make_tuple(RequestMsgCodes::Load, 1), + std::make_tuple(RequestMsgCodes::Load, 2), + std::make_tuple(RequestMsgCodes::Load, 3), + std::make_tuple(RequestMsgCodes::Load, 4), + std::make_tuple(RequestMsgCodes::Mode, 0), + std::make_tuple(RequestMsgCodes::Mode, 1), + std::make_tuple(RequestMsgCodes::Query, 0), + std::make_tuple(RequestMsgCodes::Reset, 0), + std::make_tuple(RequestMsgCodes::Tool, 0), + std::make_tuple(RequestMsgCodes::Tool, 1), + std::make_tuple(RequestMsgCodes::Tool, 2), + std::make_tuple(RequestMsgCodes::Tool, 3), + std::make_tuple(RequestMsgCodes::Tool, 4), + std::make_tuple(RequestMsgCodes::Unload, 0), + std::make_tuple(RequestMsgCodes::Version, 0), + std::make_tuple(RequestMsgCodes::Version, 1), + std::make_tuple(RequestMsgCodes::Version, 2), + std::make_tuple(RequestMsgCodes::Wait, 0), + std::make_tuple(RequestMsgCodes::unknown, 0)); + + std::array txbuff; + + CHECK(Protocol::EncodeRequest(RequestMsg(code, value), txbuff.data()) == 3); + CHECK(txbuff[0] == (uint8_t)code); + CHECK(txbuff[1] == value + '0'); + CHECK(txbuff[2] == '\n'); +} + +TEST_CASE("protocol::EncodeResponseCmdAR", "[protocol]") { + using namespace modules; + + auto requestMsg = GENERATE( + RequestMsg(RequestMsgCodes::Button, 0), + RequestMsg(RequestMsgCodes::Button, 1), + RequestMsg(RequestMsgCodes::Button, 2), + + RequestMsg(RequestMsgCodes::Cut, 0), + + RequestMsg(RequestMsgCodes::Eject, 0), + RequestMsg(RequestMsgCodes::Eject, 1), + RequestMsg(RequestMsgCodes::Eject, 2), + RequestMsg(RequestMsgCodes::Eject, 3), + RequestMsg(RequestMsgCodes::Eject, 4), + + RequestMsg(RequestMsgCodes::Load, 0), + RequestMsg(RequestMsgCodes::Load, 1), + RequestMsg(RequestMsgCodes::Load, 2), + RequestMsg(RequestMsgCodes::Load, 3), + RequestMsg(RequestMsgCodes::Load, 4), + + RequestMsg(RequestMsgCodes::Mode, 0), + RequestMsg(RequestMsgCodes::Mode, 1), + + RequestMsg(RequestMsgCodes::Tool, 0), + RequestMsg(RequestMsgCodes::Tool, 1), + RequestMsg(RequestMsgCodes::Tool, 2), + RequestMsg(RequestMsgCodes::Tool, 3), + RequestMsg(RequestMsgCodes::Tool, 4), + + RequestMsg(RequestMsgCodes::Unload, 0), + + RequestMsg(RequestMsgCodes::Wait, 0)); + + auto responseStatus = GENERATE(ResponseMsgParamCodes::Accepted, ResponseMsgParamCodes::Rejected); + + std::array txbuff; + uint8_t msglen = Protocol::EncodeResponseCmdAR(requestMsg, responseStatus, txbuff.data()); + + CHECK(msglen == 5); + CHECK(txbuff[0] == (uint8_t)requestMsg.code); + CHECK(txbuff[1] == requestMsg.value + '0'); + CHECK(txbuff[2] == ' '); + CHECK(txbuff[3] == (uint8_t)responseStatus); + CHECK(txbuff[4] == '\n'); +} + +TEST_CASE("protocol::EncodeResponseReadFINDA", "[protocol]") { + using namespace modules; + auto requestMsg = RequestMsg(RequestMsgCodes::Finda, 0); + + uint8_t findaStatus = GENERATE(0, 1); + + std::array txbuff; + uint8_t msglen = Protocol::EncodeResponseReadFINDA(requestMsg, findaStatus, txbuff.data()); + + CHECK(msglen == 6); + CHECK(txbuff[0] == (uint8_t)requestMsg.code); + CHECK(txbuff[1] == requestMsg.value + '0'); + CHECK(txbuff[2] == ' '); + CHECK(txbuff[3] == (uint8_t)ResponseMsgParamCodes::Accepted); + CHECK(txbuff[4] == findaStatus + '0'); + CHECK(txbuff[5] == '\n'); +} + +TEST_CASE("protocol::EncodeResponseVersion", "[protocol]") { + using namespace modules; + + std::uint8_t versionQueryType = GENERATE(0, 1, 2, 3); + auto requestMsg = RequestMsg(RequestMsgCodes::Version, versionQueryType); + + auto version = GENERATE(0, 1, 2, 3, 4, 10, 11, 12, 20, 99, 100, 101, 255); + + std::array txbuff; + uint8_t msglen = Protocol::EncodeResponseVersion(requestMsg, version, txbuff.data()); + + CHECK(msglen <= 8); + CHECK(txbuff[0] == (uint8_t)requestMsg.code); + CHECK(txbuff[1] == requestMsg.value + '0'); + CHECK(txbuff[2] == ' '); + CHECK(txbuff[3] == (uint8_t)ResponseMsgParamCodes::Accepted); + + if (version < 10) { + CHECK(txbuff[4] == version + '0'); + } else if (version < 100) { + CHECK(txbuff[4] == version / 10 + '0'); + CHECK(txbuff[5] == version % 10 + '0'); + } else { + CHECK(txbuff[4] == version / 100 + '0'); + CHECK(txbuff[5] == (version / 10) % 10 + '0'); + CHECK(txbuff[6] == version % 10 + '0'); + } + + CHECK(txbuff[msglen - 1] == '\n'); +} + +TEST_CASE("protocol::EncodeResponseQueryOperation", "[protocol]") { + using namespace modules; + + auto requestMsg = GENERATE( + RequestMsg(RequestMsgCodes::Cut, 0), + + RequestMsg(RequestMsgCodes::Eject, 0), + RequestMsg(RequestMsgCodes::Eject, 1), + RequestMsg(RequestMsgCodes::Eject, 2), + RequestMsg(RequestMsgCodes::Eject, 3), + RequestMsg(RequestMsgCodes::Eject, 4), + + RequestMsg(RequestMsgCodes::Load, 0), + RequestMsg(RequestMsgCodes::Load, 1), + RequestMsg(RequestMsgCodes::Load, 2), + RequestMsg(RequestMsgCodes::Load, 3), + RequestMsg(RequestMsgCodes::Load, 4), + + RequestMsg(RequestMsgCodes::Tool, 0), + RequestMsg(RequestMsgCodes::Tool, 1), + RequestMsg(RequestMsgCodes::Tool, 2), + RequestMsg(RequestMsgCodes::Tool, 3), + RequestMsg(RequestMsgCodes::Tool, 4), + + RequestMsg(RequestMsgCodes::Unload, 0), + + RequestMsg(RequestMsgCodes::Wait, 0)); + + auto responseStatus = GENERATE(ResponseMsgParamCodes::Processing, ResponseMsgParamCodes::Error, ResponseMsgParamCodes::Finished); + + auto value = GENERATE(0, 1, 2, 3, 10, 11, 99, 100, 101, 102, 200, 255); + + std::array txbuff; + uint8_t msglen = Protocol::EncodeResponseQueryOperation(requestMsg, responseStatus, value, txbuff.data()); + + CHECK(msglen <= 8); + CHECK(txbuff[0] == (uint8_t)requestMsg.code); + CHECK(txbuff[1] == requestMsg.value + '0'); + CHECK(txbuff[2] == ' '); + CHECK(txbuff[3] == (uint8_t)responseStatus); + + if (responseStatus == ResponseMsgParamCodes::Finished) { + CHECK(txbuff[4] == '\n'); + CHECK(msglen == 5); + } else { + if (value < 10) { + CHECK(txbuff[4] == value + '0'); + } else if (value < 100) { + CHECK(txbuff[4] == value / 10 + '0'); + CHECK(txbuff[5] == value % 10 + '0'); + } else { + CHECK(txbuff[4] == value / 100 + '0'); + CHECK(txbuff[5] == (value / 10) % 10 + '0'); + CHECK(txbuff[6] == value % 10 + '0'); + } + + CHECK(txbuff[msglen - 1] == '\n'); + } }