diff --git a/.github/workflows/clang.yml b/.github/workflows/clang.yml index 44b9d6b..faac1b5 100644 --- a/.github/workflows/clang.yml +++ b/.github/workflows/clang.yml @@ -131,7 +131,7 @@ jobs: - name: Summary env: - clang-threshold: 2 + clang-threshold: 1 run: | echo "Status: ${{ steps.tidy.outputs.checks-failed }} checks failed, threshold for success is ${{ env.clang-threshold }}." echo "Clang report:" diff --git a/README.md b/README.md index 1d5c56a..e7fc0df 100644 --- a/README.md +++ b/README.md @@ -196,19 +196,19 @@ Home Assistant with MQTT is recommended for the best experience, but the desk al #### Diagnostics -| Name | Description | Requirement | -| -------------- | ---------------------------------------- | ----------- | -| Button down | Simulate a physical down-button press | `PIN_TPDN` | -| Button up | Simulate a physical up-button press | `PIN_TPUP` | -| Calibrate | Recalibrate the leg encoder sensors | | -| Firmware AVR | AVR firmware version | | -| Firmware ESP32 | ESP32 firmware version | | -| Offset | Current leg offset | | -| Position | Average encoder position | | -| Power supply | Input voltage of the desk’s power supply | `PIN_ADC` | -| Serial RX | Last UART message received | | -| Serial TX | Last UART message sent | | -| Temperature | Internal temperature of the ESP32 | | -| Wi-Fi signal | ESP32 Wi-Fi RSSI | | +| Name | Description | Requirement | +| ------------ | ---------------------------------------- | ----------- | +| Button down | Simulate a physical down-button press | `PIN_TPDN` | +| Button up | Simulate a physical up-button press | `PIN_TPUP` | +| Calibrate | Recalibrate the leg encoder sensors | | +| Errors | Currently detected communication errors | | +| Firmware | ESP32 firmware version | | +| Offset | Current leg offset | | +| Position | Average encoder position | | +| Power supply | Input voltage of the desk’s power supply | `PIN_ADC` | +| Serial RX | Last UART message received | | +| Serial TX | Last UART message sent | | +| Temperature | Internal temperature of the ESP32 | | +| Wi-Fi signal | ESP32 Wi-Fi RSSI | | To avoid cluttering the Home Assistant interface, only a handful of entities are enabled by default. diff --git a/include/avr/ConsoleHandler.h b/include/avr/ConsoleHandler.h index 939e014..96f3988 100644 --- a/include/avr/ConsoleHandler.h +++ b/include/avr/ConsoleHandler.h @@ -29,16 +29,14 @@ class ConsoleHandler { BUTTON_DOWN = 1U, BUTTON_UP, - ENCODER8, - ENCODER9, - INITIALIZE, + CONSOLE, + INITIALIZATION, + LIN, NODE8, NODE9, POSITION, PRESET_HIGH, PRESET_LOW, - STATE8, - STATE9, }; /** diff --git a/include/avr/ControllerService.h b/include/avr/ControllerService.h index 755258c..dfec33b 100644 --- a/include/avr/ControllerService.h +++ b/include/avr/ControllerService.h @@ -46,6 +46,8 @@ class ControllerService static ControllerService &getInstance(); private: + bool error8{false}; + bool error9{false}; bool pending{false}; unsigned char state8{0U}; @@ -66,7 +68,7 @@ class ControllerService /** * Handles leg movement commands and encoder communication. */ - LegHandler lin{}; + LegHandler leg{}; State state{State::IDLE}; diff --git a/include/avr/LegHandler.h b/include/avr/LegHandler.h index 019e95c..c8368d5 100644 --- a/include/avr/LegHandler.h +++ b/include/avr/LegHandler.h @@ -24,39 +24,34 @@ class LegHandler static constexpr unsigned char linDiagnosticResponseId{0x3DU}; static constexpr unsigned char linSyncByte{0x55U}; - void serialBreak(); + void requestDiscardResponse(); + + void sendResponse(unsigned char pid); - [[nodiscard]] unsigned char calcParity(unsigned char identifier); + void serialBreak(); - [[nodiscard]] int readWithTimeout(unsigned int &remainingTime); + [[nodiscard]] int read(unsigned int &remainingTime); +public: /** - * Calculates the complemented checksum for a sequence of bytes. - * @param data Bytes to include in the checksum. - * @param sum Initial checksum sum. - * @return The complemented checksum. + * Builds a LIN protected identifier from a frame identifier. + * + * @param identifier Frame identifier; only the lower six bits are used. + * @return The identifier with its parity bits in the upper two bits. */ - template [[nodiscard]] unsigned char calcChecksum(const unsigned char (&data)[N], unsigned int sum) - { - for (const unsigned char byte : data) - { - sum += byte; - } - while ((sum >> 8U) != 0U) - { - sum = (sum & 0xFFU) + (sum >> 8U); - } - return static_cast(~sum); - } - - template bool sendRequest(const unsigned char (&packet)[N]) + [[nodiscard]] static constexpr unsigned char getPid(unsigned char identifier) { - send(linDiagnosticRequestId, packet); - unsigned char response[N]{}; - return request(linDiagnosticResponseId, response); + identifier &= 0x3FU; + const unsigned int parity0{ + static_cast(identifier & 1U) ^ (static_cast(identifier >> 1U) & 1U) ^ + (static_cast(identifier >> 2U) & 1U) ^ (static_cast(identifier >> 4U) & 1U)}; + const unsigned int parity1{ + ~((static_cast(identifier >> 1U) & 1U) ^ (static_cast(identifier >> 3U) & 1U) ^ + (static_cast(identifier >> 4U) & 1U) ^ (static_cast(identifier >> 5U) & 1U)) & + 1U}; + return static_cast(identifier | ((parity0 | (parity1 << 1U)) << 6U)); } -public: enum class Command : unsigned char { FINISH = 0x84U, @@ -69,9 +64,9 @@ class LegHandler IDLE = 0xFCU, }; - [[nodiscard]] bool begin(); + [[nodiscard]] unsigned char begin(); - void send(unsigned char identifier); + [[nodiscard]] unsigned char getLeg(unsigned char pid, unsigned char (&node)[3U]); /** * Sends a desk command with a target encoder position. @@ -82,69 +77,100 @@ class LegHandler void sendCommand(Command command, unsigned int position); /** - * Sends a LIN frame containing the specified payload. - * - * @param identifier LIN frame identifier. - * @param data Payload bytes to transmit. + * Calculates the complemented checksum for a sequence of bytes. + * @param data Bytes to include in the checksum. + * @param sum Initial checksum sum. + * @return The complemented checksum. */ - template void send(unsigned char identifier, const unsigned char (&data)[N]) + template + [[nodiscard]] unsigned char getChecksum(const unsigned char (&data)[N], unsigned int sum = 0U) { - static_assert(N <= 8U); - const unsigned char addressByte{static_cast((identifier & 0x3FU) | calcParity(identifier))}; - serialBreak(); - Serial.write(linSyncByte); - Serial.write(addressByte); - Serial.write(data, N); - Serial.write(calcChecksum(data, identifier == linDiagnosticRequestId ? 0U : addressByte)); - Serial.flush(); + for (const unsigned char byte : data) + { + sum += byte; + } + while ((sum >> 8U) != 0U) + { + sum = (sum & 0xFFU) + (sum >> 8U); + } + return static_cast(~sum); } /** - * Receives a LIN response for the specified identifier. + * Requests a LIN response and reads its payload and checksum. * - * @param identifier LIN frame identifier to request. + * @param pid Protected identifier to transmit in the response header. * @param data Buffer to populate with the received payload. - * @return `true` if a complete response with a valid checksum is received, `false` on timeout or checksum - * failure. + * @return The received checksum byte, or `-1` if the response is incomplete. */ - template [[nodiscard]] bool request(unsigned char identifier, unsigned char (&data)[N]) + template int receiveResponse(unsigned char pid, unsigned char (&data)[N]) { static_assert(N <= 8U); - const unsigned char idByte{static_cast((identifier & 0x3FU) | calcParity(identifier))}; serialBreak(); Serial.write(linSyncByte); - Serial.write(idByte); + Serial.write(pid); Serial.flush(); int receivedByte{}; unsigned int remainingTime{static_cast(LinFrame::frameBits * 1'000'000UL / baudRate)}; do // NOLINT(cppcoreguidelines-avoid-do-while) { - receivedByte = readWithTimeout(remainingTime); + receivedByte = read(remainingTime); } while (receivedByte != -1 && receivedByte != static_cast(linSyncByte)); if (receivedByte == -1) { - return false; + return -1; } do // NOLINT(cppcoreguidelines-avoid-do-while) { - receivedByte = readWithTimeout(remainingTime); - } while (receivedByte != -1 && receivedByte != idByte); + receivedByte = read(remainingTime); + } while (receivedByte != -1 && receivedByte != pid); if (receivedByte == -1) { - return false; + return -1; } for (unsigned char &dataByte : data) { - receivedByte = readWithTimeout(remainingTime); + receivedByte = read(remainingTime); if (receivedByte == -1) { - return false; + return -1; } dataByte = static_cast(receivedByte); } - receivedByte = readWithTimeout(remainingTime); - return receivedByte != -1 && - calcChecksum(data, identifier == linDiagnosticResponseId ? 0U : idByte) == receivedByte; + return read(remainingTime); + } + + /** + * Sends a LIN diagnostic request with a classic checksum. + * + * @param data Diagnostic payload to transmit. + */ + template void sendDiagnosticRequest(const unsigned char (&data)[N]) + { + static_assert(N <= 8U); + serialBreak(); + Serial.write(linSyncByte); + Serial.write(getPid(linDiagnosticRequestId)); + Serial.write(data, N); + Serial.write(getChecksum(data)); + Serial.flush(); + } + + /** + * Sends a LIN response with an enhanced checksum. + * + * @param pid Protected identifier to transmit. + * @param data Response payload to transmit. + */ + template void sendResponse(unsigned char pid, const unsigned char (&data)[N]) + { + static_assert(N <= 8U); + serialBreak(); + Serial.write(linSyncByte); + Serial.write(pid); + Serial.write(data, N); + Serial.write(getChecksum(data, pid)); + Serial.flush(); } }; diff --git a/include/esp/ConsoleHandler.h b/include/esp/ConsoleHandler.h index 6fac6e7..814a197 100644 --- a/include/esp/ConsoleHandler.h +++ b/include/esp/ConsoleHandler.h @@ -2,6 +2,7 @@ #ifdef ARDUINO_ARCH_ESP32 +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -22,16 +23,14 @@ class ConsoleHandler { BUTTON_DOWN = 1U, BUTTON_UP, - ENCODER8, - ENCODER9, - INITIALIZE, + CONSOLE, + INITIALIZATION, + LIN, NODE8, NODE9, POSITION, PRESET_HIGH, PRESET_LOW, - STATE8, - STATE9, }; /** @@ -39,6 +38,8 @@ class ConsoleHandler */ void begin(); + void getErrors(JsonArray &errors); + /** * Processes available console input. */ @@ -49,11 +50,16 @@ class ConsoleHandler */ void forward(); + void reset(); + void send(Command command); void send(Command command, uint16_t value); private: + uint8_t errorLin{0U}; + uint8_t errorTx{0U}; + size_t bytesRx{0U}; size_t bytesTx{0U}; size_t lengthRx{0U}; @@ -72,12 +78,16 @@ class ConsoleHandler */ State stateRx{}; - static inline hardwareSerial_error_t lastError{hardwareSerial_error_t::UART_NO_ERROR}; + static inline hardwareSerial_error_t errorRx{hardwareSerial_error_t::UART_NO_ERROR}; /** * Parses a received console payload. */ - void parse() const; + void parse(); + + void setErrorLin(uint8_t flags); + + void setErrorTx(uint8_t flags); void write(std::span payload); diff --git a/include/esp/DeskService.h b/include/esp/DeskService.h index 98166d4..d0ce539 100644 --- a/include/esp/DeskService.h +++ b/include/esp/DeskService.h @@ -23,6 +23,9 @@ class DeskService bool reset{false}; bool saved{true}; + uint8_t error8{0U}; + uint8_t error9{0U}; + uint8_t errorInit{0U}; uint8_t state8{0U}; uint8_t state9{0U}; @@ -36,6 +39,8 @@ class DeskService size_t lengthRx{0U}; size_t lengthTx{0U}; + hardwareSerial_error_t errorRx{hardwareSerial_error_t::UART_NO_ERROR}; + std::string versionLatest{}; std::array payloadRx{}; @@ -68,6 +73,8 @@ class DeskService void statusNode(); + void getErrors(JsonArray &list); + [[nodiscard]] float decode(float encoder); [[nodiscard]] uint16_t encode(float height); @@ -97,9 +104,17 @@ class DeskService void setButtonUp(bool state); - void setEncoder8(uint16_t position); + void setError8(uint8_t flags); + + void setError9(uint8_t flags); + + void setErrorInit(uint8_t flags); - void setEncoder9(uint16_t position); + void setNode8(uint16_t position, uint8_t state); + + void setNode9(uint16_t position, uint8_t state); + + void setPending(); void setPresetHigh(uint16_t encoder); @@ -107,10 +122,6 @@ class DeskService void setRx(std::span payload); - void setState8(uint8_t state); - - void setState9(uint8_t state); - void setTx(std::span payload); void statusRed(); diff --git a/src/avr/ConsoleHandler.cpp b/src/avr/ConsoleHandler.cpp index acf872a..f962e07 100644 --- a/src/avr/ConsoleHandler.cpp +++ b/src/avr/ConsoleHandler.cpp @@ -8,13 +8,19 @@ /** * @brief Buffers a serial command and parses it when its complete payload is received. * - * The first byte specifies the payload length and command identifier. + * The first byte specifies the payload length and command identifier. USART receive errors are reported before the + * byte is consumed. */ void ConsoleHandler::handle() { - const int byte{Serial1.read()}; - if (byte != -1) + if (Serial1.available() != 0) { + const unsigned char errors{UCSR1A}; // NOLINT(clang-analyzer-core.FixedAddressDereference) + if ((errors & ((0b1U << UPE1) | (0b1U << DOR1) | (0b1U << FE1))) != 0U) + { + send(State::CONSOLE, errors); + } + const int byte{Serial1.read()}; if (lengthRx == 0U) { lengthRx = static_cast(static_cast(byte) >> 4U); @@ -90,7 +96,7 @@ void ConsoleHandler::send(State state) { Serial1.write(static_cast(state)); + Serial1.write(static_cast((1U << 4U) | static_cast(state))); Serial1.write(byte); } diff --git a/src/avr/ControllerService.cpp b/src/avr/ControllerService.cpp index 6f39e8f..0119892 100644 --- a/src/avr/ControllerService.cpp +++ b/src/avr/ControllerService.cpp @@ -26,9 +26,10 @@ void ControllerService::begin() EEPROM.get(static_cast('l'), presetLow); console.send(ConsoleHandler::State::PRESET_HIGH, presetHigh); console.send(ConsoleHandler::State::PRESET_LOW, presetLow); - if (!lin.begin()) + const unsigned char init{leg.begin()}; + if (init != 0U) { - console.send(ConsoleHandler::State::INITIALIZE); + console.send(ConsoleHandler::State::INITIALIZATION, init); tone(0b1U << 8U); return; } @@ -64,73 +65,56 @@ void ControllerService::handle() bool ControllerService::read() { constexpr unsigned char empty[3U]{0U, 0U, 0U}; - lin.send(0x11U, empty); - unsigned char node8[3U]{}; - unsigned char node9[3U]{}; - const bool valid8{lin.request(0x8U, node8)}; - const bool valid9{lin.request(0x9U, node9)}; - if (valid8) + leg.sendResponse(LegHandler::getPid(0x11U), empty); + unsigned char node[3U]{}; + const unsigned char status8{leg.getLeg(LegHandler::getPid(0x8U), node)}; + if (status8 == 0U) { - const unsigned int _encoder8{static_cast(node8[0U]) | static_cast(node8[1U]) << 8U}; - if (_encoder8 != encoder8 && state8 != node8[2U]) + const unsigned int _encoder8{static_cast(node[0U]) | static_cast(node[1U]) << 8U}; + if (_encoder8 != encoder8 || state8 != node[2U] || error8) { + if (_encoder8 != encoder8) + { + lastMillis = millis(); + } encoder8 = _encoder8; - state8 = node8[2U]; - lastMillis = millis(); - console.send(ConsoleHandler::State::NODE8, node8); - } - else if (_encoder8 != encoder8) - { - encoder8 = _encoder8; - lastMillis = millis(); - console.send(ConsoleHandler::State::ENCODER8, encoder8); - } - else if (state8 != node8[2U]) - { - state8 = node8[2U]; - console.send(ConsoleHandler::State::STATE8, state8); + state8 = node[2U]; + error8 = false; + console.send(ConsoleHandler::State::NODE8, node); } } else { - console.send(ConsoleHandler::State::NODE8); - if (pending) - { - tone(0b1U << 8U); - } + error8 = true; + console.send(ConsoleHandler::State::NODE8, status8); } - if (valid9) + const unsigned char status9{leg.getLeg(LegHandler::getPid(0x9U), node)}; + if (status9 == 0U) { - const unsigned int _encoder9{static_cast(node9[0U]) | static_cast(node9[1U]) << 8U}; - if (_encoder9 != encoder9 && state9 != node9[2U]) + const unsigned int _encoder9{static_cast(node[0U]) | static_cast(node[1U]) << 8U}; + if (_encoder9 != encoder9 || state9 != node[2U] || error9) { + if (_encoder9 != encoder9) + { + lastMillis = millis(); + } encoder9 = _encoder9; - state9 = node9[2U]; - lastMillis = millis(); - console.send(ConsoleHandler::State::NODE9, node9); - } - else if (_encoder9 != encoder9) - { - encoder9 = _encoder9; - lastMillis = millis(); - console.send(ConsoleHandler::State::ENCODER9, encoder9); - } - else if (state9 != node9[2U]) - { - state9 = node9[2U]; - console.send(ConsoleHandler::State::STATE9, state9); + state9 = node[2U]; + error9 = false; + console.send(ConsoleHandler::State::NODE9, node); } } else { - console.send(ConsoleHandler::State::NODE9); + error9 = true; + console.send(ConsoleHandler::State::NODE9, status9); + } + if (status8 != 0U || status9 != 0U) + { if (pending) { tone(0b1U << 8U); } - } - if (!valid8 || !valid9) - { return false; } wdt_reset(); @@ -175,7 +159,7 @@ void ControllerService::process() break; case State::RECAL_DONE: state = State::IDLE; - lin.sendCommand(LegHandler::Command::CALIBRATE_END, 99U); + leg.sendCommand(LegHandler::Command::CALIBRATE_END, 99U); break; } } @@ -293,7 +277,7 @@ void ControllerService::handleStateRecalOngoing() state = State::RECAL_DONE; return; } - lin.sendCommand(LegHandler::Command::CALIBRATE_BEGIN, 0U); + leg.sendCommand(LegHandler::Command::CALIBRATE_BEGIN, 0U); } /** @@ -309,7 +293,7 @@ void ControllerService::sendCommand(LegHandler::Command command) : Encoder::maxLimit}; const unsigned int minTarget{maxCurrent > Encoder::minLimit + Encoder::maxDelta ? maxCurrent - Encoder::maxDelta : Encoder::minLimit}; - lin.sendCommand(command, constrain(encoderTarget, minTarget, maxTarget)); + leg.sendCommand(command, constrain(encoderTarget, minTarget, maxTarget)); } /** diff --git a/src/avr/LegHandler.cpp b/src/avr/LegHandler.cpp index 9111d4f..f6de71e 100644 --- a/src/avr/LegHandler.cpp +++ b/src/avr/LegHandler.cpp @@ -2,6 +2,7 @@ #include "avr/LegHandler.h" +#include "avr/ConsoleHandler.h" #include "avr/constants.h" #include @@ -9,9 +10,10 @@ /** * @brief Initializes the LIN interface and configures the connected device. * - * @return true if initialization and device detection succeed, false otherwise. + * @return `0` on success; bits 0 and 1 report no response and checksum mismatch for probe A, and bits 2 and 3 + * report the same conditions for probe B. */ -bool LegHandler::begin() +unsigned char LegHandler::begin() { pinMode(Pin::lin, OUTPUT); Serial.begin(baudRate); @@ -23,22 +25,26 @@ bool LegHandler::begin() for (const unsigned char (&data)[2U] : initial) { const unsigned char packet[8U]{0xFFU, data[0U], data[1U], 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU}; - send(linDiagnosticRequestId, packet); + sendDiagnosticRequest(packet); } const unsigned char packet[8U]{0xD0U, 0x2U, 0x7U, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU}; - sendRequest(packet); + sendDiagnosticRequest(packet); + requestDiscardResponse(); unsigned char pid{0U}; for (; pid < 8U; ++pid) { const unsigned char probeA[8U]{pid, 0x2U, 0x7U, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU}; - if (sendRequest(probeA)) + sendDiagnosticRequest(probeA); + unsigned char response[sizeof(probeA)]{}; + const int checksum{receiveResponse(getPid(linDiagnosticResponseId), response)}; + if (checksum != -1 && getChecksum(response) == checksum) { break; } - } - if (pid == 8U) - { - return false; + if (pid == 7U) + { + return static_cast(checksum == -1 ? 0b1U : 0b1U << 1U); + } } constexpr unsigned char preProbe[6U][2U]{ {0x6U, 0x9U}, @@ -51,19 +57,24 @@ bool LegHandler::begin() for (const unsigned char (&data)[2U] : preProbe) { const unsigned char packet[8U]{pid, data[0U], data[1U], 0x0U, 0xFFU, 0xFFU, 0xFFU, 0xFFU}; - sendRequest(packet); + sendDiagnosticRequest(packet); + requestDiscardResponse(); } for (; pid < 8U; ++pid) { const unsigned char probeB[8U]{pid, 0x2U, 0x0U, 0x0U, 0xFFU, 0xFFU, 0xFFU, 0xFFU}; - if (sendRequest(probeB)) + // unsigned char response[sizeof(probeB)]{}; + sendDiagnosticRequest(probeB); + unsigned char response[sizeof(probeB)]{}; + const int checksum{receiveResponse(getPid(linDiagnosticResponseId), response)}; + if (checksum != -1 && getChecksum(response) == checksum) { break; } - } - if (pid == 8U) - { - return false; + if (pid == 7U) + { + return static_cast(checksum == -1 ? 0b1U << 2U : 0b1U << 3U); + } } constexpr unsigned char preBroadcast[6U][2U]{ {0x6U, 0x9U}, @@ -76,49 +87,56 @@ bool LegHandler::begin() for (const unsigned char (&data)[2U] : preBroadcast) { const unsigned char packet[8U]{pid, data[0U], data[1U], 0x0U, 0xFFU, 0xFFU, 0xFFU, 0xFFU}; - sendRequest(packet); + sendDiagnosticRequest(packet); + requestDiscardResponse(); } for (; pid < 8U; ++pid) { const unsigned char broadcast[8U]{pid, 0x2U, 0x1U, 0x0U, 0xFFU, 0xFFU, 0xFFU, 0xFFU}; - sendRequest(broadcast); + sendDiagnosticRequest(broadcast); + requestDiscardResponse(); } constexpr unsigned char postBroadcast[2U]{0x1U, 0x2U}; for (const unsigned char &data : postBroadcast) { const unsigned char packet[8U]{0xD0U, data, 0x7U, 0x0U, 0xFFU, 0xFFU, 0xFFU, 0xFFU}; - send(linDiagnosticRequestId, packet); + sendDiagnosticRequest(packet); } constexpr unsigned char final[3U]{0xF6U, 0xFFU, 0xBFU}; - send(0x12U, final); - return true; + sendResponse(getPid(0x12U), final); + return 0U; } /** - * @brief Calculates the LIN protected identifier parity bits. + * @brief Requests a desk-leg status frame and validates its checksum. * - * @param identifier Six-bit LIN identifier. - * @return Parity bits positioned in bits 6 and 7. + * @param pid Protected identifier of the leg status frame. + * @param node Buffer for the two-byte encoder position and one-byte state. + * @return `0` for a valid response, bit 0 for an incomplete response, or bit 1 for a checksum mismatch. */ -unsigned char LegHandler::calcParity(unsigned char identifier) +unsigned char LegHandler::getLeg(unsigned char pid, unsigned char (&node)[3U]) { - const unsigned int parity0{ - static_cast(identifier & 1U) ^ (static_cast(identifier >> 1U) & 1U) ^ - (static_cast(identifier >> 2U) & 1U) ^ (static_cast(identifier >> 4U) & 1U)}; - const unsigned int parity1{ - ~((static_cast(identifier >> 1U) & 1U) ^ (static_cast(identifier >> 3U) & 1U) ^ - (static_cast(identifier >> 4U) & 1U) ^ (static_cast(identifier >> 5U) & 1U)) & - 1U}; - return static_cast((parity0 | (parity1 << 1U)) << 6U); + const int checksum{receiveResponse(pid, node)}; + if (checksum == -1) + { + return 0b1U; + } + if (getChecksum(node, pid) == checksum) + { + return 0U; + } + return 0b1U << 1U; } /** * @brief Reads a serial byte within the available time budget. * + * Reports parity, data-overrun, and frame errors to the console before returning the byte. + * * @param remainingTime Maximum wait time in microseconds; reduced by the time spent waiting. * @return int The received byte, or -1 if no byte is available before the timeout. */ -int LegHandler::readWithTimeout(unsigned int &remainingTime) +int LegHandler::read(unsigned int &remainingTime) { constexpr unsigned int interval{static_cast(1'000'000UL / baudRate)}; while (remainingTime != 0U && Serial.available() == 0) @@ -127,22 +145,33 @@ int LegHandler::readWithTimeout(unsigned int &remainingTime) delayMicroseconds(delayTime); remainingTime -= delayTime; } + if (Serial.available() == 0) + { + return -1; + } + const unsigned char errors{UCSR0A}; // NOLINT(clang-analyzer-core.FixedAddressDereference) + if ((errors & ((0b1U << UPE0) | (0b1U << DOR0) | (0b1U << FE0))) != 0U) + { + Serial1.write((1U << 4U) | static_cast(ConsoleHandler::State::LIN)); + Serial1.write(errors); + } return Serial.read(); } /** - * @brief Transmits a LIN frame for the specified identifier. - * - * @param identifier LIN identifier; its lower six bits are used to form the protected identifier. + * @brief Requests and discards a diagnostic response within one frame time budget. */ -void LegHandler::send(unsigned char identifier) +void LegHandler::requestDiscardResponse() { - const unsigned char address{static_cast((identifier & 0x3FU) | calcParity(identifier))}; serialBreak(); Serial.write(linSyncByte); - Serial.write(address); - Serial.write(identifier == linDiagnosticRequestId ? 0xFFU : static_cast(~address)); + Serial.write(getPid(linDiagnosticResponseId)); Serial.flush(); + unsigned int remainingTime{static_cast(LinFrame::frameBits * 1'000'000UL / baudRate)}; + while (remainingTime != 0U) + { + static_cast(read(remainingTime)); + } } /** @@ -151,19 +180,33 @@ void LegHandler::send(unsigned char identifier) * @param command Command code to transmit. * @param position Position value included in the command payload. */ -void LegHandler::sendCommand(LegHandler::Command command, unsigned int position) +void LegHandler::sendCommand(Command command, unsigned int position) { for (unsigned char idx{0U}; idx < 6U; ++idx) { - send(0x10U); + sendResponse(getPid(0x10U)); } - send(0x1U); + sendResponse(getPid(0x1U)); const unsigned char packet[3U]{ static_cast(position & 0xFFU), static_cast(position >> 8U), static_cast(command), }; - send(0x12U, packet); + sendResponse(getPid(0x12U), packet); +} + +/** + * @brief Sends a LIN response header followed by the complemented protected identifier. + * + * @param pid Protected identifier to transmit. + */ +void LegHandler::sendResponse(unsigned char pid) +{ + serialBreak(); + Serial.write(linSyncByte); + Serial.write(pid); + Serial.write(static_cast(~pid)); + Serial.flush(); } /** diff --git a/src/esp/ConsoleHandler.cpp b/src/esp/ConsoleHandler.cpp index cdcbad8..cd89d9d 100644 --- a/src/esp/ConsoleHandler.cpp +++ b/src/esp/ConsoleHandler.cpp @@ -15,11 +15,12 @@ void ConsoleHandler::begin() pinMode(PIN_MISO, INPUT); pinMode(PIN_SCK, OUTPUT); Serial1.onReceiveError(&onReceiveError); + Serial1.setRxBufferSize(0b1U << 9U); Serial1.begin(115'200UL, SerialConfig::SERIAL_8N1, PIN_MISO, PIN_SCK); } /** - * @brief Processes available secondary-serial data, pending UART errors, or primary-serial input. + * @brief Processes a secondary-serial byte or forwards primary-serial input when none is available. */ void ConsoleHandler::handle() { @@ -40,16 +41,6 @@ void ConsoleHandler::handle() lengthRx = 0U; } } - else if (lastError != hardwareSerial_error_t::UART_NO_ERROR) - { - const uint8_t _error{static_cast(lastError)}; - lastError = hardwareSerial_error_t::UART_NO_ERROR; - ESP_LOGW("hardwareSerial_error_t", "%d", _error); - desk.statusRed(); - JsonDocument doc{}; - doc["hardwareSerial_error_t"].set(_error); - desk.transmit(doc); - } else { forward(); @@ -82,12 +73,65 @@ void ConsoleHandler::forward() } } +/** + * @brief Appends descriptions of recorded serial communication errors. + * + * @param errors JSON array to append to. + */ +void ConsoleHandler::getErrors(JsonArray &errors) +{ + if ((errorLin & (0b1U << 2U)) != 0U) + { + errors.add("USART0: parity error"); + } + if ((errorLin & (0b1U << 3U)) != 0U) + { + errors.add("USART0: data overrun"); + } + if ((errorLin & (0b1U << 4U)) != 0U) + { + errors.add("USART0: frame error"); + } + if ((errorTx & (0b1U << 2U)) != 0U) + { + errors.add("USART1: parity error"); + } + if ((errorTx & (0b1U << 3U)) != 0U) + { + errors.add("USART1: data overrun"); + } + if ((errorTx & (0b1U << 4U)) != 0U) + { + errors.add("USART1: frame error"); + } + switch (errorRx) + { + case hardwareSerial_error_t::UART_NO_ERROR: + break; + case hardwareSerial_error_t::UART_BREAK_ERROR: + errors.add("UART: break"); + break; + case hardwareSerial_error_t::UART_BUFFER_FULL_ERROR: + errors.add("UART: buffer full"); + break; + case hardwareSerial_error_t::UART_FIFO_OVF_ERROR: + errors.add("UART: FIFO overflow"); + break; + case hardwareSerial_error_t::UART_FRAME_ERROR: + errors.add("UART: frame error"); + break; + case hardwareSerial_error_t::UART_PARITY_ERROR: + errors.add("UART: parity error"); + break; + } +} + /** * @brief Applies the buffered console frame to the corresponding device state. * * Invalid command and payload-length combinations set the device status to red. */ -void ConsoleHandler::parse() const +void ConsoleHandler::parse() { desk.setRx(std::span{bufferRx}.subspan(0U, lengthRx + 1U)); if (stateRx == State::BUTTON_DOWN && lengthRx == 1U) @@ -98,27 +142,37 @@ void ConsoleHandler::parse() const { desk.setButtonUp(static_cast(bufferRx.at(1U))); } - else if (stateRx == State::ENCODER8 && lengthRx == 2U) + else if (stateRx == State::CONSOLE && lengthRx == 1U) + { + setErrorTx(bufferRx.at(1U)); + } + else if (stateRx == State::INITIALIZATION && lengthRx == 1U) { - desk.setEncoder8(static_cast(bufferRx.at(1U)) | - static_cast(static_cast(bufferRx.at(2U)) << 8U)); + desk.setErrorInit(bufferRx.at(1U)); } - else if (stateRx == State::ENCODER9 && lengthRx == 2U) + else if (stateRx == State::LIN && lengthRx == 1U) { - desk.setEncoder9(static_cast(bufferRx.at(1U)) | - static_cast(static_cast(bufferRx.at(2U)) << 8U)); + setErrorLin(bufferRx.at(1U)); + } + else if (stateRx == State::NODE8 && lengthRx == 1U) + { + desk.setError8(bufferRx.at(1U)); } else if (stateRx == State::NODE8 && lengthRx == 3U) { - desk.setEncoder8(static_cast(bufferRx.at(1U)) | - static_cast(static_cast(bufferRx.at(2U)) << 8U)); - desk.setState8(bufferRx.at(3U)); + desk.setNode8(static_cast(static_cast(bufferRx.at(1U)) | + static_cast(static_cast(bufferRx.at(2U)) << 8U)), + bufferRx.at(3U)); + } + else if (stateRx == State::NODE9 && lengthRx == 1U) + { + desk.setError9(bufferRx.at(1U)); } else if (stateRx == State::NODE9 && lengthRx == 3U) { - desk.setEncoder9(static_cast(bufferRx.at(1U)) | - static_cast(static_cast(bufferRx.at(2U)) << 8U)); - desk.setState9(bufferRx.at(3U)); + desk.setNode9(static_cast(static_cast(bufferRx.at(1U)) | + static_cast(static_cast(bufferRx.at(2U)) << 8U)), + bufferRx.at(3U)); } else if (stateRx == State::PRESET_HIGH && lengthRx == 2U) { @@ -130,20 +184,22 @@ void ConsoleHandler::parse() const desk.setPresetLow(static_cast(bufferRx.at(1U)) | static_cast(static_cast(bufferRx.at(2U)) << 8U)); } - else if (stateRx == State::STATE8 && lengthRx == 1U) - { - desk.setState8(bufferRx.at(1U)); - } - else if (stateRx == State::STATE9 && lengthRx == 1U) - { - desk.setState9(bufferRx.at(1U)); - } else { desk.statusRed(); } } +/** + * @brief Clears all recorded serial communication errors. + */ +void ConsoleHandler::reset() +{ + errorLin = 0U; + errorRx = hardwareSerial_error_t::UART_NO_ERROR; + errorTx = 0U; +} + /** * @brief Sends a command without an associated value. * @@ -171,6 +227,36 @@ void ConsoleHandler::send(Command command, uint16_t value) write(payload); } +/** + * @brief Records LIN USART error flags and signals an error state. + * + * @param flags AVR USART status flags. + */ +void ConsoleHandler::setErrorLin(uint8_t flags) +{ + if (flags != errorLin) + { + errorLin = flags; + desk.setPending(); + } + desk.statusRed(); +} + +/** + * @brief Records console USART error flags and signals an error state. + * + * @param flags AVR USART status flags. + */ +void ConsoleHandler::setErrorTx(uint8_t flags) +{ + if (flags != errorTx) + { + errorTx = flags; + desk.setPending(); + } + desk.statusRed(); +} + /** * @brief Transmits a framed payload through the secondary serial interface. * @@ -187,10 +273,19 @@ void ConsoleHandler::write(std::span payload) } /** - * @brief Stores the latest hardware serial error for processing. + * @brief Records the latest hardware serial receive error and signals an error state. * * @param error Hardware serial error to store. */ -void ConsoleHandler::onReceiveError(hardwareSerial_error_t error) { lastError = error; } +void ConsoleHandler::onReceiveError(hardwareSerial_error_t error) +{ + ESP_LOGW("hardwareSerial_error_t", "%u", static_cast(error)); + if (error != errorRx) + { + errorRx = error; + desk.setPending(); + } + desk.statusRed(); +} #endif // ARDUINO_ARCH_ESP32 diff --git a/src/esp/DeskService.cpp b/src/esp/DeskService.cpp index b457743..3b1adf1 100644 --- a/src/esp/DeskService.cpp +++ b/src/esp/DeskService.cpp @@ -69,10 +69,10 @@ void DeskService::begin() attachInterrupt(PIN_TPUP, onInterruptUp, CHANGE); #endif // PIN_TPUP digitalWrite(PIN_RST, HIGH); + console.begin(); wifi.begin(); ota.begin(); isp.begin(); - console.begin(); mqtt.begin(); fetchRelease(); } @@ -125,32 +125,35 @@ void DeskService::handle() } /** - * @brief Converts an encoder value to the corresponding physical desk height. - * - * @param encoder Encoder value to convert. - * @return Physical desk height corresponding to the encoder value. + * @brief Disables device processing and disconnects serial and MQTT services. */ -float DeskService::decode(float encoder) +void DeskService::safeMode() { - return ((encoder - static_cast(ReferenceHeight::encoderLow)) * - (ReferenceHeight::heightHigh - ReferenceHeight::heightLow) / - static_cast(ReferenceHeight::encoderHigh - ReferenceHeight::encoderLow)) + - ReferenceHeight::heightLow; + process = false; + Serial1.end(); + mqtt.disconnect(); } /** - * @brief Converts a physical desk height to its corresponding encoder value. - * - * @param height Physical desk height. - * @return uint16_t Encoder value mapped from the configured height range. + * @brief Persists encoder, preset, and output-enable state to non-volatile storage. */ -uint16_t DeskService::encode(float height) +void DeskService::save() { - return static_cast( - lroundf(((height - ReferenceHeight::heightLow) * - static_cast(ReferenceHeight::encoderHigh - ReferenceHeight::encoderLow) / - (ReferenceHeight::heightHigh - ReferenceHeight::heightLow)) + - static_cast(ReferenceHeight::encoderLow))); + nvs_handle_t handle{}; + if (nvs_open("bekant", nvs_open_mode_t::NVS_READWRITE, &handle) == ESP_OK) + { + saved = true; + nvs_set_u16(handle, "8", encoder8); + nvs_set_u16(handle, "9", encoder9); + nvs_set_u16(handle, "h", presetHigh); + nvs_set_u16(handle, "l", presetLow); + nvs_set_u8(handle, "oe", static_cast(enable)); + if (nvs_commit(handle) != ESP_OK) + { + saved = false; + } + nvs_close(handle); + } } /** @@ -225,38 +228,6 @@ void DeskService::request(JsonObjectConst doc) } } -/** - * @brief Disables device processing and disconnects serial and MQTT services. - */ -void DeskService::safeMode() -{ - process = false; - Serial1.end(); - mqtt.disconnect(); -} - -/** - * @brief Persists encoder, preset, and output-enable state to non-volatile storage. - */ -void DeskService::save() -{ - nvs_handle_t handle{}; - if (nvs_open("bekant", nvs_open_mode_t::NVS_READWRITE, &handle) == ESP_OK) - { - saved = true; - nvs_set_u16(handle, "8", encoder8); - nvs_set_u16(handle, "9", encoder9); - nvs_set_u16(handle, "h", presetHigh); - nvs_set_u16(handle, "l", presetLow); - nvs_set_u8(handle, "oe", static_cast(enable)); - if (nvs_commit(handle) != ESP_OK) - { - saved = false; - } - nvs_close(handle); - } -} - /** * @brief Publishes the current device state and telemetry. * @@ -269,6 +240,9 @@ void DeskService::transmit(JsonDocument &doc) doc["desk"].set(decode(static_cast(encoder8 + encoder9) / 2.0F)); doc["encoders"][0U].set(encoder8); doc["encoders"][1U].set(encoder9); + JsonArray errors{doc["errors"].to()}; + getErrors(errors); + console.getErrors(errors); const float leg8{decode(static_cast(encoder8))}; const float leg9{decode(static_cast(encoder9))}; doc["legs"][0U].set(leg8); @@ -311,6 +285,76 @@ void DeskService::transmit(JsonDocument &doc) mqtt.transmit(doc); } +/** + * @brief Converts an encoder value to the corresponding physical desk height. + * + * @param encoder Encoder value to convert. + * @return Physical desk height corresponding to the encoder value. + */ +float DeskService::decode(float encoder) +{ + return ((encoder - static_cast(ReferenceHeight::encoderLow)) * + (ReferenceHeight::heightHigh - ReferenceHeight::heightLow) / + static_cast(ReferenceHeight::encoderHigh - ReferenceHeight::encoderLow)) + + ReferenceHeight::heightLow; +} + +/** + * @brief Converts a physical desk height to its corresponding encoder value. + * + * @param height Physical desk height. + * @return uint16_t Encoder value mapped from the configured height range. + */ +uint16_t DeskService::encode(float height) +{ + return static_cast( + lroundf(((height - ReferenceHeight::heightLow) * + static_cast(ReferenceHeight::encoderHigh - ReferenceHeight::encoderLow) / + (ReferenceHeight::heightHigh - ReferenceHeight::heightLow)) + + static_cast(ReferenceHeight::encoderLow))); +} + +/** + * @brief Appends descriptions of recorded leg initialization and communication errors. + * + * @param list JSON array to append to. + */ +void DeskService::getErrors(JsonArray &list) +{ + if ((errorInit & 0b1U) != 0U) + { + list.add("probe A: no response"); + } + if ((errorInit & (0b1U << 1U)) != 0U) + { + list.add("probe A: checksum mismatch"); + } + if ((errorInit & (0b1U << 2U)) != 0U) + { + list.add("probe B: no response"); + } + if ((errorInit & (0b1U << 3U)) != 0U) + { + list.add("probe B: checksum mismatch"); + } + if ((error8 & 0b1U) != 0U) + { + list.add("node 8: no response"); + } + if ((error8 & (0b1U << 1U)) != 0U) + { + list.add("node 8: checksum mismatch"); + } + if ((error9 & 0b1U) != 0U) + { + list.add("node 9: no response"); + } + if ((error9 & (0b1U << 1U)) != 0U) + { + list.add("node 9: checksum mismatch"); + } +} + /** * @brief Updates the down-button state and requests a state publication. * @@ -370,39 +414,131 @@ void DeskService::setDriveUp(bool state) } /** - * @brief Updates the encoder 8 position and marks the desk state for saving and publication. + * @brief Records node 8 communication errors and signals an error state. + * + * @param flags Error bitmask with bit 0 for no response and bit 1 for a checksum mismatch. + */ +void DeskService::setError8(uint8_t flags) +{ + if (flags != error8) + { + error8 = flags; + pending = true; + } + statusRed(); +} + +/** + * @brief Records node 9 communication errors and signals an error state. * - * Updates the status indicator when the position changes. + * @param flags Error bitmask with bit 0 for no response and bit 1 for a checksum mismatch. + */ +void DeskService::setError9(uint8_t flags) +{ + if (flags != error9) + { + error9 = flags; + pending = true; + } + statusRed(); +} + +/** + * @brief Records leg initialization errors and signals an error state. * - * @param position New encoder 8 position. + * @param flags Error bitmask with response and checksum failures in bits 0 and 1 for probe A and bits 2 and 3 for + * probe B. */ -void DeskService::setEncoder8(uint16_t position) +void DeskService::setErrorInit(uint8_t flags) { - if (position != encoder8) + if (flags != errorInit) + { + errorInit = flags; + pending = true; + } + statusRed(); +} + +/** + * @brief Updates node 8 data and clears its communication error. + * + * Changes are marked for publication, and position changes are also marked for persistence. + * + * @param position Encoder position reported by the node. + * @param state State reported by the node. + */ +void DeskService::setNode8(uint16_t position, uint8_t state) +{ + if (position != encoder8 && state != state8) { encoder8 = position; + state8 = state; + error8 = 0U; saved = false; pending = true; statusNode(); } + else if (position != encoder8) + { + encoder8 = position; + error8 = 0U; + saved = false; + pending = true; + statusNode(); + } + else if (state != state8) + { + state8 = state; + error8 = 0U; + pending = true; + statusNode(); + } + else if (error8 != 0U) + { + error8 = 0U; + pending = true; + } } /** - * @brief Updates the secondary encoder value. + * @brief Updates node 9 data and clears its communication error. * - * Marks the device state for persistence and publication when the value changes. + * Changes are marked for publication, and position changes are also marked for persistence. * - * @param position New secondary encoder value. + * @param position Encoder position reported by the node. + * @param state State reported by the node. */ -void DeskService::setEncoder9(uint16_t position) +void DeskService::setNode9(uint16_t position, uint8_t state) { - if (position != encoder9) + if (position != encoder9 && state != state9) + { + encoder9 = position; + state9 = state; + error9 = 0U; + saved = false; + pending = true; + statusNode(); + } + else if (position != encoder9) { encoder9 = position; + error9 = 0U; saved = false; pending = true; statusNode(); } + else if (state != state9) + { + state9 = state; + error9 = 0U; + pending = true; + statusNode(); + } + else if (error9 != 0U) + { + error9 = 0U; + pending = true; + } } /** @@ -427,6 +563,11 @@ void DeskService::setOutputEnable(bool state) #endif // PIN_OE } +/** + * @brief Requests device-state publication on the next service cycle. + */ +void DeskService::setPending() { pending = true; } + /** * @brief Sets the high preset value and marks the device state for persistence and publication. * @@ -479,36 +620,6 @@ void DeskService::setRx(std::span payload) } } -/** - * @brief Updates the state of drive 8. - * - * @param state New drive state. - */ -void DeskService::setState8(uint8_t state) -{ - if (state != state8) - { - state8 = state; - pending = true; - statusNode(); - } -} - -/** - * @brief Updates the motor state for encoder 9. - * - * @param state New motor state. - */ -void DeskService::setState9(uint8_t state) -{ - if (state != state9) - { - state9 = state; - pending = true; - statusNode(); - } -} - /** * @brief Updates the stored transmitted serial payload. * @@ -669,11 +780,27 @@ void DeskService::onInterruptDown() /** * @brief Updates the reset state and status indicator from the reset input. + * + * Clears recorded communication errors and removes captured serial payloads from subsequent publications while reset + * is asserted. */ void DeskService::onInterruptReset() { desk.reset = digitalRead(PIN_RST) == LOW; - desk.reset ? desk.status.setNone(true) : desk.status.setWhite(); + if (desk.reset) + { + desk.error8 = 0U; + desk.error9 = 0U; + desk.errorInit = 0U; + desk.lengthRx = 0U; + desk.lengthTx = 0U; + desk.console.reset(); + desk.status.setNone(true); + } + else + { + desk.status.setWhite(); + } desk.pending = true; } diff --git a/src/esp/HomeAssistantHandler.cpp b/src/esp/HomeAssistantHandler.cpp index f472098..7c22eb3 100644 --- a/src/esp/HomeAssistantHandler.cpp +++ b/src/esp/HomeAssistantHandler.cpp @@ -250,7 +250,7 @@ void HomeAssistantHandler::configuration() * @brief Configures diagnostic entities for Home Assistant discovery. * * Adds diagnostic controls and sensors for calibration, encoder data, firmware - * versions, positional offset, serial activity, temperature, Wi-Fi signal + * versions, communication errors, positional offset, serial activity, temperature, Wi-Fi signal * strength, and optionally button inputs and power-supply voltage. Diagnostic * entities are categorized and selected hardware-specific entities are disabled * by default. @@ -307,6 +307,16 @@ void HomeAssistantHandler::diagnostic() calibrate[ComponentAbbreviations::platform].set("button"); calibrate[ComponentAbbreviations::unique_id].set("calibrate"); } + { + JsonObject errors{discovery[ComponentAbbreviations::components]["error"].to()}; + errors[ComponentAbbreviations::entity_category].set(entityCategory); + errors[ComponentAbbreviations::icon].set("mdi:alert-outline"); + errors[ComponentAbbreviations::name].set("Errors"); + errors[ComponentAbbreviations::platform].set("sensor"); + errors[ComponentAbbreviations::state_topic].set(stateTopic); + errors[ComponentAbbreviations::unique_id].set("error"); + errors[ComponentAbbreviations::value_template].set("{{value_json.errors|join(', ')}}"); + } { JsonObject firmware{discovery[ComponentAbbreviations::components]["firmware"].to()}; firmware[ComponentAbbreviations::enabled_by_default].set(false);