~aleteoryx/muditaos

dc00afaad3fc80328fca1867eb1c793eb4f31866 — breichel 5 years ago 28d811b
[EGD-5599] Change AT and DLC channels for big return

Change which allow to handle large return from
at commands both via UART and DLC-Commands
M enabled_unittests => enabled_unittests +4 -0
@@ 222,6 222,10 @@ TESTS_LIST["catch2-unittest_parse_CSCA"]="
    CSCA set data;
"
#---------
TESTS_LIST["catch2-unittest_ATStream"]="
    Channel Test- AT return parser;
"
#---------
TESTS_LIST["catch2-utils"]="
    Split tests;
    toNumeric tests;

M module-cellular/CMakeLists.txt => module-cellular/CMakeLists.txt +1 -0
@@ 7,6 7,7 @@ include(SerialPort)

set(SOURCES
        ${CMAKE_CURRENT_SOURCE_DIR}/Modem/ATParser.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/Modem/ATStream.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/Modem/ATCommon.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/Modem/TS0710/DLC_channel.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/Modem/TS0710/TS0710.cpp

M module-cellular/Modem/ATCommon.cpp => module-cellular/Modem/ATCommon.cpp +11 -76
@@ 10,6 10,7 @@
#include <vector>
#include <inttypes.h> // for PRIu32
#include <Utils.hpp>
#include "ATStream.hpp"

using namespace at;



@@ 24,37 25,6 @@ const std::string Channel::CMS_ERROR  = "+CMS ERROR:";
// const std::string Channel::RING = "RING";
// const std::string Channel::NO_DIALTONE = "NO DIALTONE";

Result::Code Channel::at_check(const std::vector<std::string> &arr)
{
    if (arr.size()) {
        for (auto el : arr) {
            if (el.compare(0, OK.length(), OK) == 0) {
                return Result::Code::OK;
            }
            else if (el.compare(0, ERROR.length(), ERROR) == 0) {
                return Result::Code::ERROR;
            }
        }
    }
    return Result::Code::NONE;
}

bool Channel::at_check_cmx_error(const std::string &CMX, const std::vector<std::string> &arr, uint32_t &errcode)
{
    if (arr.size()) {
        for (auto cmxerr : arr) {
            if (cmxerr.compare(0, CMX.length(), CMX) == 0) {
                auto serr     = utils::trim(cmxerr.substr(CMX.length(), cmxerr.length() - CMX.length()));
                int parsedVal = 0;
                auto ret      = utils::toNumeric(serr, parsedVal);
                errcode       = parsedVal;
                return ret;
            }
        }
    }
    return false;
}

void Channel::cmd_log(std::string cmd, const Result &result, uint32_t timeout)
{
    cmd.erase(std::remove(cmd.begin(), cmd.end(), '\r'), cmd.end());


@@ 97,64 67,29 @@ Result Channel::cmd(const std::string &cmd, std::chrono::milliseconds timeout, s

    cmd_init();
    std::string cmdFixed = formatCommand(cmd);

    cmd_send(cmdFixed);

    uint32_t currentTime   = cpp_freertos::Ticks::GetTicks();
    uint32_t timeoutNeeded = ((timeout.count() == UINT32_MAX) ? UINT32_MAX : currentTime + timeout.count());
    uint32_t timeElapsed   = currentTime;

    ATStream atStream(rxCount);

    while (true) {
        if (timeoutNeeded != UINT32_MAX && timeElapsed >= timeoutNeeded) {
            result.code = Result::Code::TIMEOUT;
            break;
        }

        auto ret    = ulTaskNotifyTake(pdTRUE, timeoutNeeded - timeElapsed);
        timeElapsed = cpp_freertos::Ticks::GetTicks();
        if (ret != 0u) {
            std::vector<std::string> ret = cmd_receive();

            result.response.insert(std::end(result.response), std::begin(ret), std::end(ret));

            uint32_t errcode = 0;
            if (at_check_cmx_error(CME_ERROR, ret, errcode)) {
                result.code =
                    Result::Code::ERROR; // setup error but in this case error from +CME ERROR with valid errorCode
                auto tmp_ec = magic_enum::enum_cast<EquipmentErrorCode>(errcode);
                if (tmp_ec.has_value()) {
                    LOG_ERROR("%s", utils::enumToString(tmp_ec.value()).c_str());
                    result.errorCode = tmp_ec.value();
                }
                else {
                    LOG_ERROR("Unknow CME error code %" PRIu32, errcode);
                    result.errorCode = at::EquipmentErrorCode::Unknown;
                }
                break;
            }

            if (at_check_cmx_error(CMS_ERROR, ret, errcode)) {
                result.code =
                    Result::Code::ERROR; // setup error but in this case error from +CME ERROR with valid errorCode
        auto taskUnlocked = ulTaskNotifyTake(pdTRUE, timeoutNeeded - timeElapsed);
        timeElapsed       = cpp_freertos::Ticks::GetTicks();

                auto atmp_ec = magic_enum::enum_cast<NetworkErrorCode>(errcode);
        if (taskUnlocked) {
            atStream.write(cmd_receive());

                if (atmp_ec.has_value()) {
                    LOG_ERROR("%s", utils::enumToString(atmp_ec.value()).c_str());
                    result.errorCode = atmp_ec.value();
                }
                else {
                    LOG_ERROR("Unknow CMS error code %" PRIu32, errcode);
                    result.errorCode = at::NetworkErrorCode::Unknown;
                }
                break;
            }

            result.code = at_check(ret);
            if (result.code != Result::Code::NONE) {
                break;
            }
            if (rxCount != 0 && result.response.size() >= rxCount) {
                result.code = Result::Code::TOKENS;
            if (atStream.isReady()) {
                result = atStream.getResult();
                break;
            }
        }


@@ 175,7 110,7 @@ auto Channel::cmd(const at::Cmd &at) -> Result

auto Channel::cmd(const at::AT &at) -> Result
{
    auto cmd = at::factory(at);
    auto cmd  = at::factory(at);
    auto time = utils::time::Scoped("Time to run at command" + cmd.getCmd());
    return this->cmd(cmd);
}

M module-cellular/Modem/ATCommon.hpp => module-cellular/Modem/ATCommon.hpp +2 -4
@@ 16,6 16,8 @@ namespace sys

namespace at
{
    constexpr auto delimiter = "\r\n"; /// use std::strlen()

    class Channel : public BaseChannel
    {
      protected:


@@ 45,10 47,6 @@ namespace at
                         size_t rxCount                    = 0) -> Result final;
        virtual auto cmd(const at::AT &at) -> Result final;
        virtual auto cmd(const at::Cmd &at) -> Result final;
        /// check for OK, ERROR in string in last token
        virtual Result::Code at_check(const std::vector<std::string> &arr);
        /// check for +CME ERROR: errors if exists in last token
        virtual bool at_check_cmx_error(const std::string &CMX, const std::vector<std::string> &arr, uint32_t &errcode);
        virtual void cmd_log(std::string cmd, const Result &result, uint32_t timeout) final;
        virtual auto ProcessNewData(sys::Service *service) -> int
        {

M module-cellular/Modem/ATParser.cpp => module-cellular/Modem/ATParser.cpp +2 -2
@@ 113,10 113,10 @@ void ATParser::cmd_send(std::string cmd)
    cellular->Write(const_cast<char *>(cmd.c_str()), cmd.size());
}

std::vector<std::string> ATParser::cmd_receive()
std::string ATParser::cmd_receive()
{
    cpp_freertos::LockGuard lock(mutex);
    return utils::split(responseBuffer, "\r\n");
    return responseBuffer;
}

void ATParser::cmd_post()

M module-cellular/Modem/ATParser.hpp => module-cellular/Modem/ATParser.hpp +1 -1
@@ 38,7 38,7 @@ class ATParser : public at::Channel

    virtual void cmd_init() override final;
    virtual void cmd_send(std::string cmd) override final;
    virtual std::vector<std::string> cmd_receive() override final;
    virtual std::string cmd_receive() override final;
    virtual void cmd_post() override final;

  private:

A module-cellular/Modem/ATStream.cpp => module-cellular/Modem/ATStream.cpp +214 -0
@@ 0,0 1,214 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ATStream.hpp"

#include "ATCommon.hpp"
namespace at
{

    Result::Code ATStream::parseState(const std::string &state, uint32_t &errcode)
    {
        const std::map<std::string, Result::Code> convert_map = {
            {Channel::OK, Result::Code::OK},
            {Channel::ERROR, Result::Code::ERROR},
            {Channel::CME_ERROR, Result::Code::CME_ERROR},
            {Channel::CMS_ERROR, Result::Code::CMS_ERROR},

        };

        auto it = convert_map.find(state);
        if (it != convert_map.end()) {
            if ((it->second == Result::Code::CME_ERROR) || (it->second == Result::Code::CMS_ERROR)) {
                return Result::Code::PARSING_ERROR;
            }
            return it->second;
        }

        auto cmxErrorTest = state.substr(0, Channel::CME_ERROR.length()); /// same length for CMS

        it = convert_map.find(cmxErrorTest);
        if ((it != convert_map.end()) &&
            ((it->second == Result::Code::CME_ERROR) || (it->second == Result::Code::CMS_ERROR))) {
            auto serr =
                utils::trim(state.substr(Channel::CME_ERROR.length(), state.length() - Channel::CME_ERROR.length()));
            if (serr.length() == 0) {
                return Result::Code::PARSING_ERROR;
            }
            int parsedVal = 0;
            auto ret      = utils::toNumeric(serr, parsedVal);
            if (!ret) {
                return Result::Code::PARSING_ERROR;
            }
            errcode = parsedVal;
            return it->second;
        }

        return Result::Code::NONE;
    }

    void ATStream::checkError()
    {
        if (result.code == Result::Code::CME_ERROR) {
            result.code =
                Result::Code::ERROR; // setup error but in this case error from +CME ERROR with valid errorCode
            auto tmp_ec = magic_enum::enum_cast<EquipmentErrorCode>(errcode);
            if (tmp_ec.has_value()) {
                LOG_ERROR("%s", utils::enumToString(tmp_ec.value()).c_str());
                result.errorCode = tmp_ec.value();
            }
            else {
                LOG_ERROR("Unknow CME error code %" PRIu32, errcode);
                result.errorCode = at::EquipmentErrorCode::Unknown;
            }
        }

        if (result.code == Result::Code::CMS_ERROR) {
            result.code =
                Result::Code::ERROR; // setup error but in this case error from +CME ERROR with valid errorCode

            auto atmp_ec = magic_enum::enum_cast<NetworkErrorCode>(errcode);

            if (atmp_ec.has_value()) {
                LOG_ERROR("%s", utils::enumToString(atmp_ec.value()).c_str());
                result.errorCode = atmp_ec.value();
            }
            else {
                LOG_ERROR("Unknow CMS error code %" PRIu32, errcode);
                result.errorCode = at::NetworkErrorCode::Unknown;
            }
        }
    }

    bool ATStream::checkATBegin()
    {
        auto pos = atBuffer.find(at::delimiter, std::strlen(at::delimiter));
        if (pos != std::string::npos) {
            beginChecked   = true;
            std::string rr = atBuffer.substr(std::strlen(at::delimiter), pos - std::strlen(at::delimiter)).c_str();
            auto code      = parseState(rr, errcode);
            if (code != Result::Code::NONE) {
                result.code = code;
                return true;
            }
        }
        return false;
    }

    bool ATStream::checkATEnd()
    {
        auto pos = atBuffer.rfind(at::delimiter);
        if (pos != std::string::npos) {
            auto pos2 = atBuffer.rfind(at::delimiter, pos - std::strlen(at::delimiter));
            if (pos2 != std::string::npos) {
                std::string rr =
                    atBuffer.substr(pos2 + std::strlen(at::delimiter), pos - pos2 - std::strlen(at::delimiter)).c_str();
                auto code = parseState(rr, errcode);
                if (code != Result::Code::NONE) {
                    result.code = code;
                    return true;
                }
            }
        }

        return false;
    }

    void ATStream::countLines()
    {
        if (rxCount != 0) {
            auto pos = atBuffer.find(at::delimiter, lastPos);
            while (pos != std::string::npos) {
                /// do not count empty lines, see implementation of utils:split
                if ((lastPos) != pos) {
                    lineCounter++;
                }
                lastPos = pos + std::strlen(at::delimiter);
                pos     = atBuffer.find(at::delimiter, lastPos);
            }
        }
    }

    bool ATStream::parse()
    {
        /// check for return at the begin eg <cr<lf>X<cr><lf>
        if ((!beginChecked) && (atBuffer.length() > minATCmdRet) && (atBuffer[0] == '\r') && (atBuffer[1] == '\n')) {
            if (checkATBegin()) {
                atAtTheBegin = true;
                /**
                 * This is a compromise between the certainty of getting all the elements and the waiting time.
                 * In case if we always waited, the waiting time would be equal timeout.
                 * In this case, we wait for a specified number of commands,
                 * the wrong one may result in returning incorrect / incomplete values.
                 * It only applies to a group of specific commands like AT + QPING, in which the result (OK)
                 * is returned at the beginning, followed by information. This could happen only if we not fit
                 * in one return of cmd_receive return.
                 */
                if (rxCount == 0) {
                    return true;
                }
            }
        }
        countLines();
        /**
         * Check for pattern <cr><lf>XXX<cr><lf> at the end or check counted lines
         * XXX must be one of AT return codes
         */
        if (atAtTheBegin) {
            if (rxCount <= (lineCounter)) {
                return true;
            }
        }
        else {
            if (checkATEnd()) {
                return true;
            }
        }

        return false;
    }

    /**
     *
     * @param buffer
     * @return false if could not write (mean ready). Reset to write again
     */
    bool ATStream::write(const std::string &buffer)
    {
        if (isATReady) {
            return false;
        }
        atBuffer += buffer;
        if (parse()) {
            isATReady = true;

            auto fullRet = utils::split(atBuffer, at::delimiter);
            result.response.insert(std::end(result.response), std::begin(fullRet), std::end(fullRet));

            checkError();

            if (rxCount != 0 && result.response.size() > rxCount) {
                result.code = Result::Code::TOKENS;
            }
            return false;
        }
        else {

            return true;
        }
    }

    void ATStream::reset()
    {
        result.code = Result::Code::NONE;
        result.response.clear();
        result.errorCode = at::EquipmentErrorCode::NoInformation;
        atAtTheBegin     = false;
        beginChecked     = false;
        lastPos          = 0;
        lineCounter      = 0;
        atBuffer         = {};
        isATReady        = false;
        errcode          = 0;
    }
} // namespace at
\ No newline at end of file

A module-cellular/Modem/ATStream.hpp => module-cellular/Modem/ATStream.hpp +52 -0
@@ 0,0 1,52 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <at/ErrorCode.hpp>
#include <at/Result.hpp>

namespace at
{
    constexpr auto minATCmdRet = 5; /// minimum return from AT in command mode "<cr<lf>X<cr><lf>"

    class ATStream
    {

      private:
        at::Result result;
        bool atAtTheBegin    = false; /// case for commands like QPING
        bool beginChecked    = false;
        size_t lastPos       = 0;
        size_t lineCounter   = 0;
        size_t rxCount       = 0;
        std::string atBuffer = {};
        bool isATReady       = false;
        uint32_t errcode     = 0;

      protected:
        bool checkATBegin();
        bool checkATEnd();
        void checkError();
        bool parse();
        void countLines();

      public:
        ATStream(size_t rxCount = 0) : rxCount(rxCount)
        {}

        bool write(const std::string &buffer);
        bool isReady()
        {
            return isATReady;
        }
        void reset();
        at::Result getResult()
        {
            return result;
        }

        Result::Code parseState(const std::string &state, uint32_t &errcode);
    };

} // namespace at

M module-cellular/Modem/BaseChannel.hpp => module-cellular/Modem/BaseChannel.hpp +1 -1
@@ 30,7 30,7 @@ namespace at
        {}
        virtual void cmd_init()                        = 0;
        virtual void cmd_send(std::string cmd)         = 0;
        virtual std::vector<std::string> cmd_receive() = 0;
        virtual std::string cmd_receive()              = 0;
        virtual void cmd_post()                        = 0;
        /// }
    };

M module-cellular/Modem/TS0710/DLC_channel.cpp => module-cellular/Modem/TS0710/DLC_channel.cpp +2 -2
@@ 99,7 99,7 @@ void DLC_channel::cmd_send(std::string cmd)
    SendData(data);
}

std::vector<std::string> DLC_channel::cmd_receive()
std::string DLC_channel::cmd_receive()
{
    cpp_freertos::LockGuard lock(mutex);
    TS0710_Frame::frame_t frame;


@@ 128,7 128,7 @@ std::vector<std::string> DLC_channel::cmd_receive()
        deserialisedData += str;
    }
    mFrames.clear();
    return utils::split(deserialisedData, "\r\n");
    return deserialisedData;
}

void DLC_channel::cmd_post()

M module-cellular/Modem/TS0710/DLC_channel.h => module-cellular/Modem/TS0710/DLC_channel.h +1 -1
@@ 69,7 69,7 @@ class DLC_channel : public at::Channel

    virtual void cmd_init() override final;
    virtual void cmd_send(std::string cmd) override final;
    virtual std::vector<std::string> cmd_receive() override final;
    virtual std::string cmd_receive() override final;
    virtual void cmd_post() override final;

    std::vector<std::string> SendCommandPrompt(const char *cmd, size_t rxCount, uint32_t timeout = 300);

A module-cellular/Modem/doc/ATStream.md => module-cellular/Modem/doc/ATStream.md +36 -0
@@ 0,0 1,36 @@
![alt text](./Images/mudita_logo.png "MUDITA")

# Channel implementation

## Table of contents
1. [History](#history)
2. [Scope](#scope)
3. [Current implementation](#currentimpl)


## History <a name="history"></a>

| Authors           | Change description        | Status | Modification date |
| ----------------- | ------------------------- | ------ | ----------------- |
| Bartosz Reichel | Initial version           | Draft  | 2021.02.10        |


## Scope <a name="scope"></a>
Description of dependencies for channel and ATStream usage 



## Current implementation <a name="currentimpl"></a>


![alt text](./Images/class_channel.png "Class diagram for channel")

When initializing the modem, communication is direct via the UART channel through the ATParser channel (AT commands). 
At CMUX configuration, the channel for AT commands is represented by DLC_Channel (see Channel::Commands and also 
Channel::Notifications and Channel::Data).

Both ATParser and DLC_Channel use ATStream (in Channel) for parsing AT command. ATStream parse AT commands in terms of correctness /return (not the selected command) 

below way of how ATStream parse command (especially such strange like QPING)

![alt text](./Images/atstream.png "ATStream write flow")
\ No newline at end of file

A module-cellular/Modem/doc/Images/atstream.png => module-cellular/Modem/doc/Images/atstream.png +0 -0
A module-cellular/Modem/doc/Images/class_channel.png => module-cellular/Modem/doc/Images/class_channel.png +0 -0
A module-cellular/Modem/doc/Images/current_volte_on.png => module-cellular/Modem/doc/Images/current_volte_on.png +0 -0
A module-cellular/Modem/doc/Images/mudita_logo.png => module-cellular/Modem/doc/Images/mudita_logo.png +0 -0
A module-cellular/Modem/doc/Images/volte_on.png => module-cellular/Modem/doc/Images/volte_on.png +0 -0
A module-cellular/Modem/doc/scripts/atstream.pu => module-cellular/Modem/doc/scripts/atstream.pu +49 -0
@@ 0,0 1,49 @@
@startuml

start


:pass data to ATStream via write;

if (isReady) then 
	:return error (could be call reset, to get new data);
	Stop;
endif

:add new data to buffer;


    if (AT Return at the begin) then (yes)	
		:Set "Return at the begin" flag;
		if (Expected line count= 0) then (yes)
		:(isReady=true);
			Stop;
		endif
	else (no)
	endif
	
	:Count lines from last position;
	
    if ("Return at the begin" flag is set) then (yes)
	   if (More lines then expected) then (yes)
	   :(isReady=true);
		Stop;
	   endif
	else (no)
	 if (AT Return at the end) then (yes)
		:(isReady=true);
		Stop;
	 endif
	endif
	
	:Stream still wait for data (isReady=false);
	Stop;
	







@enduml
\ No newline at end of file

A module-cellular/Modem/doc/scripts/class_channel.pu => module-cellular/Modem/doc/scripts/class_channel.pu +12 -0
@@ 0,0 1,12 @@
@startuml

abstract class BaseChannel

BaseChannel <|-- Channel

Channel ..> ATStream: Channel use ATStream

Channel <|-- DLC_channel
Channel <|-- ATParser

@enduml
\ No newline at end of file

M module-cellular/at/Result.hpp => module-cellular/at/Result.hpp +3 -1
@@ 17,7 17,9 @@ namespace at
        enum class Code
        {
            OK,            /// at OK
            ERROR,         /// at ERROR
            ERROR,         /// at ERROR For compatibility also for CME_ERROR and CMS_ERROR (details in errorCode)
            CME_ERROR,     /// In case CME error see errorCode
            CMS_ERROR,     /// In case CMS error see errorCode
            TIMEOUT,       /// at Timeout
            TOKENS,        /// at numbers of tokens needed met
            NONE,          /// no code

M module-cellular/test/CMakeLists.txt => module-cellular/test/CMakeLists.txt +9 -0
@@ 28,3 28,12 @@ add_catch2_executable(
        module-cellular
)


add_catch2_executable(
        NAME
        unittest_ATStream
        SRCS
        unittest_ATStream.cpp
        LIBS
        module-cellular
)

M module-cellular/test/mock/AtCommon_channel.hpp => module-cellular/test/mock/AtCommon_channel.hpp +2 -2
@@ 3,11 3,11 @@

#pragma once

#include "Modem/ATCommon.hpp"
#include "Modem/BaseChannel.hpp"

namespace at
{

    class ChannelMock : public BaseChannel
    {
      public:


@@ 40,7 40,7 @@ namespace at
        void cmd_post() override
        {}

        std::vector<std::string> cmd_receive() override
        std::string cmd_receive() override
        {
            return {};
        }

A module-cellular/test/unittest_ATStream.cpp => module-cellular/test/unittest_ATStream.cpp +95 -0
@@ 0,0 1,95 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <string>
#define CATCH_CONFIG_MAIN

#include <catch2/catch.hpp>
#include "Modem/ATStream.hpp"
#include "Result.hpp"

TEST_CASE("Channel Test- AT return parser")
{
    SECTION("Parse AT - OK")
    {
        at::ATStream stream;
        uint32_t errcode = 0;
        auto code        = stream.parseState("OK", errcode);
        REQUIRE(code == at::Result::Code::OK);
    }

    SECTION("Parse AT - ERROR")
    {
        at::ATStream stream;
        uint32_t errcode = 0;
        auto code        = stream.parseState("ERROR", errcode);
        REQUIRE(code == at::Result::Code::ERROR);
    }

    SECTION("Parse AT - +CME ERROR: Valid")
    {
        at::ATStream stream;
        uint32_t errcode = 0;
        auto code        = stream.parseState("+CME ERROR: 33", errcode);
        REQUIRE(code == at::Result::Code::CME_ERROR);
        REQUIRE(errcode == 33);
    }

    SECTION("Parse AT - +CMS ERROR: Valid")
    {
        at::ATStream stream;
        uint32_t errcode = 0;
        auto code        = stream.parseState("+CMS ERROR: 33", errcode);
        REQUIRE(code == at::Result::Code::CMS_ERROR);
        REQUIRE(errcode == 33);
    }

    SECTION("Parse AT - +CMS ERROR: Invalid")
    {
        at::ATStream stream;
        uint32_t errcode = 0;
        auto code        = stream.parseState("+CMS ERROR: ssss", errcode);
        REQUIRE(code == at::Result::Code::PARSING_ERROR);
    }

    SECTION("Parse AT - +CMS ERROR: Invalid empty")
    {
        at::ATStream stream;
        uint32_t errcode = 0;
        auto code        = stream.parseState("+CMS ERROR:", errcode);
        REQUIRE(code == at::Result::Code::PARSING_ERROR);
    }

    SECTION("Parse AT - +CME ERROR: Valid")
    {
        at::ATStream stream;
        uint32_t errcode = 0;
        auto code        = stream.parseState("+CME ERROR: 33", errcode);
        REQUIRE(code == at::Result::Code::CME_ERROR);
        REQUIRE(errcode == 33);
    }

    SECTION("Parse AT - +CME ERROR: Invalid")
    {
        at::ATStream stream;
        uint32_t errcode = 0;
        auto code        = stream.parseState("+CME ERROR: ssss", errcode);
        REQUIRE(code == at::Result::Code::PARSING_ERROR);
    }

    SECTION("Parse AT - +CME ERROR: Invalid empty")
    {
        at::ATStream stream;
        uint32_t errcode = 0;
        auto code        = stream.parseState("+CME ERROR:", errcode);
        REQUIRE(code == at::Result::Code::PARSING_ERROR);
    }

    SECTION("Parse AT - Wrong data")
    {
        at::ATStream stream;
        uint32_t errcode = 0;
        auto code        = stream.parseState("+XME ERROR:", errcode);
        REQUIRE(code == at::Result::Code::NONE);
    }
}