commit e1dcd557571a5750185871ede2f0c6ad18f31a93 Author: Dr. Julian-Steffen Müller Date: Wed Jul 15 10:49:03 2020 +0200 Inital commit. Meisten features sind implementiert und müssen getestet werden diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4236767 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode +CMakeFiles +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b0e3850 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.10) +project(thrawn) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_EXPORT_COMPILE_COMMANDS true) + +include_directories("/usr/local/include") +include_directories("/usr/local/include/mqttpp-0.1") +link_directories("/usr/local/lib/mqttpp-0.1") + +find_package(spdlog CONFIG REQUIRED) +include(FindLua) +find_package(lua REQUIRED) + +add_executable(thrawn src/main.cpp src src/thrawn_api.cpp src/Service.cpp src/Cache.cpp src/LuaEngine.cpp) +target_link_libraries(thrawn ${LIBS} spdlog::spdlog ${LUA_LIBRARIES} mqttpp) +target_include_directories(thrawn PRIVATE ${LUA_INCLUDE_DIR}) +install(TARGETS thrawn DESTINATION bin) \ No newline at end of file diff --git a/src/Cache.cpp b/src/Cache.cpp new file mode 100644 index 0000000..642f968 --- /dev/null +++ b/src/Cache.cpp @@ -0,0 +1,28 @@ +#include "Cache.h" +#include +std::vector Cache::topic_states(std::vector& topics) { + std::vector states; + + for (const auto& topic : topics) { + auto hit = std::find_if(cache.begin(), cache.end(), [&](TopicState& cache_state) { + return cache_state.topic == topic; + }); + if (hit != cache.end()) { + states.push_back(*hit); + } + } + + return states; +} + +TopicState Cache::set_topic_value(const std::string& topic, const std::string& value) { + for (auto& state : cache) { + if (state.topic == topic) { + state.value = value; + + return state; + } + } + + return cache.emplace_back(topic, value); +} \ No newline at end of file diff --git a/src/Cache.h b/src/Cache.h new file mode 100644 index 0000000..3652a2a --- /dev/null +++ b/src/Cache.h @@ -0,0 +1,14 @@ +#if !defined(CACHE_H) +#define CACHE_H +#include +#include +#include "Service.h" +class Cache { + private: + std::vector cache; + public: + std::vector topic_states(std::vector& topics); + TopicState set_topic_value(const std::string& topic, const std::string& value); +}; + +#endif // CACHE_H diff --git a/src/LuaEngine.cpp b/src/LuaEngine.cpp new file mode 100644 index 0000000..1ebed9b --- /dev/null +++ b/src/LuaEngine.cpp @@ -0,0 +1,32 @@ +#include "LuaEngine.h" + +LuaEngine::LuaEngine() { +} + +void LuaEngine::load_services() { + for (auto& service : services) { + service.load(); + } +} + +void LuaEngine::start_services() { + for (auto& service : services) { + service.start(); + } +} + +LuaService* LuaEngine::create_service(std::string filename) { + services.emplace_back(filename); + + return &services.back(); +} + +LuaService* LuaEngine::service_by_lua_state(lua_State* lua) { + for (auto& service : services) { + if (service.lua == lua) { + return &service; + } + } + + return nullptr; +} \ No newline at end of file diff --git a/src/LuaEngine.h b/src/LuaEngine.h new file mode 100644 index 0000000..6f14dd1 --- /dev/null +++ b/src/LuaEngine.h @@ -0,0 +1,32 @@ +#if !defined(LUA_ENGINE_H) +#define LUA_ENGINE_H +#include "Service.h" +#include "Cache.h" +#include +#include +#include + +class LuaEngine { + private: + LuaEngine(); + public: + std::function publish; + static LuaEngine& get_instance() { + static LuaEngine instance; + + return instance; + } + std::vector services; + + Cache cache; + + void load_services(); + void start_services(); + LuaService* create_service(std::string filename); + LuaService* service_by_lua_state(lua_State* lua); + + LuaEngine(LuaEngine const&) = delete; + void operator=(LuaEngine const&) = delete; +}; + +#endif // LUA_ENGINE_H diff --git a/src/Service.cpp b/src/Service.cpp new file mode 100644 index 0000000..17bf4b3 --- /dev/null +++ b/src/Service.cpp @@ -0,0 +1,110 @@ +#include "Service.h" +#include +#include "thrawn_api.h" +#include "spdlog/spdlog.h" + +std::string TopicState::to_string() { + std::string str = "[ Topic: '"; + str += this->topic; + str += "', Value: '"; + str += this->value; + str +="' ]"; + + return str; +} + +std::optional TopicState::create_from_lua(lua_State* lua, int lua_table_stack_index) { + if (! lua_istable(lua, lua_table_stack_index)) { + return std::nullopt; + } + + TopicState state; + + lua_getfield(lua, lua_table_stack_index, "topic"); + state.topic = lua_tostring(lua, -1); + lua_pop(lua, 1); + + lua_getfield(lua, lua_table_stack_index, "value"); + state.value = lua_tostring(lua, -1); + lua_pop(lua, 1); + + return state; +} + +LuaService::LuaService(std::string filename) { + lua = luaL_newstate(); + name = ""; + started = false; + loaded = false; + + luaL_openlibs(lua); + + lua_pushcfunction(lua, thrawn_log); + lua_setglobal(lua, "thrawn_log"); + + lua_pushcfunction(lua, thrawn_register_service); + lua_setglobal(lua, "thrawn_register_service"); + + lua_pushcfunction(lua, thrawn_subscribe); + lua_setglobal(lua, "thrawn_subscribe"); + + lua_pushcfunction(lua, thrawn_publish); + lua_setglobal(lua, "thrawn_publish"); + + lua_pushcfunction(lua, thrawn_create_topic_state); + lua_setglobal(lua, "thrawn_create_topic_state"); + + lua_pushcfunction(lua, thrawn_topic_state_str); + lua_setglobal(lua, "thrawn_topic_state_str"); + + lua_pushcfunction(lua, thrawn_topic_cache); + lua_setglobal(lua, "thrawn_topic_cache"); + + + if (luaL_loadfile(lua, filename.c_str()) != 0) { + throw std::runtime_error("Error during loading of '" + filename + "' - [Lua error: '" + lua_tostring(lua,-1) + "']."); + } +} + +LuaService::~LuaService() { + if (lua != nullptr) { + lua_close(lua); + } +} + +LuaService::LuaService(LuaService&& service) noexcept { + lua = service.lua; + service.lua = nullptr; + name = std::move(service.name); +} + +LuaService& LuaService::operator=(LuaService&& service) noexcept { + lua = service.lua; + service.lua = nullptr; + name = std::move(service.name); + + return *this; +} + +void LuaService::load() { + if (!loaded) { + loaded = true; + + if (lua_pcall(lua, 0, 0 ,0) != 0) { + spdlog::error("[{}] Unable to call 'lua_pcall()' in load(). [Lua: '{}']", name, lua_tostring(lua,-1)); + } + } +} + +void LuaService::start() { + if (!started && loaded) { + started = true; + call_thrawn_on_start(*this); + } +} + +void LuaService::on_update(const TopicState& state) { + if (started && loaded) { + call_thrawn_on_update(*this, state); + } +} \ No newline at end of file diff --git a/src/Service.h b/src/Service.h new file mode 100644 index 0000000..39dc603 --- /dev/null +++ b/src/Service.h @@ -0,0 +1,50 @@ +#if !defined(SERVICE_H) +#define SERVICE_H + +extern "C" +{ +#include +#include +#include +} +#include +#include +#include +#include + +struct TopicState { + std::string topic; + std::string value; + + TopicState() : topic(""), value(""){} + TopicState(const std::string& topic, const std::string& value) : topic(topic), value(value){} + + static std::optional create_from_lua(lua_State* lua, int lua_table_stack_index); + + std::string to_string(); +}; + +struct LuaService { + public: + lua_State* lua; + std::string name; + std::vector subscriptions; + + LuaService(std::string filename); + ~LuaService(); + + LuaService(const LuaService&) = delete; + LuaService& operator=(const LuaService&) = delete; + LuaService(LuaService&&) noexcept; //Noexcept is important because of std::vector + LuaService& operator=(LuaService&&) noexcept; //Noexcept is important because of std::vector + + void load(); + void start(); + void on_update(const TopicState& topic_state); + + private: + bool loaded; + bool started; +}; + +#endif // SERVICE_H \ No newline at end of file diff --git a/src/Version.h b/src/Version.h new file mode 100644 index 0000000..7fb0ac7 --- /dev/null +++ b/src/Version.h @@ -0,0 +1,8 @@ +#if !defined(VERSION_H) +#define VERSION_H + +#define THRAWN_MAJOR_VERSION 1 +#define THRAWN_MINOR_VERSION 0 +#define THRAWN_PATCH_VERSION 0 + +#endif // VERSION_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..4070ee6 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include "Service.h" +#include "thrawn_api.h" +#include "mqttpp.h" +#include "spdlog/spdlog.h" +#include "Cache.h" +#include "LuaEngine.h" +#include +#include "Version.h" +#include + +std::vector lua_scripts_in_folder(const std::string& path_str) { + std::vector scripts; + + std::filesystem::path scripts_folder(path_str); + + if (std::filesystem::exists(scripts_folder)) { + for (const auto& file: std::filesystem::directory_iterator(scripts_folder)) { + if (file.is_regular_file() && file.path().extension() == ".lua") { + scripts.push_back(file.path().string()); + } + } + }else{ + throw std::invalid_argument("Folder '" + path_str + "' does not exist."); + } + + return scripts; +} + +int main(int argc, char* argv[]) { + std::string services_path_str = "/etc/thrawn"; + std::string mqttServerURI = "tcp://chimaera:1883"; + mqttpp::LastWill last_will {mqttServerURI, "0"}; + std::string lastWillConnected = "1"; + + + /* + * Parsing arguments + * Show version or load path + */ + if (argc > 1) { + if (strcmp(argv[1], "-v") == 0 || strcmp(argv[1], "--version") == 0) { + std::cout << "Version: " << THRAWN_MAJOR_VERSION << "." << THRAWN_MINOR_VERSION << "." << THRAWN_PATCH_VERSION << std::endl; + + return 0; + } + + services_path_str = argv[1]; + } + + /* + * MQTT + */ + spdlog::info("Starting MQTT connection to '{}' .", mqttServerURI); + + mqttpp::Client mqtt(mqttServerURI, last_will); + + if (! mqtt.connect()) { + spdlog::error("Failed to establishe mqtt connection to ‘{}'", mqttServerURI); + + return 1; + } + + mqtt.publish(last_will.topic, lastWillConnected); + mqtt.subscribe("#", [&] (const char* topic, const std::string& msg) { + LuaEngine::get_instance().cache.set_topic_value(topic, msg); + + for (auto& service : LuaEngine::get_instance().services) { + for (const std::string& sub_topic : service.subscriptions) { + if (sub_topic == topic) { + service.on_update(TopicState(topic, msg)); + } + } + } + }); + + LuaEngine::get_instance().publish = [&](const TopicState& state) { + mqtt.publish(state.topic, state.value); + }; + + spdlog::info("Connection to MQTT server '{}' successfull.", mqttServerURI); + + try { + for (const auto& script : lua_scripts_in_folder(services_path_str)) { + LuaEngine::get_instance().create_service(script); + } + }catch(const std::exception& e) { + spdlog::error(e.what()); + + return 1; + } + + /* + * LUA Engine + */ + spdlog::info("Starting Thrawn services on Chimaera."); + + LuaEngine::get_instance().load_services(); + + spdlog::info("{} service(s) loaded.", LuaEngine::get_instance().services.size()); + + LuaEngine::get_instance().start_services(); + + spdlog::info("All services loaded."); + + while (true) {} + + return 0; +} \ No newline at end of file diff --git a/src/thrawn_api.cpp b/src/thrawn_api.cpp new file mode 100644 index 0000000..75446c6 --- /dev/null +++ b/src/thrawn_api.cpp @@ -0,0 +1,191 @@ +#include "thrawn_api.h" +#include +#include "Cache.h" +#include "LuaEngine.h" +#include "spdlog/spdlog.h" + +/* + HELPER +*/ +void push_topic_state_table(lua_State* lua, TopicState state) { + lua_newtable(lua); + lua_pushliteral(lua, "topic"); + lua_pushstring(lua, state.topic.c_str()); + lua_settable(lua, -3); + + lua_pushliteral(lua, "value"); + lua_pushstring(lua, state.value.c_str()); + lua_settable(lua, -3); +} + +void push_list_topic_state(lua_State* lua, std::vector states) { + lua_newtable(lua); + + int i = 1; + for (auto& state : states) { + lua_pushinteger(lua, i); + push_topic_state_table(lua, state); + lua_settable(lua, -3); + + i++; + } +} + +//Ab hier ist ok +int thrawn_log(lua_State* lua) +{ + int paramCount = lua_gettop(lua); + + if(paramCount != 1) + { + return -1; + } + + if (!lua_isstring (lua, -1)) { + return -1; + } + + spdlog::info("[{}] {}", LuaEngine::get_instance().service_by_lua_state(lua)->name, lua_tostring(lua, -1)); + + return 0; +} + +int thrawn_register_service(lua_State* lua) { + int paramCount = lua_gettop(lua); + + if(paramCount != 1) + { + return -1; + } + + LuaEngine::get_instance().service_by_lua_state(lua)->name = lua_tostring(lua, -1); + + spdlog::info("Service '{}' registered", lua_tostring(lua, -1)); + + return 0; +} + +int thrawn_create_topic_state(lua_State* lua) { + int paramCount = lua_gettop(lua); + + if(paramCount < 0 || paramCount > 3) + { + return -1; + } + + TopicState state; + + if (paramCount == 1) { + if (!lua_isstring (lua, -1)) { + return -1; + } + state.topic = lua_tostring(lua, -1); + }else if (paramCount == 2) { + if (!lua_isstring (lua, -2) && !lua_isstring (lua, -1)) { + return -1; + } + state.topic = lua_tostring(lua, -2); + state.value = lua_tostring(lua, -1); + } + + push_topic_state_table(lua, state); + + return 1; +} + +int thrawn_topic_state_str(lua_State* lua) { + int paramCount = lua_gettop(lua); + + if(paramCount != 1) + { + return -1; + } + + std::optional state = TopicState::create_from_lua(lua, -1); + + if (! state.has_value()) { + return -1; + } + + lua_pushstring(lua, state.value().to_string().c_str()); + + return 1; +} + + +int thrawn_subscribe(lua_State* lua) { + int paramCount = lua_gettop(lua); + + if(paramCount != 1){ + return -1; + } + + LuaService* service = LuaEngine::get_instance().service_by_lua_state(lua); + + if (service->name == "") { + spdlog::error("Not registered service tried to subscribe to '{}'", lua_tostring(lua, -1)); + + return 0; + } + + service->subscriptions.push_back(lua_tostring(lua, -1)); + + spdlog::info("Service '{}' subscribed to '{}'", service->name, lua_tostring(lua, -1)); + + return 0; +} + +int thrawn_topic_cache(lua_State* lua) { + int paramCount = lua_gettop(lua); + + if(paramCount != 0){ + return -1; + } + + LuaEngine& lua_engine = LuaEngine::get_instance(); + LuaService* service = lua_engine.service_by_lua_state(lua); + + push_list_topic_state(lua, lua_engine.cache.topic_states(service->subscriptions)); + + return 1; +} +// thrawn_publish(topic, value) +int thrawn_publish(lua_State* lua) { + int paramCount = lua_gettop(lua); + + if (paramCount != 1) { + return -1; + } + + std::optional state = TopicState::create_from_lua(lua, -1); + + if (! state.has_value()) { + return -1; + } + + LuaEngine::get_instance().publish(state.value()); + + return 0; +} + +void call_thrawn_on_start(LuaService& service) { + lua_getglobal(service.lua, "thrawn_on_start"); + + if (lua_pcall(service.lua,0, 0, 0) != 0) { + spdlog::error("[{}] Unable to call 'thrawn_on_start'. [Lua: '{}']", service.name, lua_tostring(service.lua,-1)); + + return; + } +} + +void call_thrawn_on_update(LuaService& service, TopicState state) { + lua_getglobal(service.lua, "thrawn_on_update"); + + push_topic_state_table(service.lua, state); + + if (lua_pcall(service.lua, 1, 0, 0) != 0) { + spdlog::error("[{}] Unable to call 'thrawn_on_update'. [Lua: '{}']", service.name, lua_tostring(service.lua,-1)); + + return; + } +} \ No newline at end of file diff --git a/src/thrawn_api.h b/src/thrawn_api.h new file mode 100644 index 0000000..a3279bd --- /dev/null +++ b/src/thrawn_api.h @@ -0,0 +1,35 @@ +#if !defined(THRAWN_API_H) +#define THRAWN_API_H + +extern "C" +{ +#include +#include +#include +} + +#include +#include +#include "Service.h" + +//Functions for LUA +int thrawn_log(lua_State* lua); + +int thrawn_register_service(lua_State* lua); + +int thrawn_subscribe(lua_State* lua); + +int thrawn_publish(lua_State* lua); + +int thrawn_create_topic_state(lua_State* lua); + +int thrawn_topic_state_str(lua_State* lua); + +int thrawn_topic_cache(lua_State* lua); + +// Function in LUA +void call_thrawn_on_start(LuaService& service); + +void call_thrawn_on_update(LuaService& service, TopicState state); + +#endif // THRAWN_API_H \ No newline at end of file