diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bcf1bf5b..85bf6b2a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ on: - 'platformio.ini' - '.github/workflows/tests.yml' concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.head_ref || github.ref_name }} cancel-in-progress: true jobs: diff --git a/conf/conf.hpp b/conf/conf.hpp index c148c526..192a8f48 100644 --- a/conf/conf.hpp +++ b/conf/conf.hpp @@ -144,6 +144,9 @@ namespace fabomatic /// @brief Backend reply (sub-topic of the full machine topic) static constexpr std::string_view response_topic{"/reply"}; + /// @brief Backend requests (sub-topic of the full machine topic) + static constexpr std::string_view request_topic{"/request"}; + /// @brief Number of tries to get a reply from the backend static constexpr auto MAX_TRIES{2}; diff --git a/include/BoardLogic.hpp b/include/BoardLogic.hpp index 38a7f387..0301d089 100644 --- a/include/BoardLogic.hpp +++ b/include/BoardLogic.hpp @@ -79,6 +79,7 @@ namespace fabomatic [[nodiscard]] auto getMachine() const -> const Machine &; [[nodiscard]] auto authorize(const card::uid_t uid) -> bool; [[nodiscard]] auto getHostname() const -> const std::string; + auto processBackendRequests() -> void; // copy reference BoardLogic &operator=(const BoardLogic &board) = delete; diff --git a/include/FabBackend.hpp b/include/FabBackend.hpp index d8a0a60b..33bbf397 100644 --- a/include/FabBackend.hpp +++ b/include/FabBackend.hpp @@ -45,8 +45,10 @@ namespace fabomatic std::string topic{""}; std::string response_topic{""}; + std::string request_topic{""}; std::string last_query{""}; std::string last_reply{""}; + std::string last_request{""}; bool online{false}; bool answer_pending{false}; @@ -55,6 +57,7 @@ namespace fabomatic Buffer buffer; auto messageReceived(String &topic, String &payload) -> void; + auto requestReceived(String &topic, String &payload) -> void; template [[nodiscard]] auto publish(const QueryT &payload) -> PublishResult; @@ -86,6 +89,8 @@ namespace fabomatic [[nodiscard]] auto transmitBuffer() -> bool; [[nodiscard]] auto saveBuffer() -> bool; + [[nodiscard]] auto checkBackendRequest() -> std::optional>; + auto connect() -> bool; auto connectWiFi() -> bool; auto loop() -> bool; diff --git a/include/MQTTtypes.hpp b/include/MQTTtypes.hpp index 95766c26..d292cf6c 100644 --- a/include/MQTTtypes.hpp +++ b/include/MQTTtypes.hpp @@ -29,7 +29,7 @@ namespace fabomatic::MQTTInterface const card::uid_t uid; UserQuery() = delete; - constexpr UserQuery(card::uid_t card_uid) : uid(card_uid){}; + constexpr UserQuery(card::uid_t card_uid) : uid(card_uid) {}; [[nodiscard]] auto waitForReply() const -> bool override { return true; }; [[nodiscard]] auto payload() const -> const std::string override; @@ -63,7 +63,7 @@ namespace fabomatic::MQTTInterface const card::uid_t uid; StartUseQuery() = delete; - constexpr StartUseQuery(card::uid_t card_uid) : uid(card_uid){}; + constexpr StartUseQuery(card::uid_t card_uid) : uid(card_uid) {}; [[nodiscard]] auto payload() const -> const std::string override; [[nodiscard]] auto waitForReply() const -> bool override { return true; }; @@ -83,7 +83,7 @@ namespace fabomatic::MQTTInterface /// @param card_uid machine user card id /// @param mid machine id /// @param duration duration of usage, in seconds - constexpr StopUseQuery(card::uid_t card_uid, std::chrono::seconds duration) : uid(card_uid), duration_s(duration){}; + constexpr StopUseQuery(card::uid_t card_uid, std::chrono::seconds duration) : uid(card_uid), duration_s(duration) {}; [[nodiscard]] auto payload() const -> const std::string override; [[nodiscard]] auto waitForReply() const -> bool override { return true; }; [[nodiscard]] auto buffered() const -> bool override { return true; }; @@ -102,7 +102,7 @@ namespace fabomatic::MQTTInterface /// @param card_uid machine user card id /// @param mid machine id /// @param duration duration of usage, in seconds - constexpr InUseQuery(card::uid_t card_uid, std::chrono::seconds duration) : uid(card_uid), duration_s(duration){}; + constexpr InUseQuery(card::uid_t card_uid, std::chrono::seconds duration) : uid(card_uid), duration_s(duration) {}; [[nodiscard]] auto payload() const -> const std::string override; [[nodiscard]] auto waitForReply() const -> bool override { return true; }; [[nodiscard]] auto buffered() const -> bool override { return false; }; @@ -115,7 +115,7 @@ namespace fabomatic::MQTTInterface const card::uid_t uid; RegisterMaintenanceQuery() = delete; - constexpr RegisterMaintenanceQuery(card::uid_t card_uid) : uid(card_uid){}; + constexpr RegisterMaintenanceQuery(card::uid_t card_uid) : uid(card_uid) {}; [[nodiscard]] auto payload() const -> const std::string override; [[nodiscard]] auto waitForReply() const -> bool override { return true; }; @@ -129,7 +129,7 @@ namespace fabomatic::MQTTInterface const bool request_ok{false}; /* True if the request was processed by the server */ Response() = delete; - constexpr Response(bool result) : request_ok(result){}; + constexpr Response(bool result) : request_ok(result) {}; }; /// @brief Result code for user authentication result @@ -150,10 +150,10 @@ namespace fabomatic::MQTTInterface FabUser::UserLevel user_level{FabUser::UserLevel::Unknown}; /* User priviledges */ UserResponse() = delete; - UserResponse(bool rok) : Response(rok){}; + UserResponse(bool rok) : Response(rok) {}; UserResponse(bool rok, UserResult res) : Response(rok), - result(static_cast(res)){}; + result(static_cast(res)) {}; [[nodiscard]] static auto fromJson(JsonDocument &doc) -> std::unique_ptr; @@ -174,7 +174,7 @@ namespace fabomatic::MQTTInterface uint16_t grace{0}; /* Grace period in minutes */ std::string description{""}; /* Description of the expired maintenance */ MachineResponse() = delete; - MachineResponse(bool rok) : Response(rok){}; + MachineResponse(bool rok) : Response(rok) {}; [[nodiscard]] static auto fromJson(JsonDocument &doc) -> std::unique_ptr; }; @@ -184,10 +184,19 @@ namespace fabomatic::MQTTInterface { public: SimpleResponse() = delete; - constexpr SimpleResponse(bool rok) : Response(rok){}; + constexpr SimpleResponse(bool rok) : Response(rok) {}; [[nodiscard]] static auto fromJson(JsonDocument &doc) -> std::unique_ptr; }; + /// @brief Class for server request + class BackendRequest + { + public: + card::uid_t requester; + std::string request_type; + BackendRequest(const card::uid_t &uid, const std::string &request) : requester{uid}, request_type(request) {}; + [[nodiscard]] static auto fromJson(const JsonDocument &doc) -> std::optional>; + }; } // namespace fabomatic::MQTTInterface #endif // MQTTTYPES_HPP_ diff --git a/include/card.hpp b/include/card.hpp index a93920fe..54996b5c 100644 --- a/include/card.hpp +++ b/include/card.hpp @@ -31,6 +31,22 @@ namespace fabomatic::card return ss.str(); } + /** + * @brief Returns an UID from its string representation + * @param str hex string to convert + * @return uid value + */ + [[nodiscard]] inline auto str_uid(const std::string &str) -> card::uid_t + { + uint64_t ll_value; + + std::stringstream ss{}; + ss << std::hex << str; + ss >> ll_value; + + return ll_value; + } + /** * @brief Converts a UID from an array of bytes to a number * @param uid array of bytes diff --git a/platformio.ini b/platformio.ini index ede3f3d7..6c840e7b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -14,6 +14,7 @@ name = fab-o-matic [env] platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.08.10/platform-espressif32.zip +#platform =https://github.com/pioarduino/platform-espressif32/releases/download/51.03.04/platform-espressif32.zip framework = arduino test_framework = unity check_tool = clangtidy @@ -31,8 +32,9 @@ lib_deps = https://github.com/PBrunot/LiquidCrystal.git#use_const adafruit/Adafruit NeoPixel@^1.12.3 https://github.com/tzapu/WiFiManager.git@2.0.17 ArduinoOTA -build_unflags = -std=gnu++11 -fexceptions +build_unflags = -std=gnu++11 -fexceptions -fno-lto build_flags = -std=gnu++2b + -flto=auto -g3 -Os -I conf diff --git a/src/BoardLogic.cpp b/src/BoardLogic.cpp index 3aa6b47d..f3f3ca5a 100644 --- a/src/BoardLogic.cpp +++ b/src/BoardLogic.cpp @@ -652,4 +652,28 @@ namespace fabomatic std::to_string(conf::default_config::machine_id.id); } + auto BoardLogic::processBackendRequests() -> void + { + auto &backend = getServer(); + backend.loop(); + if (auto result = backend.checkBackendRequest(); result.has_value()) + { + const auto &req = result->get(); + ESP_LOGI(TAG, "Processing backend request : %s", req->request_type.c_str()); + FabUser fu{req->requester, "BACKEND", true, FabUser::UserLevel::FabAdmin}; + + if (req->request_type == "start") + { + logout(); + if (!authorize(fu.card_uid)) + { + ESP_LOGE(TAG, "Failure to execute start request from backend"); + } + } + if (req->request_type == "stop") + { + logout(); + } + } + } } // namespace fabomatic \ No newline at end of file diff --git a/src/FabBackend.cpp b/src/FabBackend.cpp index 13995b74..8c0fe326 100644 --- a/src/FabBackend.cpp +++ b/src/FabBackend.cpp @@ -245,10 +245,23 @@ namespace fabomatic */ void FabBackend::messageReceived(String &s_topic, String &s_payload) { - ESP_LOGI(TAG, "MQTT Client: Received on %s -> %s", s_topic.c_str(), s_payload.c_str()); + ESP_LOGI(TAG, "MQTT Client: message received on %s -> %s", s_topic.c_str(), s_payload.c_str()); - last_reply.assign(s_payload.c_str()); - answer_pending = false; + // Needed for equality test below + std::string_view view_topic{s_topic.c_str()}; + if (view_topic == this->response_topic) + { + last_reply.assign(s_payload.c_str()); + answer_pending = false; + } + else if (view_topic == this->request_topic) + { + last_request.assign(s_payload.c_str()); + } + else if (view_topic != this->topic) + { + ESP_LOGW(TAG, "MQTT Client: unrecognized topic %s", s_topic.c_str()); + } } /** @@ -343,9 +356,9 @@ namespace fabomatic // Setup subscriptions if (client.connected()) { - std::stringstream tmp_topic; - tmp_topic << topic << conf::mqtt::response_topic; - response_topic.assign(tmp_topic.str()); + std::stringstream ss_resp{}; + ss_resp << topic << conf::mqtt::response_topic; + response_topic.assign(ss_resp.str()); if (!client.subscribe(response_topic.c_str())) { @@ -356,6 +369,21 @@ namespace fabomatic ESP_LOGD(TAG, "MQTT Client: subscribed to reply topic %s", response_topic.c_str()); online = true; } + + std::stringstream ss_req{}; + ss_req << topic << conf::mqtt::request_topic; + request_topic.assign(ss_req.str()); + + if (!client.subscribe(request_topic.c_str())) + { + ESP_LOGE(TAG, "MQTT Client: failure to subscribe to requests topic %s", request_topic.c_str()); + } + else + { + ESP_LOGD(TAG, "MQTT Client: subscribed to requests topic %s", request_topic.c_str()); + online = true; + } + // Announce the board to the server if (auto query = MQTTInterface::AliveQuery{}; publish(query) == PublishResult::PublishedWithoutAnswer) { @@ -622,7 +650,7 @@ namespace fabomatic } } } - last_reply = ""; + last_reply.clear(); ESP_LOGW(TAG, "Retransmittion completed, remaining messages=%d", buffer.count()); return !hasBufferedMsg(); @@ -655,4 +683,24 @@ namespace fabomatic buffer.setChanged(false); ESP_LOGI(TAG, "Loaded buffer with %d messages", buffer.count()); } + + auto FabBackend::checkBackendRequest() -> std::optional> + { + if (!this->last_request.empty()) + { + const auto payload = last_request.c_str(); + if (DeserializationError error = deserializeJson(doc, payload)) + { + ESP_LOGE(TAG, "Failed to parse json: %s (%s)", payload, error.c_str()); + last_request.clear(); + return std::nullopt; + } + + last_request.clear(); + + return MQTTInterface::BackendRequest::fromJson(doc); + } + return std::nullopt; + } + } // namespace fabomatic diff --git a/src/MQTTtypes.cpp b/src/MQTTtypes.cpp index dc3673b2..7cd31d7c 100644 --- a/src/MQTTtypes.cpp +++ b/src/MQTTtypes.cpp @@ -148,4 +148,16 @@ namespace fabomatic::MQTTInterface auto response = std::make_unique(doc["request_ok"].as()); return response; } + + auto BackendRequest::fromJson(const JsonDocument &doc) -> std::optional> + { + if (doc.containsKey("request_type") && doc.containsKey("uid")) + { + const auto request = doc["request_type"].as(); + const auto struid = doc["uid"].as(); + return std::make_unique(card::str_uid(struid), request); + } + ESP_LOGW(TAG, "Cannot decode backend request from JsonDocument"); + return std::nullopt; + } } // namespace fabomatic::MQTTInterface \ No newline at end of file diff --git a/src/RFIDWrapper.tpp b/src/RFIDWrapper.tpp index cccb3667..1560c0c3 100644 --- a/src/RFIDWrapper.tpp +++ b/src/RFIDWrapper.tpp @@ -21,15 +21,15 @@ namespace fabomatic { const auto result = driver->PICC_IsNewCardPresent(); - if (conf::debug::ENABLE_LOGS && result) - ESP_LOGD(TAG, "isNewCardPresent=%d", result); - - if (disabledUntil && disabledUntil > fabomatic::Tasks::arduinoNow()) + if (disabledUntil.has_value() && disabledUntil > fabomatic::Tasks::arduinoNow()) { ESP_LOGD(TAG, "isNewCardPresent is disabled"); return false; } + if (conf::debug::ENABLE_LOGS && result) + ESP_LOGD(TAG, "isNewCardPresent=%d", result); + return result; } diff --git a/src/main.cpp b/src/main.cpp index b17fb2db..11c3d8fe 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -86,6 +86,7 @@ namespace fabomatic void taskCheckRfid() { Board::logic.checkRfid(); + Board::logic.processBackendRequests(); } /// @brief blink led diff --git a/test/test_chrono/test_chrono.cpp b/test/test_chrono/test_chrono.cpp index bfa455f4..869f2ebe 100644 --- a/test/test_chrono/test_chrono.cpp +++ b/test/test_chrono/test_chrono.cpp @@ -8,6 +8,8 @@ #include "Tasks.hpp" #include "Logging.hpp" #include "Espressif.hpp" +#include "card.hpp" +#include "secrets.hpp" [[maybe_unused]] static const char *TAG4 = "test_chrono"; @@ -27,6 +29,20 @@ namespace fabomatic::tests // set stuff up here } + void test_card(void) + { + for (const auto &[uid, level, name] : secrets::cards::whitelist) + { + auto str_val = card::uid_str(uid); + auto uid2 = card::str_uid(str_val); + auto arr1 = card::to_array(uid); + auto uid3 = card::from_array(arr1); + + TEST_ASSERT_TRUE_MESSAGE(uid2 == uid, "Card UID: string conversion data loss"); + TEST_ASSERT_TRUE_MESSAGE(uid3 == uid, "Card UID: array conversion data loss"); + } + } + void test_steady_clock(void) { static constexpr auto nb_tests = 100; @@ -58,6 +74,7 @@ void setup() esp_log_level_set(TAG4, LOG_LOCAL_LEVEL); UNITY_BEGIN(); RUN_TEST(fabomatic::tests::test_steady_clock); + RUN_TEST(fabomatic::tests::test_card); UNITY_END(); // stop unit testing } diff --git a/test/test_logic/test_common.h b/test/test_logic/test_common.h index 5f708d34..4cac1986 100644 --- a/test/test_logic/test_common.h +++ b/test/test_logic/test_common.h @@ -34,6 +34,12 @@ namespace fabomatic::tests std::optional duration_tap = std::nullopt); void machine_init(BoardLogic &logic, RFIDWrapper &rfid); + + constexpr fabomatic::card::uid_t get_test_uid(size_t idx) + { + auto [card_uid, level, name] = fabomatic::tests::test_whitelist[idx]; + return card_uid; + } } #endif // TEST_COMMON_H_ \ No newline at end of file diff --git a/test/test_logic/test_logic.cpp b/test/test_logic/test_logic.cpp index 2f64bf12..21f96644 100644 --- a/test/test_logic/test_logic.cpp +++ b/test/test_logic/test_logic.cpp @@ -30,12 +30,6 @@ fabomatic::BoardLogic logic; using BoardLogic = fabomatic::BoardLogic; -constexpr fabomatic::card::uid_t get_test_uid(size_t idx) -{ - auto [card_uid, level, name] = fabomatic::tests::test_whitelist[idx]; - return card_uid; -} - namespace fabomatic::tests { void test_machine_defaults() diff --git a/test/test_mqtt/test_mqtt.cpp b/test/test_mqtt/test_mqtt.cpp index 42c5c1d2..d0e1b602 100644 --- a/test/test_mqtt/test_mqtt.cpp +++ b/test/test_mqtt/test_mqtt.cpp @@ -18,6 +18,7 @@ #include #include #include "LiquidCrystal.h" +#include "../test_logic/test_common.h" using namespace std::chrono_literals; @@ -36,6 +37,57 @@ namespace fabomatic::tests std::atomic exit_request{false}; + void busyWait(std::chrono::seconds d = 5s) + { + auto start = fabomatic::Tasks::arduinoNow(); + while (fabomatic::Tasks::arduinoNow() - start <= d) + { + test_scheduler.execute(); + delay(25); + } + } + + /// @brief Simulates RFID card tap + /// @param rfid RFID wrapper for simulation + /// @param logic Board logic, the checkRfid() method will be called repeatedly + /// @param uid card UID to tap + /// @param duration_tap duration of the tap. pass milliseconds::max() to keep the card in the field + /// @return + BoardLogic::Status simulate_rfid_card(RFIDWrapper &rfid, BoardLogic &logic, std::optional uid, + std::optional duration_tap) + { + constexpr auto DEFAULT_CYCLES = 3; + + MockMrfc522 &driver = rfid.getDriver(); + + driver.resetUid(); + rfid.setDisabledUntil(std::nullopt); + + for (auto i = 0; i < DEFAULT_CYCLES; i++) + { + logic.checkRfid(); + rfid.setDisabledUntil(std::nullopt); + } + + if (uid.has_value()) + { + driver.setUid(uid.value(), duration_tap); + TEST_ASSERT_TRUE_MESSAGE(uid == rfid.getUid(), "Card UID not equal"); + auto start = fabomatic::Tasks::arduinoNow(); + do + { + logic.checkRfid(); + rfid.setDisabledUntil(std::nullopt); + delay(50); + } while (duration_tap.has_value() && fabomatic::Tasks::arduinoNow() - start < duration_tap); + } + else if (duration_tap) + { + delay(duration_tap.value().count()); + } + return logic.getStatus(); + } + void *threadMQTTServer(void *arg) { while (!exit_request) @@ -245,6 +297,7 @@ namespace fabomatic::tests void test_taskCheckRfid() { logic.checkRfid(); + logic.processBackendRequests(); } /// @brief blink led @@ -341,6 +394,57 @@ namespace fabomatic::tests // Remove the HW Watchdog esp32::removeWatchdog(); } + + void test_backend_commands() + { + constexpr auto millis_delay = std::chrono::duration_cast(conf::machine::DELAY_BETWEEN_SWEEPS).count(); + delay(millis_delay); + auto card1 = get_test_uid(2); + auto card2 = get_test_uid(1); + + TEST_ASSERT_EQUAL_UINT16_MESSAGE(BoardLogic::Status::MachineFree, logic.getStatus(), "Status not MachineFree (0)"); + + simulate_rfid_card(rfid, logic, card1); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(BoardLogic::Status::LoggedIn, logic.getStatus(), "Status not LoggedIn (0)"); + // Card away + simulate_rfid_card(rfid, logic, std::nullopt); + + std::stringstream ss{}, ss2{}, ss3{}; + ss3 << conf::mqtt::topic << "/" << logic.getMachine().getMachineId().id << conf::mqtt::request_topic; + std::string topic = ss3.str(); + + ss << "{\"request_type\":\"stop\"," + << "\"uid\":\"" << card::uid_str(card2) << "\"" + << "}"; + std::string payload_stop = ss.str(); + broker.publish(topic, payload_stop); + + busyWait(5s); + + // Stop request has been processed + TEST_ASSERT_EQUAL_UINT16_MESSAGE(BoardLogic::Status::MachineFree, logic.getStatus(), "Status not MachineFree (1)"); + TEST_ASSERT_TRUE_MESSAGE(logic.getMachine().isFree(), "Machine should be available"); + + ss2 << "{\"request_type\":\"start\"," + << "\"uid\":\"" << card::uid_str(card2) << "\"" + << "}"; + std::string payload_start = ss2.str(); + broker.publish(topic, payload_start); + + busyWait(5s); + + // Start request has been processed + TEST_ASSERT_EQUAL_UINT16_MESSAGE(BoardLogic::Status::MachineInUse, logic.getStatus(), "Status not MachineInUse (2)"); + TEST_ASSERT_TRUE_MESSAGE(!logic.getMachine().isFree(), "Machine should not be available"); + TEST_ASSERT_TRUE_MESSAGE(logic.getMachine().getActiveUser().card_uid == card2, "Wrong active user"); + + broker.publish(topic, payload_stop); + + busyWait(5s); + + // Stop request has been processed + TEST_ASSERT_EQUAL_UINT16_MESSAGE(BoardLogic::Status::MachineFree, logic.getStatus(), "Status not MachineFree (2)"); + } } // namespace fabomatic::Tests void tearDown(void) {}; @@ -365,6 +469,7 @@ void setup() RUN_TEST(fabomatic::tests::test_check_transmission); RUN_TEST(fabomatic::tests::test_fabserver_calls); RUN_TEST(fabomatic::tests::test_normal_use); + RUN_TEST(fabomatic::tests::test_backend_commands); RUN_TEST(fabomatic::tests::test_stop_broker); UNITY_END(); // stop unit testing