diff --git a/sw/minihil/CMakeLists.txt b/sw/minihil/CMakeLists.txt index 272c60e..3102a15 100644 --- a/sw/minihil/CMakeLists.txt +++ b/sw/minihil/CMakeLists.txt @@ -42,9 +42,14 @@ else() target_compile_definitions(minihild PRIVATE MOCK_GPIO) endif() +# Find OpenSSL +find_package(OpenSSL REQUIRED) + target_link_libraries(minihild PRIVATE nlohmann_json::nlohmann_json + OpenSSL::SSL + OpenSSL::Crypto ) # Install rules (for Yocto installation) diff --git a/sw/minihil/include/network/tcp_server.hpp b/sw/minihil/include/network/tcp_server.hpp index 7110000..63a3729 100644 --- a/sw/minihil/include/network/tcp_server.hpp +++ b/sw/minihil/include/network/tcp_server.hpp @@ -12,7 +12,12 @@ namespace minihil { class TcpServer : public IServer { public: - TcpServer(int port, std::shared_ptr rpcHandler); + TcpServer(int port, std::shared_ptr rpcHandler, + bool useSsl = false, + bool useMtls = false, + const std::string& caPath = "", + const std::string& certPath = "", + const std::string& keyPath = ""); ~TcpServer() override; // Starts the listener thread (non-blocking call) @@ -22,13 +27,25 @@ class TcpServer : public IServer { void stop() override; private: + struct ClientSession { + int socketFd; + void* ssl; // SSL* wrapper + }; + int m_port; std::shared_ptr m_rpcHandler; std::atomic m_running; int m_serverSocket; std::thread m_listenerThread; - std::vector m_clientSockets; + bool m_useSsl; + bool m_useMtls; + std::string m_caPath; + std::string m_certPath; + std::string m_keyPath; + void* m_sslCtx; // SSL_CTX* pointer + + std::vector m_clientSessions; std::vector m_clientThreads; std::mutex m_clientsMutex; diff --git a/sw/minihil/src/hardware/gpiod_relay_controller.cpp b/sw/minihil/src/hardware/gpiod_relay_controller.cpp index 17d506c..53b8175 100644 --- a/sw/minihil/src/hardware/gpiod_relay_controller.cpp +++ b/sw/minihil/src/hardware/gpiod_relay_controller.cpp @@ -60,7 +60,7 @@ bool GpiodRelayController::init() { line_cfg.add_line_settings(offset, ::gpiod::line_settings() .set_direction(::gpiod::line::direction::OUTPUT) - .set_output_value(::gpiod::line::value::INACTIVE) + .set_output_value(::gpiod::line::value::ACTIVE) ); } @@ -82,7 +82,7 @@ bool GpiodRelayController::setRelay(int relayId, bool state) { m_states[relayId] = state; try { unsigned int offset = m_relayGpioMap.at(relayId); - m_impl->request.set_value(offset, state ? ::gpiod::line::value::ACTIVE : ::gpiod::line::value::INACTIVE); + m_impl->request.set_value(offset, state ? ::gpiod::line::value::INACTIVE : ::gpiod::line::value::ACTIVE); return true; } catch (const std::exception& e) { std::cerr << "[GpiodRelayController] Error setting relay " << relayId << ": " << e.what() << std::endl; diff --git a/sw/minihil/src/main.cpp b/sw/minihil/src/main.cpp index b55fcdb..d38620a 100644 --- a/sw/minihil/src/main.cpp +++ b/sw/minihil/src/main.cpp @@ -6,6 +6,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include "core/idevice_controller.hpp" #include "protocol/jsonrpc_router.hpp" @@ -19,6 +25,8 @@ using ConcreteController = minihil::SilRelayController; using ConcreteController = minihil::GpiodRelayController; #endif +namespace fs = std::filesystem; + constexpr int PORT = 9000; // Synchronization for graceful daemon shutdown @@ -35,12 +43,258 @@ void signalHandler(int signum) { g_shutdownCV.notify_one(); } -int main() { +bool add_ext(X509* cert, int nid, const char* value) { + X509_EXTENSION* ex = nullptr; + X509V3_CTX ctx; + X509V3_set_ctx_nodb(&ctx); + X509V3_set_ctx(&ctx, cert, cert, nullptr, nullptr, 0); + ex = X509V3_EXT_conf_nid(nullptr, &ctx, nid, const_cast(value)); + if (!ex) return false; + X509_add_ext(cert, ex, -1); + X509_EXTENSION_free(ex); + return true; +} + +bool add_ext_signed(X509* cert, X509* issuer, int nid, const char* value) { + X509_EXTENSION* ex = nullptr; + X509V3_CTX ctx; + X509V3_set_ctx_nodb(&ctx); + X509V3_set_ctx(&ctx, issuer, cert, nullptr, nullptr, 0); + ex = X509V3_EXT_conf_nid(nullptr, &ctx, nid, const_cast(value)); + if (!ex) return false; + X509_add_ext(cert, ex, -1); + X509_EXTENSION_free(ex); + return true; +} + +EVP_PKEY* generateKeyPair() { + EVP_PKEY* pkey = nullptr; + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr); + if (!ctx) return nullptr; + if (EVP_PKEY_keygen_init(ctx) <= 0 || + EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) <= 0 || + EVP_PKEY_keygen(ctx, &pkey) <= 0) { + EVP_PKEY_CTX_free(ctx); + if (pkey) EVP_PKEY_free(pkey); + return nullptr; + } + EVP_PKEY_CTX_free(ctx); + return pkey; +} + +bool writePrivateKey(const std::string& path, EVP_PKEY* pkey) { + FILE* f = fopen(path.c_str(), "wb"); + if (!f) return false; + bool ok = (PEM_write_PrivateKey(f, pkey, nullptr, nullptr, 0, nullptr, nullptr) > 0); + fclose(f); + return ok; +} + +bool writeCert(const std::string& path, X509* x509) { + FILE* f = fopen(path.c_str(), "wb"); + if (!f) return false; + bool ok = (PEM_write_X509(f, x509) > 0); + fclose(f); + return ok; +} + +bool generatePki(const std::string& caCertPath, const std::string& caKeyPath, + const std::string& serverCertPath, const std::string& serverKeyPath, + const std::string& clientCertPath, const std::string& clientKeyPath) { + std::cout << "[Main] Generating Root CA and certificates (Server & Client)..." << std::endl; + + // 1. Generate Root CA + EVP_PKEY* caKey = generateKeyPair(); + if (!caKey) return false; + X509* caCert = X509_new(); + if (!caCert) { + EVP_PKEY_free(caKey); + return false; + } + X509_set_version(caCert, 2); // V3 + ASN1_INTEGER_set(X509_get_serialNumber(caCert), 1); + X509_gmtime_adj(X509_get_notBefore(caCert), 0); + X509_gmtime_adj(X509_get_notAfter(caCert), 315360000L); // 10 years + X509_set_pubkey(caCert, caKey); + + X509_NAME* caName = X509_get_subject_name(caCert); + X509_NAME_add_entry_by_txt(caName, "C", MBSTRING_ASC, (const unsigned char*)"US", -1, -1, 0); + X509_NAME_add_entry_by_txt(caName, "O", MBSTRING_ASC, (const unsigned char*)"Electux", -1, -1, 0); + X509_NAME_add_entry_by_txt(caName, "CN", MBSTRING_ASC, (const unsigned char*)"MiniHIL Root CA", -1, -1, 0); + X509_set_issuer_name(caCert, caName); + + add_ext(caCert, NID_basic_constraints, "critical,CA:TRUE"); + add_ext(caCert, NID_key_usage, "critical,keyCertSign,cRLSign"); + + if (!X509_sign(caCert, caKey, EVP_sha256())) { + X509_free(caCert); + EVP_PKEY_free(caKey); + return false; + } + + // 2. Generate Server Certificate + EVP_PKEY* serverKey = generateKeyPair(); + if (!serverKey) { + X509_free(caCert); + EVP_PKEY_free(caKey); + return false; + } + X509* serverCert = X509_new(); + if (!serverCert) { + EVP_PKEY_free(serverKey); + X509_free(caCert); + EVP_PKEY_free(caKey); + return false; + } + X509_set_version(serverCert, 2); + ASN1_INTEGER_set(X509_get_serialNumber(serverCert), 2); + X509_gmtime_adj(X509_get_notBefore(serverCert), 0); + X509_gmtime_adj(X509_get_notAfter(serverCert), 31536000L); // 1 year + X509_set_pubkey(serverCert, serverKey); + + X509_NAME* serverName = X509_get_subject_name(serverCert); + X509_NAME_add_entry_by_txt(serverName, "C", MBSTRING_ASC, (const unsigned char*)"US", -1, -1, 0); + X509_NAME_add_entry_by_txt(serverName, "O", MBSTRING_ASC, (const unsigned char*)"Electux", -1, -1, 0); + X509_NAME_add_entry_by_txt(serverName, "CN", MBSTRING_ASC, (const unsigned char*)"localhost", -1, -1, 0); + X509_set_issuer_name(serverCert, X509_get_subject_name(caCert)); + + add_ext_signed(serverCert, caCert, NID_basic_constraints, "CA:FALSE"); + add_ext_signed(serverCert, caCert, NID_key_usage, "critical,digitalSignature,keyEncipherment"); + add_ext_signed(serverCert, caCert, NID_ext_key_usage, "serverAuth"); + + if (!X509_sign(serverCert, caKey, EVP_sha256())) { + X509_free(serverCert); + EVP_PKEY_free(serverKey); + X509_free(caCert); + EVP_PKEY_free(caKey); + return false; + } + + // 3. Generate Client Certificate + EVP_PKEY* clientKey = generateKeyPair(); + if (!clientKey) { + X509_free(serverCert); + EVP_PKEY_free(serverKey); + X509_free(caCert); + EVP_PKEY_free(caKey); + return false; + } + X509* clientCert = X509_new(); + if (!clientCert) { + EVP_PKEY_free(clientKey); + X509_free(serverCert); + EVP_PKEY_free(serverKey); + X509_free(caCert); + EVP_PKEY_free(caKey); + return false; + } + X509_set_version(clientCert, 2); + ASN1_INTEGER_set(X509_get_serialNumber(clientCert), 3); + X509_gmtime_adj(X509_get_notBefore(clientCert), 0); + X509_gmtime_adj(X509_get_notAfter(clientCert), 31536000L); // 1 year + X509_set_pubkey(clientCert, clientKey); + + X509_NAME* clientName = X509_get_subject_name(clientCert); + X509_NAME_add_entry_by_txt(clientName, "C", MBSTRING_ASC, (const unsigned char*)"US", -1, -1, 0); + X509_NAME_add_entry_by_txt(clientName, "O", MBSTRING_ASC, (const unsigned char*)"Electux", -1, -1, 0); + X509_NAME_add_entry_by_txt(clientName, "CN", MBSTRING_ASC, (const unsigned char*)"minihildesk", -1, -1, 0); + X509_set_issuer_name(clientCert, X509_get_subject_name(caCert)); + + add_ext_signed(clientCert, caCert, NID_basic_constraints, "CA:FALSE"); + add_ext_signed(clientCert, caCert, NID_key_usage, "critical,digitalSignature"); + add_ext_signed(clientCert, caCert, NID_ext_key_usage, "clientAuth"); + + if (!X509_sign(clientCert, caKey, EVP_sha256())) { + X509_free(clientCert); + EVP_PKEY_free(clientKey); + X509_free(serverCert); + EVP_PKEY_free(serverKey); + X509_free(caCert); + EVP_PKEY_free(caKey); + return false; + } + + // Write keys & certs to disk + bool success = true; + success &= writePrivateKey(caKeyPath, caKey); + success &= writeCert(caCertPath, caCert); + success &= writePrivateKey(serverKeyPath, serverKey); + success &= writeCert(serverCertPath, serverCert); + success &= writePrivateKey(clientKeyPath, clientKey); + success &= writeCert(clientCertPath, clientCert); + + X509_free(clientCert); + EVP_PKEY_free(clientKey); + X509_free(serverCert); + EVP_PKEY_free(serverKey); + X509_free(caCert); + EVP_PKEY_free(caKey); + + if (success) { + std::cout << "[Main] Successfully generated full PKI certs:\n" + << " - CA: " << caCertPath << ", " << caKeyPath << "\n" + << " - Server: " << serverCertPath << ", " << serverKeyPath << "\n" + << " - Client: " << clientCertPath << ", " << clientKeyPath << std::endl; + } else { + std::cerr << "[Main] Error writing certificates/keys to disk." << std::endl; + } + return success; +} + +int main(int argc, char* argv[]) { + bool useSsl = false; + bool useMtls = false; + std::string caCertPath = "ca.crt"; + std::string caKeyPath = "ca.key"; + std::string certPath = "server.crt"; + std::string keyPath = "server.key"; + std::string clientCertPath = "client.crt"; + std::string clientKeyPath = "client.key"; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--ssl") { + useSsl = true; + } else if (arg == "--mtls") { + useSsl = true; + useMtls = true; + } else if (arg == "--ca" && i + 1 < argc) { + caCertPath = argv[++i]; + } else if (arg == "--cert" && i + 1 < argc) { + certPath = argv[++i]; + } else if (arg == "--key" && i + 1 < argc) { + keyPath = argv[++i]; + } else if (arg == "--help" || arg == "-h") { + std::cout << "Usage: minihild [options]\n" + << "Options:\n" + << " --ssl Enable SSL/TLS secure connection\n" + << " --mtls Enable SSL/TLS and enforce Mutual TLS (mTLS) client verification\n" + << " --ca Path to CA certificate file (default: ca.crt)\n" + << " --cert Path to Server SSL certificate file (default: server.crt)\n" + << " --key Path to Server SSL private key file (default: server.key)\n" + << " -h, --help Show this help message\n"; + return 0; + } + } + std::cout << "Starting minihild (MiniHIL POSIX C++ Daemon)..." << std::endl; // Register POSIX signal handlers std::signal(SIGINT, signalHandler); std::signal(SIGTERM, signalHandler); + std::signal(SIGPIPE, SIG_IGN); + + // Generate certificates if SSL is requested and files do not exist + if (useSsl) { + if (!fs::exists(caCertPath) || !fs::exists(caKeyPath) || + !fs::exists(certPath) || !fs::exists(keyPath) || + !fs::exists(clientCertPath) || !fs::exists(clientKeyPath)) { + if (!generatePki(caCertPath, caKeyPath, certPath, keyPath, clientCertPath, clientKeyPath)) { + std::cerr << "[Main] Fatal: Failed to generate PKI certificates." << std::endl; + return 1; + } + } + } // 1. Instantiate HAL / SIL Device Controller (DIP) std::shared_ptr controller = std::make_shared(); @@ -52,9 +306,7 @@ int main() { // 2. Instantiate Protocol Router auto router = std::make_shared(); - // 3. Register JSON-RPC Methods (Open/Closed Principle) - - // set_relay: { "relay_id": int [1-8], "state": bool } + // 3. Register JSON-RPC Methods router->registerMethod("set_relay", [controller](const nlohmann::json& params, const nlohmann::json& id) -> nlohmann::json { if (!params.is_object() || !params.contains("relay_id") || !params.contains("state")) { return {{"code", -32602}, {"error", "Invalid params: 'relay_id' (integer) and 'state' (boolean) are required."}}; @@ -75,7 +327,6 @@ int main() { return {{"success", true}, {"relay_id", relayId}, {"state", state}}; }); - // get_relays: returns states of all 8 relays router->registerMethod("get_relays", [controller](const nlohmann::json& params, const nlohmann::json& id) -> nlohmann::json { auto states = controller->getAllStates(); nlohmann::json result = nlohmann::json::object(); @@ -85,8 +336,8 @@ int main() { return result; }); - // 4. Instantiate Server and Inject Router Dependency (Dependency Inversion) - auto server = std::make_shared(PORT, router); + // 4. Instantiate Server and Inject Router Dependency + auto server = std::make_shared(PORT, router, useSsl, useMtls, caCertPath, certPath, keyPath); // 5. Start Server if (!server->start()) { diff --git a/sw/minihil/src/network/tcp_server.cpp b/sw/minihil/src/network/tcp_server.cpp index 24e7d94..683122b 100644 --- a/sw/minihil/src/network/tcp_server.cpp +++ b/sw/minihil/src/network/tcp_server.cpp @@ -6,14 +6,23 @@ #include #include #include +#include +#include namespace minihil { -TcpServer::TcpServer(int port, std::shared_ptr rpcHandler) +TcpServer::TcpServer(int port, std::shared_ptr rpcHandler, + bool useSsl, bool useMtls, const std::string& caPath, const std::string& certPath, const std::string& keyPath) : m_port(port), m_rpcHandler(rpcHandler), m_running(false), - m_serverSocket(-1) {} + m_serverSocket(-1), + m_useSsl(useSsl), + m_useMtls(useMtls), + m_caPath(caPath), + m_certPath(certPath), + m_keyPath(keyPath), + m_sslCtx(nullptr) {} TcpServer::~TcpServer() { stop(); @@ -22,10 +31,55 @@ TcpServer::~TcpServer() { bool TcpServer::start() { if (m_running) return true; + if (m_useSsl) { + // Initialize OpenSSL context + const SSL_METHOD* method = TLS_server_method(); + SSL_CTX* ctx = SSL_CTX_new(method); + if (!ctx) { + std::cerr << "[TcpServer] Failed to create SSL context." << std::endl; + ERR_print_errors_fp(stderr); + return false; + } + + // Load cert and key + if (SSL_CTX_use_certificate_file(ctx, m_certPath.c_str(), SSL_FILETYPE_PEM) <= 0) { + std::cerr << "[TcpServer] Failed to use certificate file: " << m_certPath << std::endl; + ERR_print_errors_fp(stderr); + SSL_CTX_free(ctx); + return false; + } + + if (SSL_CTX_use_PrivateKey_file(ctx, m_keyPath.c_str(), SSL_FILETYPE_PEM) <= 0) { + std::cerr << "[TcpServer] Failed to use private key file: " << m_keyPath << std::endl; + ERR_print_errors_fp(stderr); + SSL_CTX_free(ctx); + return false; + } + + // Load CA if mTLS is enabled + if (m_useMtls && !m_caPath.empty()) { + if (SSL_CTX_load_verify_locations(ctx, m_caPath.c_str(), nullptr) <= 0) { + std::cerr << "[TcpServer] Failed to load CA verify locations from: " << m_caPath << std::endl; + ERR_print_errors_fp(stderr); + SSL_CTX_free(ctx); + return false; + } + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); + std::cout << "[TcpServer] mTLS enabled: requiring client certificate verified by CA: " << m_caPath << std::endl; + } + + m_sslCtx = ctx; + std::cout << "[TcpServer] SSL Context initialized successfully with cert: " << m_certPath << " (mTLS: " << (m_useMtls ? "ENABLED" : "DISABLED") << ")" << std::endl; + } + // Create IPv4 TCP socket m_serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (m_serverSocket < 0) { std::cerr << "[TcpServer] Failed to create socket." << std::endl; + if (m_sslCtx) { + SSL_CTX_free(static_cast(m_sslCtx)); + m_sslCtx = nullptr; + } return false; } @@ -45,6 +99,10 @@ bool TcpServer::start() { std::cerr << "[TcpServer] Failed to bind to port " << m_port << "." << std::endl; close(m_serverSocket); m_serverSocket = -1; + if (m_sslCtx) { + SSL_CTX_free(static_cast(m_sslCtx)); + m_sslCtx = nullptr; + } return false; } @@ -52,13 +110,17 @@ bool TcpServer::start() { std::cerr << "[TcpServer] Failed to listen." << std::endl; close(m_serverSocket); m_serverSocket = -1; + if (m_sslCtx) { + SSL_CTX_free(static_cast(m_sslCtx)); + m_sslCtx = nullptr; + } return false; } m_running = true; m_listenerThread = std::thread(&TcpServer::listenLoop, this); - std::cout << "[TcpServer] Server listening on port " << m_port << "..." << std::endl; + std::cout << "[TcpServer] Server listening on port " << m_port << " (SSL: " << (m_useSsl ? "ENABLED" : "DISABLED") << ")..." << std::endl; return true; } @@ -78,15 +140,31 @@ void TcpServer::stop() { m_listenerThread.join(); } - // Close all active client connections to unblock recv() in handleClient - std::lock_guard lock(m_clientsMutex); - for (int fd : m_clientSockets) { - if (fd >= 0) { - shutdown(fd, SHUT_RDWR); - close(fd); + // Shut down active client connections to trigger read failures in client threads + { + std::lock_guard lock(m_clientsMutex); + for (const auto& session : m_clientSessions) { + if (session.socketFd >= 0) { + shutdown(session.socketFd, SHUT_RDWR); + } } } - m_clientSockets.clear(); + + // Wait for all client threads to clean up and exit + while (true) { + { + std::lock_guard lock(m_clientsMutex); + if (m_clientSessions.empty()) { + break; + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + if (m_sslCtx) { + SSL_CTX_free(static_cast(m_sslCtx)); + m_sslCtx = nullptr; + } std::cout << "[TcpServer] Server stopped." << std::endl; } @@ -103,12 +181,18 @@ void TcpServer::listenLoop() { continue; } + SSL* ssl = nullptr; + if (m_useSsl && m_sslCtx) { + ssl = SSL_new(static_cast(m_sslCtx)); + SSL_set_fd(ssl, clientSocket); + } + { std::lock_guard lock(m_clientsMutex); - m_clientSockets.push_back(clientSocket); + m_clientSessions.push_back({clientSocket, ssl}); } - // Handle each client in a separate detached thread (automatic memory reclamation on exit) + // Handle each client in a separate detached thread std::thread t(&TcpServer::handleClient, this, clientSocket); t.detach(); } @@ -119,9 +203,48 @@ void TcpServer::handleClient(int clientSocket) { char buffer[2048]; std::string requestBuffer; + SSL* ssl = nullptr; + if (m_useSsl) { + { + std::lock_guard lock(m_clientsMutex); + for (const auto& session : m_clientSessions) { + if (session.socketFd == clientSocket) { + ssl = static_cast(session.ssl); + break; + } + } + } + + if (ssl) { + if (SSL_accept(ssl) <= 0) { + std::cerr << "[TcpServer] SSL handshake failed." << std::endl; + ERR_print_errors_fp(stderr); + + if (ssl) SSL_free(ssl); + close(clientSocket); + + std::lock_guard lock(m_clientsMutex); + auto it = std::find_if(m_clientSessions.begin(), m_clientSessions.end(), + [clientSocket](const ClientSession& s) { return s.socketFd == clientSocket; }); + if (it != m_clientSessions.end()) { + m_clientSessions.erase(it); + } + return; + } + std::cout << "[TcpServer] SSL handshake completed successfully." << std::endl; + } + } + while (m_running) { memset(buffer, 0, sizeof(buffer)); - ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer) - 1, 0); + ssize_t bytesRead = 0; + + if (m_useSsl && ssl) { + bytesRead = SSL_read(ssl, buffer, sizeof(buffer) - 1); + } else { + bytesRead = recv(clientSocket, buffer, sizeof(buffer) - 1, 0); + } + if (bytesRead <= 0) { break; // Client disconnected or socket closed during shutdown } @@ -137,21 +260,34 @@ void TcpServer::handleClient(int clientSocket) { if (!rawRequest.empty() && rawRequest != "\r") { std::string rawResponse = m_rpcHandler->processRequest(rawRequest); if (!rawResponse.empty() && m_running) { - send(clientSocket, rawResponse.c_str(), rawResponse.size(), 0); + if (m_useSsl && ssl) { + SSL_write(ssl, rawResponse.c_str(), rawResponse.size()); + } else { + send(clientSocket, rawResponse.c_str(), rawResponse.size(), 0); + } } } } } + if (m_useSsl && ssl) { + SSL_shutdown(ssl); + SSL_free(ssl); + } close(clientSocket); // Remove from active client tracking - std::lock_guard lock(m_clientsMutex); - auto it = std::find(m_clientSockets.begin(), m_clientSockets.end(), clientSocket); - if (it != m_clientSockets.end()) { - m_clientSockets.erase(it); + { + std::lock_guard lock(m_clientsMutex); + auto it = std::find_if(m_clientSessions.begin(), m_clientSessions.end(), + [clientSocket](const ClientSession& s) { return s.socketFd == clientSocket; }); + if (it != m_clientSessions.end()) { + m_clientSessions.erase(it); + } } std::cout << "[TcpServer] Client disconnected." << std::endl; } +void TcpServer::cleanupClosedClients() {} + } // namespace minihil diff --git a/sw/minihildesk/CMakeLists.txt b/sw/minihildesk/CMakeLists.txt new file mode 100644 index 0000000..e32cda0 --- /dev/null +++ b/sw/minihildesk/CMakeLists.txt @@ -0,0 +1,59 @@ +cmake_minimum_required(VERSION 3.12) +project(minihildesk VERSION 1.1.6 LANGUAGES CXX) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find gtkmm-4.0 +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTKMM REQUIRED gtkmm-4.0) + +# Find nlohmann_json +find_package(nlohmann_json REQUIRED) + +# Include directories +include_directories( + src + ${GTKMM_INCLUDE_DIRS} +) + +# Find glib-compile-resources +find_program(GLIB_COMPILE_RESOURCES glib-compile-resources REQUIRED) + +# Custom command to compile XML resources to resources.cc +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/resources.cc + COMMAND ${GLIB_COMPILE_RESOURCES} + --target=${CMAKE_CURRENT_BINARY_DIR}/resources.cc + --generate-source + --sourcedir=${CMAKE_CURRENT_SOURCE_DIR}/resources + ${CMAKE_CURRENT_SOURCE_DIR}/resources/resources.gresource.xml + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/resources/resources.gresource.xml + ${CMAKE_CURRENT_SOURCE_DIR}/resources/style.css +) + +# Source files +file(GLOB_RECURSE SOURCES + "src/*.cc" +) +list(APPEND SOURCES ${CMAKE_CURRENT_BINARY_DIR}/resources.cc) + +add_executable(minihildesk ${SOURCES}) + +target_compile_options(minihildesk PRIVATE + -Wall + -Wextra + -pedantic +) + +# Find OpenSSL +find_package(OpenSSL REQUIRED) + +target_link_libraries(minihildesk + PRIVATE + ${GTKMM_LIBRARIES} + nlohmann_json::nlohmann_json + OpenSSL::SSL + OpenSSL::Crypto +) diff --git a/sw/minihildesk/build/deb_dist/minihildesk_1.1.6_amd64.deb b/sw/minihildesk/build/deb_dist/minihildesk_1.1.6_amd64.deb new file mode 100644 index 0000000..c3ec557 Binary files /dev/null and b/sw/minihildesk/build/deb_dist/minihildesk_1.1.6_amd64.deb differ diff --git a/sw/minihildesk/build/minihildesk b/sw/minihildesk/build/minihildesk new file mode 100755 index 0000000..5353971 Binary files /dev/null and b/sw/minihildesk/build/minihildesk differ diff --git a/sw/minihildesk/build/resources.cc b/sw/minihildesk/build/resources.cc new file mode 100644 index 0000000..e5a418b --- /dev/null +++ b/sw/minihildesk/build/resources.cc @@ -0,0 +1,232 @@ +#include + +#if defined (__ELF__) && ( __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 6)) +# define SECTION __attribute__ ((section (".gresource.resources"), aligned (sizeof(void *) > 8 ? sizeof(void *) : 8))) +#else +# define SECTION +#endif + +static const SECTION union { const guint8 data[781]; const double alignment; void * const ptr;} resources_resource_data = { + "\107\126\141\162\151\141\156\164\000\000\000\000\000\000\000\000" + "\030\000\000\000\254\000\000\000\000\000\000\050\005\000\000\000" + "\000\000\000\000\002\000\000\000\002\000\000\000\003\000\000\000" + "\003\000\000\000\324\265\002\000\377\377\377\377\254\000\000\000" + "\001\000\114\000\260\000\000\000\264\000\000\000\014\240\142\334" + "\004\000\000\000\264\000\000\000\011\000\166\000\300\000\000\000" + "\346\002\000\000\144\252\051\316\003\000\000\000\346\002\000\000" + "\010\000\114\000\360\002\000\000\364\002\000\000\173\242\170\174" + "\000\000\000\000\364\002\000\000\003\000\114\000\370\002\000\000" + "\374\002\000\000\104\246\353\017\002\000\000\000\374\002\000\000" + "\014\000\114\000\010\003\000\000\014\003\000\000\057\000\000\000" + "\003\000\000\000\163\164\171\154\145\056\143\163\163\000\000\000" + "\026\002\000\000\000\000\000\000\164\145\170\164\166\151\145\167" + "\040\164\145\170\164\040\173\012\040\040\040\040\142\141\143\153" + "\147\162\157\165\156\144\055\143\157\154\157\162\072\040\043\061" + "\062\061\062\061\064\073\012\040\040\040\040\143\157\154\157\162" + "\072\040\043\060\060\146\146\066\066\073\012\040\040\040\040\146" + "\157\156\164\055\146\141\155\151\154\171\072\040\155\157\156\157" + "\163\160\141\143\145\073\012\040\040\040\040\146\157\156\164\055" + "\163\151\172\145\072\040\061\063\160\170\073\012\175\012\012\056" + "\162\145\154\141\171\055\143\141\162\144\040\173\012\040\040\040" + "\040\142\157\162\144\145\162\072\040\061\160\170\040\163\157\154" + "\151\144\040\043\144\060\144\060\144\065\073\012\040\040\040\040" + "\142\157\162\144\145\162\055\162\141\144\151\165\163\072\040\070" + "\160\170\073\012\040\040\040\040\142\141\143\153\147\162\157\165" + "\156\144\055\143\157\154\157\162\072\040\043\146\067\146\067\146" + "\071\073\012\175\012\012\056\162\145\154\141\171\055\143\141\162" + "\144\040\154\141\142\145\154\040\173\012\040\040\040\040\143\157" + "\154\157\162\072\040\043\061\062\061\062\061\064\073\012\175\012" + "\012\056\162\145\154\141\171\055\143\141\162\144\072\144\151\163" + "\141\142\154\145\144\040\154\141\142\145\154\040\173\012\040\040" + "\040\040\143\157\154\157\162\072\040\043\067\146\070\143\070\144" + "\073\012\175\012\012\056\154\145\144\055\151\156\144\151\143\141" + "\164\157\162\040\173\012\040\040\040\040\146\157\156\164\055\163" + "\151\172\145\072\040\062\064\160\170\073\012\175\012\012\056\162" + "\145\154\141\171\055\143\141\162\144\040\154\141\142\145\154\056" + "\154\145\144\055\157\156\040\173\012\040\040\040\040\143\157\154" + "\157\162\072\040\043\062\145\143\143\067\061\073\012\040\040\040" + "\040\164\145\170\164\055\163\150\141\144\157\167\072\040\060\040" + "\060\040\061\060\160\170\040\043\062\145\143\143\067\061\054\040" + "\060\040\060\040\062\060\160\170\040\043\062\145\143\143\067\061" + "\073\012\175\012\012\056\162\145\154\141\171\055\143\141\162\144" + "\040\154\141\142\145\154\056\154\145\144\055\157\146\146\040\173" + "\012\040\040\040\040\143\157\154\157\162\072\040\043\142\144\143" + "\063\143\067\073\012\040\040\040\040\164\145\170\164\055\163\150" + "\141\144\157\167\072\040\156\157\156\145\073\012\175\012\000\000" + "\050\165\165\141\171\051\145\154\145\143\164\165\170\057\000\000" + "\004\000\000\000\151\157\057\000\002\000\000\000\155\151\156\151" + "\150\151\154\144\145\163\153\057\001\000\000\000" }; + +static GStaticResource static_resource = { resources_resource_data.data, sizeof (resources_resource_data.data) - 1 /* nul terminator */, NULL, NULL, NULL }; + +G_MODULE_EXPORT +GResource *resources_get_resource (void); +GResource *resources_get_resource (void) +{ + return g_static_resource_get_resource (&static_resource); +} +/* GLIB - Library of useful routines for C programming + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +/* + * Modified by the GLib Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GLib Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GLib at ftp://ftp.gtk.org/pub/gtk/. + */ + +#ifndef __G_CONSTRUCTOR_H__ +#define __G_CONSTRUCTOR_H__ + +/* + If G_HAS_CONSTRUCTORS is true then the compiler support *both* constructors and + destructors, in a usable way, including e.g. on library unload. If not you're on + your own. + + Some compilers need #pragma to handle this, which does not work with macros, + so the way you need to use this is (for constructors): + + #ifdef G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA + #pragma G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(my_constructor) + #endif + G_DEFINE_CONSTRUCTOR(my_constructor) + static void my_constructor(void) { + ... + } + +*/ + +#ifndef __GTK_DOC_IGNORE__ + +#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 7) + +#define G_HAS_CONSTRUCTORS 1 + +#define G_DEFINE_CONSTRUCTOR(_func) static void __attribute__((constructor)) _func (void); +#define G_DEFINE_DESTRUCTOR(_func) static void __attribute__((destructor)) _func (void); + +#elif defined (_MSC_VER) + +/* + * Only try to include gslist.h if not already included via glib.h, + * so that items using gconstructor.h outside of GLib (such as + * GResources) continue to build properly. + */ +#ifndef __G_LIB_H__ +#include "gslist.h" +#endif + +#include + +#define G_HAS_CONSTRUCTORS 1 + +/* We do some weird things to avoid the constructors being optimized + * away on VS2015 if WholeProgramOptimization is enabled. First we + * make a reference to the array from the wrapper to make sure its + * references. Then we use a pragma to make sure the wrapper function + * symbol is always included at the link stage. Also, the symbols + * need to be extern (but not dllexport), even though they are not + * really used from another object file. + */ + +/* We need to account for differences between the mangling of symbols + * for x86 and x64/ARM/ARM64 programs, as symbols on x86 are prefixed + * with an underscore but symbols on x64/ARM/ARM64 are not. + */ +#ifdef _M_IX86 +#define G_MSVC_SYMBOL_PREFIX "_" +#else +#define G_MSVC_SYMBOL_PREFIX "" +#endif + +#define G_DEFINE_CONSTRUCTOR(_func) G_MSVC_CTOR (_func, G_MSVC_SYMBOL_PREFIX) +#define G_DEFINE_DESTRUCTOR(_func) G_MSVC_DTOR (_func, G_MSVC_SYMBOL_PREFIX) + +#define G_MSVC_CTOR(_func,_sym_prefix) \ + static void _func(void); \ + extern int (* _array ## _func)(void); \ + int _func ## _wrapper(void); \ + int _func ## _wrapper(void) { _func(); g_slist_find (NULL, _array ## _func); return 0; } \ + __pragma(comment(linker,"/include:" _sym_prefix # _func "_wrapper")) \ + __pragma(section(".CRT$XCU",read)) \ + __declspec(allocate(".CRT$XCU")) int (* _array ## _func)(void) = _func ## _wrapper; + +#define G_MSVC_DTOR(_func,_sym_prefix) \ + static void _func(void); \ + extern int (* _array ## _func)(void); \ + int _func ## _constructor(void); \ + int _func ## _constructor(void) { atexit (_func); g_slist_find (NULL, _array ## _func); return 0; } \ + __pragma(comment(linker,"/include:" _sym_prefix # _func "_constructor")) \ + __pragma(section(".CRT$XCU",read)) \ + __declspec(allocate(".CRT$XCU")) int (* _array ## _func)(void) = _func ## _constructor; + +#elif defined(__SUNPRO_C) + +/* This is not tested, but i believe it should work, based on: + * http://opensource.apple.com/source/OpenSSL098/OpenSSL098-35/src/fips/fips_premain.c + */ + +#define G_HAS_CONSTRUCTORS 1 + +#define G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA 1 +#define G_DEFINE_DESTRUCTOR_NEEDS_PRAGMA 1 + +#define G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(_func) \ + init(_func) +#define G_DEFINE_CONSTRUCTOR(_func) \ + static void _func(void); + +#define G_DEFINE_DESTRUCTOR_PRAGMA_ARGS(_func) \ + fini(_func) +#define G_DEFINE_DESTRUCTOR(_func) \ + static void _func(void); + +#else + +/* constructors not supported for this compiler */ + +#endif + +#endif /* __GTK_DOC_IGNORE__ */ +#endif /* __G_CONSTRUCTOR_H__ */ + +#ifdef G_HAS_CONSTRUCTORS + +#ifdef G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA +#pragma G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(resourcesresource_constructor) +#endif +G_DEFINE_CONSTRUCTOR(resourcesresource_constructor) +#ifdef G_DEFINE_DESTRUCTOR_NEEDS_PRAGMA +#pragma G_DEFINE_DESTRUCTOR_PRAGMA_ARGS(resourcesresource_destructor) +#endif +G_DEFINE_DESTRUCTOR(resourcesresource_destructor) + +#else +#warning "Constructor not supported on this compiler, linking in resources will not work" +#endif + +static void resourcesresource_constructor (void) +{ + g_static_resource_init (&static_resource); +} + +static void resourcesresource_destructor (void) +{ + g_static_resource_fini (&static_resource); +} diff --git a/sw/minihildesk/resources/resources.gresource.xml b/sw/minihildesk/resources/resources.gresource.xml new file mode 100644 index 0000000..9e39c00 --- /dev/null +++ b/sw/minihildesk/resources/resources.gresource.xml @@ -0,0 +1,6 @@ + + + + style.css + + diff --git a/sw/minihildesk/resources/style.css b/sw/minihildesk/resources/style.css new file mode 100644 index 0000000..255e990 --- /dev/null +++ b/sw/minihildesk/resources/style.css @@ -0,0 +1,34 @@ +textview text { + background-color: #121214; + color: #00ff66; + font-family: monospace; + font-size: 13px; +} + +.relay-card { + border: 1px solid #d0d0d5; + border-radius: 8px; + background-color: #f7f7f9; +} + +.relay-card label { + color: #121214; +} + +.relay-card:disabled label { + color: #7f8c8d; +} + +.led-indicator { + font-size: 24px; +} + +.relay-card label.led-on { + color: #2ecc71; + text-shadow: 0 0 10px #2ecc71, 0 0 20px #2ecc71; +} + +.relay-card label.led-off { + color: #bdc3c7; + text-shadow: none; +} diff --git a/sw/minihildesk/scripts/deb/create_deb.py b/sw/minihildesk/scripts/deb/create_deb.py new file mode 100755 index 0000000..8ffd762 --- /dev/null +++ b/sw/minihildesk/scripts/deb/create_deb.py @@ -0,0 +1,240 @@ +# -*- coding: UTF-8 -*- + +''' +Module + create_deb.py +Copyright + Copyright (C) 2026 Vladimir Roncevic + minihil is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + minihil is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License along + with this program. If not, see . +Info + A Python script for creating a Debian installation package (.deb) for minihildesk. +''' + +from __future__ import annotations + +import os +import re +import shutil +import subprocess +import sys +from typing import Final + +__author__: str = 'Vladimir Roncevic' +__copyright__: str = '(C) 2026, https://vroncevic.github.io/minihil' +__credits__: list[str] = ['Vladimir Roncevic', 'Python Software Foundation'] +__license__: str = 'https://github.com/vroncevic/minihil/blob/dev/LICENSE' +__version__: str = '1.0.0' +__maintainer__: str = 'Vladimir Roncevic' +__email__: str = 'elektron.ronca@gmail.com' +__status__: str = 'Updated' + +# Resolve paths dynamically relative to script location +SCRIPT_DIR: Final[str] = os.path.dirname(os.path.abspath(__file__)) +SW_DIR: Final[str] = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..')) +REPO_ROOT: Final[str] = os.path.abspath(os.path.join(SW_DIR, '..', '..')) + +BUILD_DIR: Final[str] = os.path.join(SW_DIR, 'build') +CMAKELISTS_PATH: Final[str] = os.path.join(SW_DIR, 'CMakeLists.txt') +BINARY_PATH: Final[str] = os.path.join(BUILD_DIR, 'minihildesk') +LOGO_PATH: Final[str] = os.path.join(REPO_ROOT, 'docs', 'minihil_logo.png') +DEB_DIST_DIR: Final[str] = os.path.join(BUILD_DIR, 'deb_dist') + + +def get_version(cmake_path: str) -> str: + """ + Extract the version number from the CMakeLists.txt. + + :param cmake_path: Absolute path to the CMakeLists.txt. + :return: Version string, or '1.1.6' if not found or readable. + """ + try: + with open(cmake_path, 'r', encoding='utf-8') as f: + for line in f: + # Match project(minihildesk VERSION X.Y.Z CXX) + match = re.search(r'project\(\s*minihildesk\s+VERSION\s+(\S+)', line, re.IGNORECASE) + if match: + return match.group(1) + except OSError as e: + print(f"Warning: Could not read version from CMakeLists.txt: {e}") + return '1.1.6' + + +def get_architecture() -> str: + """ + Query the package architecture of the host system. + + :return: Architecture string (e.g., 'amd64', 'arm64'). + """ + try: + result = subprocess.run( + ['dpkg', '--print-architecture'], + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except (subprocess.SubprocessError, FileNotFoundError): + print("Warning: dpkg not found or failed to run. Defaulting to 'amd64'.") + return 'amd64' + + +def create_control_file( + dest_path: str, + version: str, + arch: str +) -> None: + """ + Generate the DEBIAN/control file. + + :param dest_path: Absolute path to write the control file to. + :param version: Package version. + :param arch: Package architecture. + """ + content = f"""Package: minihildesk +Version: {version} +Section: utils +Priority: optional +Architecture: {arch} +Maintainer: Vladimir Roncevic +Depends: libgtkmm-4.0-1 | libgtkmm-4.0-1t64, libc6, libgcc-s1, libstdc++6 +Description: Desktop GUI application for minihil hardware controller + A desktop GUI application built in C++ using gtkmm for orchestrating + and controlling minihil hardware controllers (relays, etc.) over TCP interface. +""" + with open(dest_path, 'w', encoding='utf-8') as f: + f.write(content) + + +def create_desktop_entry(dest_path: str) -> None: + """ + Generate the desktop application launcher entry. + + :param dest_path: Absolute path to write the desktop file to. + """ + content = """[Desktop Entry] +Version=1.0 +Type=Application +Name=minihil Desk +Comment=Desktop GUI for minihil hardware controller +Exec=minihildesk +Icon=minihildesk +Terminal=false +Categories=Utility;Development; +""" + with open(dest_path, 'w', encoding='utf-8') as f: + f.write(content) + + +def main() -> int: + """ + Main entry point for creating the .deb installation package. + + :return: Exit status code (0 for success, 1 for failure). + """ + print("Starting Debian package creation for minihildesk...") + + # 1. Validation checks + if not os.path.exists(BINARY_PATH): + print(f"Error: Executable not found at '{BINARY_PATH}'.") + print("Please build the application first by running 'cmake --build build' in sw/minihildesk") + return 1 + + if not os.path.exists(LOGO_PATH): + print(f"Warning: Logo not found at '{LOGO_PATH}'. Package will be created without an application icon.") + + if not shutil.which('dpkg-deb'): + print("Error: 'dpkg-deb' utility is not installed. Debian packaging is not supported on this host.") + return 1 + + # 2. Package parameters + version = get_version(CMAKELISTS_PATH) + arch = get_architecture() + package_name = f"minihildesk_{version}_{arch}" + tmp_pkg_dir = os.path.join(DEB_DIST_DIR, package_name) + + print(f"Target package: {package_name}.deb") + print(f"Version: {version}") + print(f"Architecture: {arch}") + + # 3. Create clean temporary directory structure + if os.path.exists(tmp_pkg_dir): + shutil.rmtree(tmp_pkg_dir) + + debian_dir = os.path.join(tmp_pkg_dir, "DEBIAN") + bin_dir = os.path.join(tmp_pkg_dir, "usr", "bin") + apps_dir = os.path.join(tmp_pkg_dir, "usr", "share", "applications") + pixmaps_dir = os.path.join(tmp_pkg_dir, "usr", "share", "pixmaps") + + os.makedirs(debian_dir, exist_ok=True) + os.makedirs(bin_dir, exist_ok=True) + os.makedirs(apps_dir, exist_ok=True) + os.makedirs(pixmaps_dir, exist_ok=True) + + # 4. Copy and process binary + pkg_binary_path = os.path.join(bin_dir, "minihildesk") + print(f"Copying binary to {pkg_binary_path}...") + shutil.copy2(BINARY_PATH, pkg_binary_path) + + # Strip binary to remove debugging symbols + if shutil.which('strip'): + print("Stripping debug symbols from packaged binary...") + try: + subprocess.run(['strip', pkg_binary_path], check=True) + except subprocess.SubprocessError as e: + print(f"Warning: Failed to strip binary: {e}") + else: + print("Warning: 'strip' utility not found. Packaging unstripped binary.") + + # Set binary permission to 755 (executable) + os.chmod(pkg_binary_path, 0o755) + + # 5. Copy logo/icon + if os.path.exists(LOGO_PATH): + pkg_icon_path = os.path.join(pixmaps_dir, "minihildesk.png") + print(f"Copying application icon to {pkg_icon_path}...") + shutil.copy2(LOGO_PATH, pkg_icon_path) + os.chmod(pkg_icon_path, 0o644) + + # 6. Generate control file and desktop entry + print("Generating DEBIAN/control file...") + create_control_file(os.path.join(debian_dir, "control"), version, arch) + os.chmod(os.path.join(debian_dir, "control"), 0o644) + + print("Generating desktop launcher...") + create_desktop_entry(os.path.join(apps_dir, "minihildesk.desktop")) + os.chmod(os.path.join(apps_dir, "minihildesk.desktop"), 0o644) + + # 7. Run dpkg-deb --build + os.makedirs(DEB_DIST_DIR, exist_ok=True) + deb_output_path = os.path.join(DEB_DIST_DIR, f"{package_name}.deb") + print(f"Building Debian package in {deb_output_path}...") + + try: + subprocess.run( + ['dpkg-deb', '--root-owner-group', '--build', tmp_pkg_dir, deb_output_path], + check=True + ) + print("Debian package built successfully!") + except subprocess.SubprocessError as e: + print(f"Error: dpkg-deb build failed: {e}") + return 1 + finally: + # Cleanup temporary files + print("Cleaning up temporary packaging directory...") + shutil.rmtree(tmp_pkg_dir) + + print(f"Finished. Package is available at: {deb_output_path}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/sw/minihildesk/src/app_controller.cc b/sw/minihildesk/src/app_controller.cc new file mode 100644 index 0000000..45cecb2 --- /dev/null +++ b/sw/minihildesk/src/app_controller.cc @@ -0,0 +1,149 @@ +#include "app_controller.h" +#include +#include + +namespace minihildesk { + +AppController::AppController(ConfigManager& config) : m_config(config) {} + +AppController::~AppController() { + stop(); +} + +void AppController::start() { + // Controller start (can pre-load settings or trigger autoconnect) +} + +void AppController::stop() { + requestDisconnect(); +} + +void AppController::requestConnect(const std::string& ip, int port, bool useSsl, bool useMtls) { + requestDisconnect(); + + m_signalLog.emit("[System] Connecting to " + ip + ":" + std::to_string(port) + " (SSL: " + (useSsl ? "ON" : "OFF") + ", mTLS: " + (useMtls ? "ON" : "OFF") + ")..."); + if (m_client.connect(ip, port, useSsl, useMtls)) { + m_config.setIp(ip); + m_config.setPort(port); + m_config.setUseSsl(useSsl); + m_config.setUseMtls(useMtls); + m_config.save(); + + m_running = true; + m_readThread = std::thread(&AppController::readLoop, this); + + m_signalConnectionState.emit(true); + m_signalLog.emit("[System] Connected successfully."); + + // Query initial state of all relays + queryAllRelays(); + } else { + m_signalConnectionState.emit(false); + m_signalLog.emit("[System] Connection failed."); + } +} + +void AppController::requestDisconnect() { + bool wasRunning = m_running.exchange(false); + m_client.shutdownSocket(); // Unblock SSL_read/recv first + if (m_readThread.joinable()) { + m_readThread.join(); + } + m_client.disconnect(); // Safe to free memory now that read thread is finished + if (wasRunning) { + m_signalConnectionState.emit(false); + m_signalLog.emit("[System] Disconnected."); + } +} + +bool AppController::isConnected() const { + return m_client.isOpen(); +} + +void AppController::toggleRelay(int relayId, bool state) { + nlohmann::json params; + params["relay_id"] = relayId; + params["state"] = state; + sendJsonRpc("set_relay", params); +} + +void AppController::queryAllRelays() { + sendJsonRpc("get_relays"); +} + +void AppController::sendJsonRpc(const std::string& method, const nlohmann::json& params) { + if (!m_client.isOpen()) { + m_signalLog.emit("[System] Error: Not connected."); + return; + } + + nlohmann::json req; + req["jsonrpc"] = "2.0"; + req["method"] = method; + if (!params.is_null()) { + req["params"] = params; + } + req["id"] = m_requestId++; + + std::string raw = req.dump() + "\n"; + if (m_client.send(raw)) { + m_signalLog.emit("[TX] " + req.dump()); + } else { + m_signalLog.emit("[System] Error sending command."); + } +} + +void AppController::readLoop() { + while (m_running) { + if (m_client.isOpen()) { + std::string line = m_client.receiveLine(); + if (line.empty()) { + if (m_running) { + m_running = false; + m_signalConnectionState.emit(false); + m_signalLog.emit("[System] Connection lost."); + } + break; + } + processResponse(line); + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + } +} + +void AppController::processResponse(const std::string& rawResponse) { + try { + nlohmann::json res = nlohmann::json::parse(rawResponse); + m_signalLog.emit("[RX] " + rawResponse); + + if (res.contains("error") && !res["error"].is_null()) { + m_signalLog.emit("[Error] " + res["error"].dump()); + return; + } + + if (res.contains("result")) { + auto result = res["result"]; + if (result.is_object()) { + if (result.contains("relay_id") && result.contains("state")) { + int relayId = result["relay_id"].get(); + bool state = result["state"].get(); + m_signalRelayState.emit(relayId, state); + } else { + for (auto& [key, val] : result.items()) { + try { + int relayId = std::stoi(key); + if (val.is_boolean()) { + m_signalRelayState.emit(relayId, val.get()); + } + } catch (...) {} + } + } + } + } + } catch (const std::exception& e) { + m_signalLog.emit("[System] Error parsing response: " + std::string(e.what())); + } +} + +} // namespace minihildesk diff --git a/sw/minihildesk/src/app_controller.h b/sw/minihildesk/src/app_controller.h new file mode 100644 index 0000000..d09b8c6 --- /dev/null +++ b/sw/minihildesk/src/app_controller.h @@ -0,0 +1,51 @@ +#pragma once +#include "config/config_manager.h" +#include "network/tcp_client.h" +#include +#include +#include +#include + +namespace minihildesk { + +class AppController { +public: + AppController(ConfigManager& config); + ~AppController(); + + TcpClient& getClient() { return m_client; } + + void start(); + void stop(); + + void requestConnect(const std::string& ip, int port, bool useSsl, bool useMtls); + void requestDisconnect(); + bool isConnected() const; + + void toggleRelay(int relayId, bool state); + void queryAllRelays(); + + sigc::signal& signal_log() { return m_signalLog; } + sigc::signal& signal_relay_state() { return m_signalRelayState; } + sigc::signal& signal_connection_state() { return m_signalConnectionState; } + + ConfigManager& getConfig() { return m_config; } + +private: + void readLoop(); + void processResponse(const std::string& rawResponse); + void sendJsonRpc(const std::string& method, const nlohmann::json& params = nlohmann::json()); + + ConfigManager& m_config; + TcpClient m_client; + std::thread m_readThread; + std::atomic m_running{false}; + + int m_requestId{1}; + + sigc::signal m_signalLog; + sigc::signal m_signalRelayState; + sigc::signal m_signalConnectionState; +}; + +} // namespace minihildesk diff --git a/sw/minihildesk/src/application.cc b/sw/minihildesk/src/application.cc new file mode 100644 index 0000000..8c4622e --- /dev/null +++ b/sw/minihildesk/src/application.cc @@ -0,0 +1,40 @@ +#include "application.h" +#include + +namespace minihildesk { + +EntryApplication::EntryApplication() : Gtk::Application("io.electux.minihildesk") {} + +EntryApplication::~EntryApplication() = default; + +Glib::RefPtr EntryApplication::create() { + return Glib::make_refptr_for_instance(new EntryApplication()); +} + +void EntryApplication::on_startup() { + Gtk::Application::on_startup(); + + m_config = std::make_unique(); + m_controller = std::make_unique(*m_config); + m_home = std::make_unique(*m_controller); + + m_controller->start(); + + add_window(*m_home); +} + +void EntryApplication::on_activate() { + Gtk::Application::on_activate(); + if (m_home) { + m_home->set_visible(true); + } +} + +void EntryApplication::on_shutdown() { + if (m_controller) { + m_controller->stop(); + } + Gtk::Application::on_shutdown(); +} + +} // namespace minihildesk diff --git a/sw/minihildesk/src/application.h b/sw/minihildesk/src/application.h new file mode 100644 index 0000000..e737a40 --- /dev/null +++ b/sw/minihildesk/src/application.h @@ -0,0 +1,28 @@ +#pragma once +#include +#include +#include "config/config_manager.h" +#include "app_controller.h" +#include "view/home.h" + +namespace minihildesk { + +class EntryApplication : public Gtk::Application { +public: + EntryApplication(); + ~EntryApplication() override; + + static Glib::RefPtr create(); + +protected: + void on_startup() override; + void on_activate() override; + void on_shutdown() override; + +private: + std::unique_ptr m_config; + std::unique_ptr m_controller; + std::unique_ptr m_home; +}; + +} // namespace minihildesk diff --git a/sw/minihildesk/src/config/config_manager.cc b/sw/minihildesk/src/config/config_manager.cc new file mode 100644 index 0000000..ce0ee12 --- /dev/null +++ b/sw/minihildesk/src/config/config_manager.cc @@ -0,0 +1,76 @@ +#include "config/config_manager.h" +#include +#include +#include +#include +#include + +namespace minihildesk { + +ConfigManager::ConfigManager() { + // Try loading on construction + load(); +} + +std::string ConfigManager::getConfigPath() const { + const char* home = std::getenv("HOME"); + if (!home) { + return "config.json"; + } + std::filesystem::path p(home); + p /= ".config"; + p /= "minihildesk"; + p /= "config.json"; + return p.string(); +} + +bool ConfigManager::load() { + std::string path = getConfigPath(); + if (!std::filesystem::exists(path)) { + return false; + } + try { + std::ifstream f(path); + if (!f.is_open()) return false; + nlohmann::json j; + f >> j; + if (j.contains("ip")) { + m_ip = j["ip"].get(); + } + if (j.contains("port")) { + m_port = j["port"].get(); + } + if (j.contains("ssl")) { + m_useSsl = j["ssl"].get(); + } + if (j.contains("mtls")) { + m_useMtls = j["mtls"].get(); + } + return true; + } catch (const std::exception& e) { + std::cerr << "[ConfigManager] Failed to load config: " << e.what() << std::endl; + return false; + } +} + +bool ConfigManager::save() const { + std::string path = getConfigPath(); + try { + std::filesystem::path p(path); + std::filesystem::create_directories(p.parent_path()); + std::ofstream f(path); + if (!f.is_open()) return false; + nlohmann::json j; + j["ip"] = m_ip; + j["port"] = m_port; + j["ssl"] = m_useSsl; + j["mtls"] = m_useMtls; + f << j.dump(4); + return true; + } catch (const std::exception& e) { + std::cerr << "[ConfigManager] Failed to save config: " << e.what() << std::endl; + return false; + } +} + +} // namespace minihildesk diff --git a/sw/minihildesk/src/config/config_manager.h b/sw/minihildesk/src/config/config_manager.h new file mode 100644 index 0000000..efb4ebf --- /dev/null +++ b/sw/minihildesk/src/config/config_manager.h @@ -0,0 +1,34 @@ +#pragma once +#include + +namespace minihildesk { + +class ConfigManager { +public: + ConfigManager(); + ~ConfigManager() = default; + + bool load(); + bool save() const; + + std::string getIp() const { return m_ip; } + void setIp(const std::string& ip) { m_ip = ip; } + + int getPort() const { return m_port; } + void setPort(int port) { m_port = port; } + + bool getUseSsl() const { return m_useSsl; } + void setUseSsl(bool useSsl) { m_useSsl = useSsl; } + + bool getUseMtls() const { return m_useMtls; } + void setUseMtls(bool useMtls) { m_useMtls = useMtls; } + +private: + std::string m_ip{"127.0.0.1"}; + int m_port{9000}; + bool m_useSsl{false}; + bool m_useMtls{false}; + std::string getConfigPath() const; +}; + +} // namespace minihildesk diff --git a/sw/minihildesk/src/main.cc b/sw/minihildesk/src/main.cc new file mode 100644 index 0000000..2657cbd --- /dev/null +++ b/sw/minihildesk/src/main.cc @@ -0,0 +1,8 @@ +#include "application.h" +#include + +int main(int argc, char* argv[]) { + std::signal(SIGPIPE, SIG_IGN); + auto app = minihildesk::EntryApplication::create(); + return app->run(argc, argv); +} diff --git a/sw/minihildesk/src/network/tcp_client.cc b/sw/minihildesk/src/network/tcp_client.cc new file mode 100644 index 0000000..410c82f --- /dev/null +++ b/sw/minihildesk/src/network/tcp_client.cc @@ -0,0 +1,235 @@ +#include "network/tcp_client.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace minihildesk { + +TcpClient::~TcpClient() { + disconnect(); +} + +bool TcpClient::connect(const std::string& ip, int port, bool useSsl, bool useMtls) { + disconnect(); + + std::lock_guard lock(m_mutex); + m_useSsl = useSsl; + + m_socketFd = ::socket(AF_INET, SOCK_STREAM, 0); + if (m_socketFd < 0) { + std::cerr << "[TcpClient] Failed to create socket." << std::endl; + return false; + } + + struct sockaddr_in serv_addr{}; + std::memset(&serv_addr, 0, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + serv_addr.sin_port = htons(static_cast(port)); + + if (::inet_pton(AF_INET, ip.c_str(), &serv_addr.sin_addr) <= 0) { + std::cerr << "[TcpClient] Invalid address / Address not supported." << std::endl; + ::close(m_socketFd); + m_socketFd = -1; + return false; + } + + if (::connect(m_socketFd, reinterpret_cast(&serv_addr), sizeof(serv_addr)) < 0) { + std::cerr << "[TcpClient] Connection failed." << std::endl; + ::close(m_socketFd); + m_socketFd = -1; + return false; + } + + // Initialize SSL if requested + if (m_useSsl) { + const SSL_METHOD* method = TLS_client_method(); + SSL_CTX* ctx = SSL_CTX_new(method); + if (!ctx) { + std::cerr << "[TcpClient] Failed to create SSL context." << std::endl; + ::close(m_socketFd); + m_socketFd = -1; + return false; + } + + m_sslCtx = ctx; + + if (useMtls) { + // Resolve path to ~/.config/minihildesk/ + const char* homedir = getenv("HOME"); + std::string configDir = ""; + if (homedir) { + configDir = std::string(homedir) + "/.config/minihildesk/"; + } else { + configDir = "./"; + } + std::string caPath = configDir + "ca.crt"; + std::string clientCertPath = configDir + "client.crt"; + std::string clientKeyPath = configDir + "client.key"; + + if (SSL_CTX_load_verify_locations(ctx, caPath.c_str(), nullptr) <= 0) { + std::cerr << "[TcpClient] mTLS error: Failed to load CA certificate from: " << caPath << std::endl; + ERR_print_errors_fp(stderr); + SSL_CTX_free(ctx); + m_sslCtx = nullptr; + ::close(m_socketFd); + m_socketFd = -1; + return false; + } + + if (SSL_CTX_use_certificate_file(ctx, clientCertPath.c_str(), SSL_FILETYPE_PEM) <= 0) { + std::cerr << "[TcpClient] mTLS error: Failed to use client certificate file: " << clientCertPath << std::endl; + ERR_print_errors_fp(stderr); + SSL_CTX_free(ctx); + m_sslCtx = nullptr; + ::close(m_socketFd); + m_socketFd = -1; + return false; + } + + if (SSL_CTX_use_PrivateKey_file(ctx, clientKeyPath.c_str(), SSL_FILETYPE_PEM) <= 0) { + std::cerr << "[TcpClient] mTLS error: Failed to use client private key file: " << clientKeyPath << std::endl; + ERR_print_errors_fp(stderr); + SSL_CTX_free(ctx); + m_sslCtx = nullptr; + ::close(m_socketFd); + m_socketFd = -1; + return false; + } + + // Enforce peer verification (validate server certificate) + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr); + std::cout << "[TcpClient] mTLS enabled. Loaded certificates from: " << configDir << std::endl; + } else { + // Bypass CA verification because we connect using local IPs with self-signed certs + SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr); + } + + SSL* ssl = SSL_new(ctx); + if (!ssl) { + std::cerr << "[TcpClient] Failed to create SSL object." << std::endl; + SSL_CTX_free(ctx); + m_sslCtx = nullptr; + ::close(m_socketFd); + m_socketFd = -1; + return false; + } + + SSL_set_fd(ssl, m_socketFd); + if (SSL_connect(ssl) <= 0) { + std::cerr << "[TcpClient] SSL connection handshake failed." << std::endl; + ERR_print_errors_fp(stderr); + SSL_free(ssl); + SSL_CTX_free(ctx); + m_sslCtx = nullptr; + m_ssl = nullptr; + ::close(m_socketFd); + m_socketFd = -1; + return false; + } + + m_ssl = ssl; + std::cout << "[TcpClient] SSL connection established successfully." << std::endl; + } + + return true; +} + +void TcpClient::disconnect() { + std::lock_guard lock(m_mutex); + if (m_ssl) { + SSL_free(static_cast(m_ssl)); + m_ssl = nullptr; + } + if (m_sslCtx) { + SSL_CTX_free(static_cast(m_sslCtx)); + m_sslCtx = nullptr; + } + if (m_socketFd >= 0) { + ::shutdown(m_socketFd, SHUT_RDWR); + ::close(m_socketFd); + m_socketFd = -1; + } + m_readBuffer.clear(); +} + +void TcpClient::shutdownSocket() { + std::lock_guard lock(m_mutex); + if (m_socketFd >= 0) { + ::shutdown(m_socketFd, SHUT_RDWR); + } +} + +bool TcpClient::isOpen() const { + std::lock_guard lock(m_mutex); + return m_socketFd >= 0; +} + +bool TcpClient::send(const std::string& message) { + int fd = -1; + SSL* ssl = nullptr; + { + std::lock_guard lock(m_mutex); + fd = m_socketFd; + ssl = static_cast(m_ssl); + } + if (fd < 0) return false; + + size_t totalSent = 0; + while (totalSent < message.size()) { + ssize_t sent = 0; + if (m_useSsl && ssl) { + sent = SSL_write(ssl, message.c_str() + totalSent, static_cast(message.size() - totalSent)); + } else { + sent = ::send(fd, message.c_str() + totalSent, message.size() - totalSent, 0); + } + + if (sent <= 0) { + return false; // Do not disconnect here, let the read thread handle it safely! + } + totalSent += static_cast(sent); + } + return true; +} + +std::string TcpClient::receiveLine() { + char buf[1024]; + while (true) { + size_t pos = m_readBuffer.find('\n'); + if (pos != std::string::npos) { + std::string line = m_readBuffer.substr(0, pos); + m_readBuffer.erase(0, pos + 1); + return line; + } + + int fd = -1; + SSL* ssl = nullptr; + { + std::lock_guard lock(m_mutex); + fd = m_socketFd; + ssl = static_cast(m_ssl); + } + if (fd < 0) { + return ""; + } + + ssize_t n = 0; + if (m_useSsl && ssl) { + n = SSL_read(ssl, buf, sizeof(buf) - 1); + } else { + n = ::recv(fd, buf, sizeof(buf) - 1, 0); + } + + if (n <= 0) { + return ""; + } + buf[n] = '\0'; + m_readBuffer.append(buf, static_cast(n)); + } +} + +} // namespace minihildesk diff --git a/sw/minihildesk/src/network/tcp_client.h b/sw/minihildesk/src/network/tcp_client.h new file mode 100644 index 0000000..d716f74 --- /dev/null +++ b/sw/minihildesk/src/network/tcp_client.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include + +namespace minihildesk { + +class TcpClient { +public: + TcpClient() = default; + ~TcpClient(); + + TcpClient(const TcpClient&) = delete; + TcpClient& operator=(const TcpClient&) = delete; + + bool connect(const std::string& ip, int port, bool useSsl = false, bool useMtls = false); + void disconnect(); + void shutdownSocket(); + bool isOpen() const; + + bool send(const std::string& message); + std::string receiveLine(); // reads until '\n' + +private: + int m_socketFd{-1}; + mutable std::mutex m_mutex; + std::string m_readBuffer; + + bool m_useSsl{false}; + void* m_sslCtx{nullptr}; // SSL_CTX* + void* m_ssl{nullptr}; // SSL* +}; + +} // namespace minihildesk diff --git a/sw/minihildesk/src/view/home.cc b/sw/minihildesk/src/view/home.cc new file mode 100644 index 0000000..0906645 --- /dev/null +++ b/sw/minihildesk/src/view/home.cc @@ -0,0 +1,216 @@ +#include "view/home.h" +#include +#include +#include +#include +#include +#include +#include + +namespace minihildesk { + +AppHome::AppHome(AppController& controller) : m_controller(controller) { + set_title("minihildesk"); + set_default_size(720, 560); + set_resizable(false); + + // Apply premium styling from compiled resources + auto cssProvider = Gtk::CssProvider::create(); + cssProvider->load_from_resource("/io/electux/minihildesk/style.css"); + Gtk::StyleContext::add_provider_for_display( + Gdk::Display::get_default(), cssProvider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + ); + + // Header Setup + m_ipEntry.set_text(m_controller.getConfig().getIp()); + m_ipEntry.set_width_chars(15); + m_portEntry.set_text(std::to_string(m_controller.getConfig().getPort())); + m_portEntry.set_width_chars(6); + m_sslCheck.set_active(m_controller.getConfig().getUseSsl()); + m_mtlsCheck.set_active(m_controller.getConfig().getUseMtls()); + + m_connectBtn.signal_clicked().connect( + sigc::mem_fun(*this, &AppHome::onConnectClicked) + ); + + // Smart toggling rules: mTLS requires SSL + m_mtlsCheck.signal_toggled().connect([this]() { + if (m_mtlsCheck.get_active()) { + m_sslCheck.set_active(true); + } + }); + m_sslCheck.signal_toggled().connect([this]() { + if (!m_sslCheck.get_active()) { + m_mtlsCheck.set_active(false); + } + }); + + m_headerBox.set_margin(10); + m_headerBox.append(m_ipLabel); + m_headerBox.append(m_ipEntry); + m_headerBox.append(m_portLabel); + m_headerBox.append(m_portEntry); + m_headerBox.append(m_sslCheck); + m_headerBox.append(m_mtlsCheck); + m_headerBox.append(m_connectBtn); + + // Grid of Relays (2 rows x 4 columns) + m_relayGrid.set_row_spacing(10); + m_relayGrid.set_column_spacing(10); + m_relayGrid.set_margin(10); + + for (int i = 0; i < 8; ++i) { + int relayId = i + 1; + auto widget = std::make_unique(relayId); + widget->signal_toggled().connect( + sigc::mem_fun(*this, &AppHome::onRelayToggled) + ); + + int row = i / 4; + int col = i % 4; + m_relayGrid.attach(*widget, col, row, 1, 1); + m_relayWidgets.push_back(std::move(widget)); + } + + // Traffic Log Setup + m_logTextView.set_editable(false); + m_logTextView.set_cursor_visible(false); + m_logTextView.set_wrap_mode(Gtk::WrapMode::CHAR); + m_logTextView.set_margin_start(10); + m_logTextView.set_margin_end(10); + m_logTextView.set_margin_bottom(10); + + m_logScrolled.set_child(m_logTextView); + m_logScrolled.set_vexpand(true); + m_logScrolled.set_hexpand(true); + m_logScrolled.set_size_request(-1, 220); + m_logScrolled.set_margin_start(10); + m_logScrolled.set_margin_end(10); + m_logScrolled.set_margin_bottom(10); + + // Main Box + m_mainBox.append(m_headerBox); + m_mainBox.append(m_relayGrid); + m_mainBox.append(m_logScrolled); + set_child(m_mainBox); + + // Dispatcher connects + m_logDispatcher.connect(sigc::mem_fun(*this, &AppHome::onLogDispatcher)); + m_relayDispatcher.connect(sigc::mem_fun(*this, &AppHome::onRelayDispatcher)); + m_connectionDispatcher.connect(sigc::mem_fun(*this, &AppHome::onConnectionDispatcher)); + + // Connect controller signals + m_controller.signal_log().connect([this](const std::string& msg) { postLogMessage(msg); }); + m_controller.signal_relay_state().connect(sigc::mem_fun(*this, &AppHome::onRelayStateUpdated)); + m_controller.signal_connection_state().connect(sigc::mem_fun(*this, &AppHome::onConnectionStateUpdated)); + + // Setup periodic polling timeout (polls states if connected) + m_pollConnection = Glib::signal_timeout().connect( + sigc::mem_fun(*this, &AppHome::onPollTimeout), 1000 + ); + + // Set initial sensitivites + onConnectionStateUpdated(m_controller.isConnected()); +} + +void AppHome::onConnectClicked() { + if (m_controller.isConnected()) { + m_controller.requestDisconnect(); + } else { + std::string ip = m_ipEntry.get_text(); + int port = 9000; + try { + port = std::stoi(m_portEntry.get_text()); + } catch (...) {} + bool useSsl = m_sslCheck.get_active(); + bool useMtls = m_mtlsCheck.get_active(); + m_controller.requestConnect(ip, port, useSsl, useMtls); + } +} + +void AppHome::onRelayToggled(int relayId, bool state) { + m_controller.toggleRelay(relayId, state); +} + +void AppHome::postLogMessage(const std::string& msg) { + std::lock_guard lock(m_logMutex); + + // Prefix log with timestamp + auto now = std::chrono::system_clock::now(); + auto in_time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %H:%M:%S") << " " << msg; + + m_logQueue.push(ss.str()); + m_logDispatcher.emit(); +} + +void AppHome::onLogDispatcher() { + std::lock_guard lock(m_logMutex); + auto buffer = m_logTextView.get_buffer(); + while (!m_logQueue.empty()) { + buffer->insert(buffer->end(), m_logQueue.front() + "\n"); + m_logQueue.pop(); + } + + // Scroll to end of text view + auto mark = buffer->get_insert(); + m_logTextView.scroll_to(mark, 0.0); +} + +void AppHome::onRelayStateUpdated(int relayId, bool state) { + std::lock_guard lock(m_relayMutex); + m_relayQueue.push(RelayStateUpdate{relayId, state}); + m_relayDispatcher.emit(); +} + +void AppHome::onRelayDispatcher() { + std::lock_guard lock(m_relayMutex); + while (!m_relayQueue.empty()) { + auto update = m_relayQueue.front(); + m_relayQueue.pop(); + + if (update.id >= 1 && update.id <= 8) { + m_relayWidgets[update.id - 1]->updateState(update.state); + } + } +} + +void AppHome::onConnectionStateUpdated(bool connected) { + std::lock_guard lock(m_connectionMutex); + m_connectionState = connected; + m_connectionDispatcher.emit(); +} + +void AppHome::onConnectionDispatcher() { + std::lock_guard lock(m_connectionMutex); + if (m_connectionState) { + m_connectBtn.set_label("Disconnect"); + m_ipEntry.set_sensitive(false); + m_portEntry.set_sensitive(false); + m_sslCheck.set_sensitive(false); + m_mtlsCheck.set_sensitive(false); + for (auto& widget : m_relayWidgets) { + widget->set_sensitive(true); + } + } else { + m_connectBtn.set_label("Connect"); + m_ipEntry.set_sensitive(true); + m_portEntry.set_sensitive(true); + m_sslCheck.set_sensitive(true); + m_mtlsCheck.set_sensitive(true); + for (auto& widget : m_relayWidgets) { + widget->updateState(false); + widget->set_sensitive(false); + } + } +} + +bool AppHome::onPollTimeout() { + if (m_controller.isConnected()) { + m_controller.queryAllRelays(); + } + return true; // Keep timer running +} + +} // namespace minihildesk diff --git a/sw/minihildesk/src/view/home.h b/sw/minihildesk/src/view/home.h new file mode 100644 index 0000000..b07b467 --- /dev/null +++ b/sw/minihildesk/src/view/home.h @@ -0,0 +1,85 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "app_controller.h" +#include "view/relay_widget.h" + +namespace minihildesk { + +class AppHome : public Gtk::ApplicationWindow { +public: + AppHome(AppController& controller); + ~AppHome() override = default; + + void postLogMessage(const std::string& msg); + +private: + void onConnectClicked(); + void onRelayToggled(int relayId, bool state); + + // Signal handlers from controller + void onRelayStateUpdated(int relayId, bool state); + void onConnectionStateUpdated(bool connected); + + // Thread-safe UI update handlers + void onLogDispatcher(); + void onRelayDispatcher(); + void onConnectionDispatcher(); + + bool onPollTimeout(); + + AppController& m_controller; + + // Layout boxes + Gtk::Box m_mainBox{Gtk::Orientation::VERTICAL, 10}; + Gtk::Box m_headerBox{Gtk::Orientation::HORIZONTAL, 10}; + Gtk::Grid m_relayGrid; + Gtk::ScrolledWindow m_logScrolled; + Gtk::TextView m_logTextView; + + // Header controls + Gtk::Label m_ipLabel{"IP Address:"}; + Gtk::Entry m_ipEntry; + Gtk::Label m_portLabel{"Port:"}; + Gtk::Entry m_portEntry; + Gtk::CheckButton m_sslCheck{"SSL"}; + Gtk::CheckButton m_mtlsCheck{"mTLS"}; + Gtk::Button m_connectBtn{"Connect"}; + + // 8 Relay Widgets + std::vector> m_relayWidgets; + + // Thread synchronization + Glib::Dispatcher m_logDispatcher; + std::queue m_logQueue; + std::mutex m_logMutex; + + Glib::Dispatcher m_relayDispatcher; + struct RelayStateUpdate { + int id; + bool state; + }; + std::queue m_relayQueue; + std::mutex m_relayMutex; + + Glib::Dispatcher m_connectionDispatcher; + bool m_connectionState{false}; + std::mutex m_connectionMutex; + + // Polling connection + sigc::connection m_pollConnection; +}; + +} // namespace minihildesk diff --git a/sw/minihildesk/src/view/relay_widget.cc b/sw/minihildesk/src/view/relay_widget.cc new file mode 100644 index 0000000..2f4d716 --- /dev/null +++ b/sw/minihildesk/src/view/relay_widget.cc @@ -0,0 +1,78 @@ +#include "view/relay_widget.h" +#include +#include + +namespace minihildesk { + +RelayWidget::RelayWidget(int relayId) : m_relayId(relayId) { + set_margin(5); + + // Set card styling class + get_style_context()->add_class("relay-card"); + + m_titleLabel.set_markup("Relay " + std::to_string(relayId) + ""); + m_titleLabel.set_halign(Gtk::Align::START); + + // Indicator label displays filled circle unicode + m_indicatorLabel.set_text("●"); + m_indicatorLabel.set_halign(Gtk::Align::END); + m_indicatorLabel.set_hexpand(true); + m_indicatorLabel.get_style_context()->add_class("led-indicator"); + m_indicatorLabel.get_style_context()->add_class("led-off"); + + m_headerBox.append(m_titleLabel); + m_headerBox.append(m_indicatorLabel); + + m_switch.set_halign(Gtk::Align::CENTER); + m_switch.set_valign(Gtk::Align::CENTER); + m_switch.set_margin_bottom(10); + m_switch.set_margin_top(5); + + // Block default toggling behavior to handle it asynchronously + m_switch.signal_state_set().connect( + sigc::mem_fun(*this, &RelayWidget::onStateSet), false + ); + + m_box.append(m_headerBox); + m_box.append(m_switch); + m_box.set_margin(12); + + set_child(m_box); +} + +void RelayWidget::updateState(bool active) { + m_active = active; + + // Temporarily disable the signal handler logic to prevent feedback loops + m_updating = true; + m_switch.set_active(active); + m_switch.set_state(active); + m_updating = false; + + if (active) { + m_indicatorLabel.get_style_context()->remove_class("led-off"); + m_indicatorLabel.get_style_context()->add_class("led-on"); + } else { + m_indicatorLabel.get_style_context()->remove_class("led-on"); + m_indicatorLabel.get_style_context()->add_class("led-off"); + } +} + +bool RelayWidget::getState() const { + return m_active; +} + +bool RelayWidget::onStateSet(bool state) { + if (m_updating) { + // Let state propagate visually + return false; + } + // Emit signal to parent, which will request the change via TCP + m_signalToggled.emit(m_relayId, state); + + // Return false to allow the switch to slide immediately for smooth UI response. + // If the server fails to update, the next periodic status poll will correct it. + return false; +} + +} // namespace minihildesk diff --git a/sw/minihildesk/src/view/relay_widget.h b/sw/minihildesk/src/view/relay_widget.h new file mode 100644 index 0000000..cfa9b73 --- /dev/null +++ b/sw/minihildesk/src/view/relay_widget.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace minihildesk { + +class RelayWidget : public Gtk::Frame { +public: + explicit RelayWidget(int relayId); + ~RelayWidget() override = default; + + void updateState(bool active); + bool getState() const; + + sigc::signal& signal_toggled() { return m_signalToggled; } + +private: + bool onStateSet(bool state); + + int m_relayId; + bool m_active{false}; + bool m_updating{false}; + + Gtk::Box m_box{Gtk::Orientation::VERTICAL, 8}; + Gtk::Box m_headerBox{Gtk::Orientation::HORIZONTAL, 5}; + Gtk::Label m_titleLabel; + Gtk::Label m_indicatorLabel; + Gtk::Switch m_switch; + + sigc::signal m_signalToggled; +}; + +} // namespace minihildesk