├── ai ├── .aidev.json ├── scripts │ ├── __init__.py │ └── ai_dev_helper.py ├── requirements.txt ├── ai-dev └── README.md ├── docs └── img │ └── ESP32MCPServer.png ├── .gitignore ├── test ├── test_main.cpp ├── mock │ ├── mock_wifi.h │ ├── mock_websocket.h │ ├── mock_littlefs.h │ └── MockLittleFS.h ├── test_request_queue.cpp ├── test_network_manager.cpp ├── test_mock_little_fs.cpp ├── test_metrics_system.cpp └── test_mcp_server.cpp ├── platformio.ini ├── LICENSE ├── include ├── MCPTypes.h ├── RequestQueue.h ├── MCPServer.h ├── NetworkManager.h ├── uLogger.h └── MetricsSystem.h ├── src ├── main.cpp ├── MCPServer.cpp ├── uLogger.cpp ├── MetricsSystem.cpp └── NetworkManager.cpp ├── README.md ├── data ├── wifi_setup.html └── metrics_stats.html ├── HOWTO.md ├── CONTRIBUTING.md └── examples └── client └── client.ts /ai/.aidev.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ai/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ai/requirements.txt: -------------------------------------------------------------------------------- 1 | argparse>=1.4.0 2 | python-dateutil>=2.8.2 3 | typing>=3.7.4.3 -------------------------------------------------------------------------------- /docs/img/ESP32MCPServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navado/ESP32MCPServer/HEAD/docs/img/ESP32MCPServer.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode 3 | *.code-workspace 4 | .vscode/.browse.c_cpp.db* 5 | .vscode/c_cpp_properties.json 6 | .vscode/launch.json 7 | .vscode/ipch 8 | .DS_Store 9 | *.swp 10 | *.swo 11 | build/ 12 | dist/ 13 | __pycache__/ 14 | *.pyc 15 | .pytest_cache/ 16 | *.log 17 | credentials.json 18 | -------------------------------------------------------------------------------- /ai/ai-dev: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # AI Development Helper wrapper script 4 | # Place this in the project root as 'ai-dev' 5 | 6 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 7 | PYTHON_SCRIPT="$SCRIPT_DIR/scripts/ai_dev_helper.py" 8 | 9 | # Ensure the script is executable 10 | chmod +x "$PYTHON_SCRIPT" 11 | 12 | # Forward all arguments to the Python script 13 | python3 "$PYTHON_SCRIPT" "$@" -------------------------------------------------------------------------------- /test/test_main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Declare test functions from other test files 4 | void test_request_queue(); 5 | void test_network_manager(); 6 | void test_mcp_server(); 7 | 8 | void setUp(void) { 9 | // Global setup 10 | } 11 | 12 | void tearDown(void) { 13 | // Global cleanup 14 | } 15 | 16 | int main(int argc, char **argv) { 17 | UNITY_BEGIN(); 18 | 19 | // Run all test groups 20 | test_request_queue(); 21 | test_network_manager(); 22 | test_mcp_server(); 23 | 24 | return UNITY_END(); 25 | } -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [env:esp32-s3-devkitc-1] 2 | platform = espressif32 3 | board = esp32-s3-devkitc-1 4 | framework = arduino 5 | lib_deps = 6 | fastled/FastLED@3.9.7 7 | bblanchon/ArduinoJson 8 | https://github.com/me-no-dev/ESPAsyncWebServer.git 9 | https://github.com/me-no-dev/AsyncTCP.git 10 | links2004/WebSockets@^2.6.1 11 | WiFi 12 | WiFiClientSecure 13 | monitor_speed = 115200 14 | board_build.filesystem = littlefs 15 | build_unflags = -std=gnu++11 16 | build_flags = 17 | -std=gnu++17 18 | -D CORE_DEBUG_LEVEL=5 19 | -D CONFIG_ASYNC_TCP_RUNNING_CORE=0 20 | -D WEBSOCKET_MAX_QUEUED_MESSAGES=32 21 | -D ASYNCWEBSERVER_REGEX=0 22 | 23 | 24 | 25 | 26 | [env:native] 27 | platform = native 28 | lib_deps = 29 | throwtheswitch/Unity@^2.5.2 30 | test_build_project_src = yes 31 | build_flags = 32 | -D UNITY_INCLUDE_CONFIG_H 33 | -D NATIVE_TEST -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ESP32 MCP Server Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /include/MCPTypes.h: -------------------------------------------------------------------------------- 1 | #ifndef MCP_TYPES_H 2 | #define MCP_TYPES_H 3 | 4 | #include 5 | #include 6 | 7 | namespace mcp { 8 | 9 | enum class MCPRequestType { 10 | INITIALIZE, 11 | RESOURCES_LIST, 12 | RESOURCE_READ, 13 | SUBSCRIBE, 14 | UNSUBSCRIBE 15 | }; 16 | 17 | using RequestId = uint32_t; 18 | 19 | struct MCPRequest { 20 | MCPRequestType type; 21 | RequestId id; 22 | JsonObject params; 23 | 24 | MCPRequest() : type(MCPRequestType::INITIALIZE), id(0), params() {} 25 | }; 26 | 27 | struct MCPResponse { 28 | bool success; 29 | std::string message; 30 | JsonVariant data; 31 | 32 | MCPResponse() : success(false), message(""), data() {} 33 | MCPResponse(bool s, const std::string &msg, JsonVariant d) 34 | : success(s), message(msg), data(d) {} 35 | }; 36 | 37 | struct MCPResource { 38 | std::string name; 39 | std::string uri; 40 | std::string type; 41 | std::string value; 42 | 43 | MCPResource(const std::string &n, const std::string &u, const std::string &t, const std::string &v) 44 | : name(n), uri(u), type(t), value(v) {} 45 | }; 46 | 47 | } // namespace mcp 48 | 49 | #endif // MCP_TYPES_H 50 | -------------------------------------------------------------------------------- /include/RequestQueue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | template 7 | class RequestQueue { 8 | public: 9 | RequestQueue(size_t maxSize = 32) : maxQueueSize(maxSize) {} 10 | 11 | bool push(const T& item) { 12 | std::lock_guard lock(mutex); 13 | if (queue.size() >= maxQueueSize) { 14 | return false; 15 | } 16 | queue.push(item); 17 | return true; 18 | } 19 | 20 | bool pop(T& item) { 21 | std::lock_guard lock(mutex); 22 | if (queue.empty()) { 23 | return false; 24 | } 25 | item = queue.front(); 26 | queue.pop(); 27 | return true; 28 | } 29 | 30 | bool empty() const { 31 | std::lock_guard lock(mutex); 32 | return queue.empty(); 33 | } 34 | 35 | size_t size() const { 36 | std::lock_guard lock(mutex); 37 | return queue.size(); 38 | } 39 | 40 | void clear() { 41 | std::lock_guard lock(mutex); 42 | std::queue empty; 43 | std::swap(queue, empty); 44 | } 45 | 46 | private: 47 | std::queue queue; 48 | mutable std::mutex mutex; 49 | const size_t maxQueueSize; 50 | }; -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "NetworkManager.h" 4 | #include "MCPServer.h" 5 | 6 | using namespace mcp; 7 | // Global instances 8 | NetworkManager networkManager; 9 | MCPServer mcpServer; 10 | 11 | // Task handles 12 | TaskHandle_t mcpTaskHandle = nullptr; 13 | 14 | // MCP task function 15 | void mcpTask(void* parameter) { 16 | while (true) { 17 | mcpServer.handleClient(); 18 | vTaskDelay(pdMS_TO_TICKS(10)); 19 | } 20 | } 21 | 22 | void setup() { 23 | Serial.begin(115200); 24 | Serial.println("Starting up..."); 25 | 26 | // Initialize LittleFS 27 | if (!LittleFS.begin(true)) { 28 | Serial.println("LittleFS Mount Failed"); 29 | return; 30 | } 31 | 32 | // Start network manager 33 | networkManager.begin(); 34 | 35 | // Wait for network connection or AP mode 36 | while (!networkManager.isConnected() && networkManager.getIPAddress().isEmpty()) { 37 | delay(100); 38 | } 39 | 40 | Serial.print("Device IP: "); 41 | Serial.println(networkManager.getIPAddress()); 42 | 43 | // Initialize MCP server 44 | mcpServer.begin(networkManager.isConnected()); 45 | 46 | // Create MCP task 47 | xTaskCreatePinnedToCore( 48 | mcpTask, 49 | "MCPTask", 50 | 8192, 51 | nullptr, 52 | 1, 53 | &mcpTaskHandle, 54 | 1 // Run on core 1 55 | ); 56 | } 57 | 58 | void loop() { 59 | // Main loop can be used for other tasks 60 | // Network and MCP handling is done in their respective tasks 61 | delay(1000); 62 | } -------------------------------------------------------------------------------- /include/MCPServer.h: -------------------------------------------------------------------------------- 1 | #ifndef MCP_SERVER_H 2 | #define MCP_SERVER_H 3 | 4 | #include 5 | #include "MCPTypes.h" 6 | #include 7 | #include 8 | #include 9 | 10 | namespace mcp { 11 | 12 | struct Implementation { 13 | std::string name; 14 | std::string version; 15 | }; 16 | 17 | struct ServerCapabilities { 18 | bool supportsSubscriptions; 19 | bool supportsResources; 20 | }; 21 | 22 | class MCPServer { 23 | public: 24 | MCPServer(uint16_t port = 9000); 25 | 26 | void begin(bool isConnected); 27 | void handleClient(); 28 | void handleInitialize(uint8_t clientId, const RequestId &id, const JsonObject ¶ms); 29 | void handleResourcesList(uint8_t clientId, const RequestId &id, const JsonObject ¶ms); 30 | void handleResourceRead(uint8_t clientId, const RequestId &id, const JsonObject ¶ms); 31 | void handleSubscribe(uint8_t clientId, const RequestId &id, const JsonObject ¶ms); 32 | void handleUnsubscribe(uint8_t clientId, const RequestId &id, const JsonObject ¶ms); 33 | void unregisterResource(const std::string &uri); 34 | void sendResponse(uint8_t clientId, const RequestId &id, const MCPResponse &response); 35 | void sendError(uint8_t clientId, const RequestId &id, int code, const std::string &message); 36 | void broadcastResourceUpdate(const std::string &uri); 37 | 38 | private: 39 | uint16_t port_; 40 | Implementation serverInfo{"esp32-mcp-server", "1.0.0"}; 41 | ServerCapabilities capabilities{true, true}; 42 | 43 | MCPRequest parseRequest(const std::string &json); 44 | std::string serializeResponse(const RequestId &id, const MCPResponse &response); 45 | }; 46 | 47 | } // namespace mcp 48 | 49 | #endif // MCP_SERVER_H 50 | -------------------------------------------------------------------------------- /test/mock/mock_wifi.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | // Mock WiFi class for testing 8 | class MockWiFi { 9 | public: 10 | static bool isConnected() { return connected; } 11 | static void setConnected(bool state) { connected = state; } 12 | 13 | static void setAPIP(const std::string& ip) { apIP = ip; } 14 | static void setSTAIP(const std::string& ip) { staIP = ip; } 15 | 16 | static std::string getAPIP() { return apIP; } 17 | static std::string getSTAIP() { return staIP; } 18 | 19 | private: 20 | static bool connected; 21 | static std::string apIP; 22 | static std::string staIP; 23 | }; 24 | 25 | bool MockWiFi::connected = false; 26 | std::string MockWiFi::apIP = "192.168.4.1"; 27 | std::string MockWiFi::staIP = "192.168.1.100"; 28 | 29 | // Mock WiFi event handler 30 | struct MockWiFiEvent { 31 | enum EventType { 32 | CONNECTED, 33 | DISCONNECTED, 34 | GOT_IP, 35 | AP_START, 36 | AP_STOP 37 | }; 38 | 39 | EventType type; 40 | std::string data; 41 | }; 42 | 43 | class MockWiFiEventHandler { 44 | public: 45 | static void onEvent(MockWiFiEvent event) { 46 | for (auto& handler : handlers) { 47 | handler(event); 48 | } 49 | } 50 | 51 | static void addHandler(std::function handler) { 52 | handlers.push_back(handler); 53 | } 54 | 55 | static void clearHandlers() { 56 | handlers.clear(); 57 | } 58 | 59 | private: 60 | static std::vector> handlers; 61 | }; 62 | 63 | std::vector> MockWiFiEventHandler::handlers; 64 | 65 | #define WIFI_EVENT_STA_CONNECTED MockWiFiEvent::CONNECTED 66 | #define WIFI_EVENT_STA_DISCONNECTED MockWiFiEvent::DISCONNECTED 67 | #define WIFI_EVENT_STA_GOT_IP MockWiFiEvent::GOT_IP 68 | #define WIFI_EVENT_AP_START MockWiFiEvent::AP_START 69 | #define WIFI_EVENT_AP_STOP MockWiFiEvent::AP_STOP -------------------------------------------------------------------------------- /test/test_request_queue.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "RequestQueue.h" 3 | #include 4 | #include 5 | 6 | void setUp(void) { 7 | // Set up any test prerequisites 8 | } 9 | 10 | void tearDown(void) { 11 | // Clean up after tests 12 | } 13 | 14 | void test_request_queue_push_pop() { 15 | RequestQueue queue; 16 | TEST_ASSERT_TRUE(queue.empty()); 17 | 18 | queue.push(42); 19 | TEST_ASSERT_FALSE(queue.empty()); 20 | TEST_ASSERT_EQUAL(1, queue.size()); 21 | 22 | int value; 23 | TEST_ASSERT_TRUE(queue.pop(value)); 24 | TEST_ASSERT_EQUAL(42, value); 25 | TEST_ASSERT_TRUE(queue.empty()); 26 | } 27 | 28 | void test_request_queue_multiple_items() { 29 | RequestQueue queue; 30 | 31 | for (int i = 0; i < 5; i++) { 32 | queue.push(i); 33 | } 34 | 35 | TEST_ASSERT_EQUAL(5, queue.size()); 36 | 37 | for (int i = 0; i < 5; i++) { 38 | int value; 39 | TEST_ASSERT_TRUE(queue.pop(value)); 40 | TEST_ASSERT_EQUAL(i, value); 41 | } 42 | 43 | TEST_ASSERT_TRUE(queue.empty()); 44 | } 45 | 46 | void test_request_queue_thread_safety() { 47 | RequestQueue queue; 48 | std::atomic done(false); 49 | 50 | // Producer thread 51 | std::thread producer([&]() { 52 | for (int i = 0; i < 100; i++) { 53 | queue.push(i); 54 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 55 | } 56 | done = true; 57 | }); 58 | 59 | // Consumer thread 60 | std::thread consumer([&]() { 61 | int count = 0; 62 | while (!done || !queue.empty()) { 63 | int value; 64 | if (queue.pop(value)) { 65 | TEST_ASSERT_LESS_THAN(100, value); 66 | count++; 67 | } 68 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 69 | } 70 | TEST_ASSERT_EQUAL(100, count); 71 | }); 72 | 73 | producer.join(); 74 | consumer.join(); 75 | } 76 | 77 | int runUnityTests() { 78 | UNITY_BEGIN(); 79 | 80 | RUN_TEST(test_request_queue_push_pop); 81 | RUN_TEST(test_request_queue_multiple_items); 82 | RUN_TEST(test_request_queue_thread_safety); 83 | 84 | return UNITY_END(); 85 | } 86 | 87 | #ifdef ARDUINO 88 | void setup() { 89 | delay(2000); 90 | runUnityTests(); 91 | } 92 | 93 | void loop() { 94 | } 95 | #else 96 | int main() { 97 | return runUnityTests(); 98 | } 99 | #endif -------------------------------------------------------------------------------- /include/NetworkManager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "RequestQueue.h" 9 | 10 | enum class NetworkState { 11 | INIT, 12 | CONNECTING, 13 | CONNECTED, 14 | CONNECTION_FAILED, 15 | AP_MODE 16 | }; 17 | 18 | struct NetworkCredentials { 19 | String ssid; 20 | String password; 21 | bool valid; 22 | }; 23 | 24 | struct NetworkRequest { 25 | enum class Type { 26 | CONNECT, 27 | START_AP, 28 | CHECK_CONNECTION 29 | }; 30 | 31 | Type type; 32 | String data; 33 | }; 34 | 35 | class NetworkManager { 36 | public: 37 | NetworkManager(); 38 | void begin(); 39 | bool isConnected(); 40 | String getIPAddress(); 41 | String getSSID(); 42 | 43 | private: 44 | static constexpr uint32_t CONNECT_TIMEOUT = 15000; // 15 seconds 45 | static constexpr uint8_t MAX_CONNECT_ATTEMPTS = 3; 46 | static constexpr uint16_t RECONNECT_INTERVAL = 5000; // 5 seconds 47 | static constexpr const char* SETUP_PAGE_PATH = "/wifi_setup.html"; 48 | 49 | NetworkState state; 50 | Preferences preferences; 51 | AsyncWebServer server; 52 | AsyncWebSocket ws; 53 | RequestQueue requestQueue; 54 | TaskHandle_t networkTaskHandle; 55 | 56 | String apSSID; 57 | uint8_t connectAttempts; 58 | uint32_t lastConnectAttempt; 59 | NetworkCredentials credentials; 60 | 61 | void setupWebServer(); 62 | void handleRequest(const NetworkRequest& request); 63 | 64 | void startAP(); 65 | void connect(); 66 | void checkConnection(); 67 | 68 | bool loadCredentials(); 69 | void saveCredentials(const String& ssid, const String& password); 70 | void clearCredentials(); 71 | 72 | String generateUniqueSSID(); 73 | void queueRequest(NetworkRequest::Type type, const String& data = ""); 74 | 75 | // Web handlers 76 | void handleRoot(AsyncWebServerRequest *request); 77 | void handleSave(AsyncWebServerRequest *request); 78 | void handleStatus(AsyncWebServerRequest *request); 79 | void onWebSocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, 80 | AwsEventType type, void* arg, uint8_t* data, size_t len); 81 | 82 | static void networkTaskCode(void* parameter); 83 | void networkTask(); 84 | static String getNetworkStatusJson(NetworkState state, const String& ssid, const String& ip); 85 | }; -------------------------------------------------------------------------------- /test/mock/mock_websocket.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // Mock WebSocket client for testing 9 | class MockWebSocketClient { 10 | public: 11 | virtual ~MockWebSocketClient() = default; 12 | 13 | bool isConnected() const { return connected; } 14 | void connect() { connected = true; } 15 | void disconnect() { connected = false; } 16 | 17 | virtual void onMessage(const std::string& message) = 0; 18 | 19 | void sendMessage(const std::string& message) { 20 | outgoing_messages.push(message); 21 | } 22 | 23 | std::string receiveMessage() { 24 | if (incoming_messages.empty()) { 25 | return ""; 26 | } 27 | std::string msg = incoming_messages.front(); 28 | incoming_messages.pop(); 29 | return msg; 30 | } 31 | 32 | void processOutgoingMessages() { 33 | while (!outgoing_messages.empty()) { 34 | onMessage(outgoing_messages.front()); 35 | outgoing_messages.pop(); 36 | } 37 | } 38 | 39 | void queueIncomingMessage(const std::string& message) { 40 | incoming_messages.push(message); 41 | } 42 | 43 | private: 44 | bool connected = false; 45 | std::queue incoming_messages; 46 | std::queue outgoing_messages; 47 | }; 48 | 49 | // Mock WebSocket server for testing 50 | class MockWebSocketServer { 51 | public: 52 | void begin() { running = true; } 53 | void stop() { running = false; } 54 | bool isRunning() const { return running; } 55 | 56 | void addClient(MockWebSocketClient* client) { 57 | clients.push_back(client); 58 | } 59 | 60 | void removeClient(MockWebSocketClient* client) { 61 | auto it = std::find(clients.begin(), clients.end(), client); 62 | if (it != clients.end()) { 63 | clients.erase(it); 64 | } 65 | } 66 | 67 | void broadcastMessage(const std::string& message) { 68 | for (auto client : clients) { 69 | if (client->isConnected()) { 70 | client->queueIncomingMessage(message); 71 | } 72 | } 73 | } 74 | 75 | void processMessages() { 76 | for (auto client : clients) { 77 | if (client->isConnected()) { 78 | client->processOutgoingMessages(); 79 | } 80 | } 81 | } 82 | 83 | private: 84 | bool running = false; 85 | std::vector clients; 86 | }; 87 | 88 | // Mock WebSocket event types 89 | enum MockWebSocketEventType { 90 | WStype_DISCONNECTED, 91 | WStype_CONNECTED, 92 | WStype_TEXT, 93 | WStype_ERROR 94 | }; 95 | 96 | using MockWebSocketEventCallback = std::function; -------------------------------------------------------------------------------- /test/mock/mock_littlefs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // Mock File class for testing 9 | class MockFile { 10 | public: 11 | MockFile(const std::string& name, const std::string& content) 12 | : name_(name), content_(content), position_(0) {} 13 | 14 | size_t size() const { return content_.size(); } 15 | const char* name() const { return name_.c_str(); } 16 | 17 | size_t read(uint8_t* buf, size_t size) { 18 | if (position_ >= content_.size()) return 0; 19 | size_t available = content_.size() - position_; 20 | size_t to_read = std::min(size, available); 21 | memcpy(buf, content_.c_str() + position_, to_read); 22 | position_ += to_read; 23 | return to_read; 24 | } 25 | 26 | size_t write(const uint8_t* buf, size_t size) { 27 | std::string new_content((char*)buf, size); 28 | if (position_ >= content_.size()) { 29 | content_ += new_content; 30 | } else { 31 | content_.replace(position_, size, new_content); 32 | } 33 | position_ += size; 34 | return size; 35 | } 36 | 37 | void seek(size_t position) { 38 | position_ = std::min(position, content_.size()); 39 | } 40 | 41 | private: 42 | std::string name_; 43 | std::string content_; 44 | size_t position_; 45 | }; 46 | 47 | // Mock LittleFS class for testing 48 | class MockLittleFS { 49 | public: 50 | bool begin(bool formatOnFail = false) { 51 | if (!formatted_ && formatOnFail) { 52 | format(); 53 | } 54 | return formatted_; 55 | } 56 | 57 | void format() { 58 | files_.clear(); 59 | formatted_ = true; 60 | } 61 | 62 | bool exists(const char* path) { 63 | return files_.find(path) != files_.end(); 64 | } 65 | 66 | std::shared_ptr open(const char* path, const char* mode = "r") { 67 | if (strcmp(mode, "w") == 0) { 68 | auto file = std::make_shared(path, ""); 69 | files_[path] = file; 70 | return file; 71 | } 72 | 73 | auto it = files_.find(path); 74 | if (it != files_.end()) { 75 | return it->second; 76 | } 77 | return nullptr; 78 | } 79 | 80 | bool remove(const char* path) { 81 | return files_.erase(path) > 0; 82 | } 83 | 84 | bool rename(const char* from, const char* to) { 85 | auto it = files_.find(from); 86 | if (it != files_.end()) { 87 | files_[to] = it->second; 88 | files_.erase(it); 89 | return true; 90 | } 91 | return false; 92 | } 93 | 94 | std::vector listFiles() const { 95 | std::vector result; 96 | for (const auto& pair : files_) { 97 | result.push_back(pair.first); 98 | } 99 | return result; 100 | } 101 | 102 | private: 103 | bool formatted_ = false; 104 | std::map> files_; 105 | }; 106 | 107 | extern MockLittleFS LittleFS; -------------------------------------------------------------------------------- /include/uLogger.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class uLogger { 10 | public: 11 | static const size_t MAX_NAME_LENGTH = 64; 12 | static const size_t MAX_DATA_LENGTH = 128; 13 | 14 | // Record structure for storing metric data 15 | struct Record { 16 | uint64_t timestamp; // Timestamp in milliseconds 17 | char name[MAX_NAME_LENGTH]; // Metric name 18 | uint16_t dataSize; // Size of data payload 19 | uint8_t data[MAX_DATA_LENGTH]; // Data payload 20 | 21 | Record() : timestamp(0), dataSize(0) { 22 | memset(name, 0, MAX_NAME_LENGTH); 23 | memset(data, 0, MAX_DATA_LENGTH); 24 | } 25 | }; 26 | 27 | uLogger(); 28 | ~uLogger(); 29 | 30 | /** 31 | * Initialize the logger 32 | * @param logFile Path to log file (default: "/metrics.log") 33 | * @return true if initialization successful 34 | */ 35 | bool begin(const char* logFile = "/metrics.log"); 36 | 37 | /** 38 | * Shutdown the logger 39 | */ 40 | void end(); 41 | 42 | /** 43 | * Log a metric record 44 | * @param name Metric name 45 | * @param data Pointer to data 46 | * @param dataSize Size of data in bytes 47 | * @return true if log successful 48 | */ 49 | bool logMetric(const char* name, const void* data, size_t dataSize); 50 | 51 | /** 52 | * Query metric records 53 | * @param name Metric name (empty string for all metrics) 54 | * @param startTime Start timestamp (0 for all time) 55 | * @param records Vector to store matching records 56 | * @return Number of records found 57 | */ 58 | size_t queryMetrics(const char* name, uint64_t startTime, std::vector& records); 59 | 60 | /** 61 | * Query metrics with callback 62 | * @param callback Function to call for each matching record 63 | * @param name Metric name filter 64 | * @param startTime Start timestamp filter 65 | * @return Number of records processed 66 | */ 67 | size_t queryMetrics(std::function callback, 68 | const char* name = "", uint64_t startTime = 0); 69 | 70 | /** 71 | * Get total number of records 72 | * @return Record count 73 | */ 74 | size_t getRecordCount(); 75 | 76 | /** 77 | * Clear all log data 78 | * @return true if successful 79 | */ 80 | bool clear(); 81 | 82 | /** 83 | * Compact the log file by removing old records 84 | * @param maxAge Maximum age of records to keep (in milliseconds) 85 | * @return true if successful 86 | */ 87 | bool compact(uint64_t maxAge); 88 | 89 | private: 90 | static const size_t BUFFER_SIZE = 4096; 91 | static const size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB 92 | 93 | File logFile; 94 | String logFilePath; 95 | std::mutex mutex; 96 | bool initialized; 97 | 98 | bool openLog(const char* mode); 99 | void closeLog(); 100 | bool writeRecord(const Record& record); 101 | bool readRecord(Record& record); 102 | bool seekToStart(); 103 | bool rotateLog(); 104 | }; -------------------------------------------------------------------------------- /test/test_network_manager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "NetworkManager.h" 3 | #include "mock_wifi.h" // Mock implementation for testing 4 | 5 | // Mock implementations for hardware-dependent functions 6 | static bool mock_wifi_connected = false; 7 | static String mock_ssid; 8 | static String mock_password; 9 | 10 | void setUp(void) { 11 | mock_wifi_connected = false; 12 | mock_ssid = ""; 13 | mock_password = ""; 14 | } 15 | 16 | void tearDown(void) { 17 | } 18 | 19 | // Test network manager initialization 20 | void test_network_manager_init() { 21 | NetworkManager manager; 22 | TEST_ASSERT_FALSE(manager.isConnected()); 23 | TEST_ASSERT_EQUAL_STRING("", manager.getSSID().c_str()); 24 | } 25 | 26 | // Test AP mode functionality 27 | void test_network_manager_ap_mode() { 28 | NetworkManager manager; 29 | manager.begin(); // Should start in AP mode without credentials 30 | 31 | TEST_ASSERT_FALSE(manager.isConnected()); 32 | TEST_ASSERT_TRUE(manager.getIPAddress().startsWith("192.168.4.")); // Default AP IP range 33 | TEST_ASSERT_TRUE(manager.getSSID().startsWith("ESP32_")); // AP SSID format 34 | } 35 | 36 | // Test WiFi connection process 37 | void test_network_manager_wifi_connect() { 38 | NetworkManager manager; 39 | 40 | // Simulate saving valid credentials 41 | manager.saveCredentials("TestSSID", "TestPassword"); 42 | 43 | // Verify credentials were saved 44 | TEST_ASSERT_EQUAL_STRING("TestSSID", mock_ssid.c_str()); 45 | TEST_ASSERT_EQUAL_STRING("TestPassword", mock_password.c_str()); 46 | 47 | // Simulate successful connection 48 | mock_wifi_connected = true; 49 | 50 | // Let the manager process the connection 51 | for (int i = 0; i < 10; i++) { 52 | manager.handleClient(); 53 | delay(100); 54 | } 55 | 56 | TEST_ASSERT_TRUE(manager.isConnected()); 57 | TEST_ASSERT_EQUAL_STRING("TestSSID", manager.getSSID().c_str()); 58 | } 59 | 60 | // Test connection failure and fallback 61 | void test_network_manager_connection_failure() { 62 | NetworkManager manager; 63 | 64 | // Set invalid credentials 65 | manager.saveCredentials("InvalidSSID", "InvalidPass"); 66 | 67 | // Simulate connection failure 68 | mock_wifi_connected = false; 69 | 70 | // Let the manager attempt connections 71 | for (int i = 0; i < 50; i++) { // Give enough time for retries 72 | manager.handleClient(); 73 | delay(100); 74 | } 75 | 76 | // Should be in AP mode after failures 77 | TEST_ASSERT_FALSE(manager.isConnected()); 78 | TEST_ASSERT_TRUE(manager.getIPAddress().startsWith("192.168.4.")); 79 | } 80 | 81 | // Test WebSocket notification system 82 | void test_network_manager_websocket_notifications() { 83 | NetworkManager manager; 84 | MockWebSocketClient client; 85 | 86 | manager.begin(); 87 | 88 | // Connect client 89 | client.connect(); 90 | 91 | // Change network state 92 | manager.saveCredentials("NewSSID", "NewPass"); 93 | mock_wifi_connected = true; 94 | 95 | // Let the manager process 96 | for (int i = 0; i < 5; i++) { 97 | manager.handleClient(); 98 | delay(100); 99 | } 100 | 101 | // Verify client received status updates 102 | TEST_ASSERT_TRUE(client.receivedMessage().contains("connected")); 103 | TEST_ASSERT_TRUE(client.receivedMessage().contains("NewSSID")); 104 | } 105 | 106 | int runUnityTests() { 107 | UNITY_BEGIN(); 108 | 109 | RUN_TEST(test_network_manager_init); 110 | RUN_TEST(test_network_manager_ap_mode); 111 | RUN_TEST(test_network_manager_wifi_connect); 112 | RUN_TEST(test_network_manager_connection_failure); 113 | RUN_TEST(test_network_manager_websocket_notifications); 114 | 115 | return UNITY_END(); 116 | } 117 | 118 | #ifdef ARDUINO 119 | void setup() { 120 | delay(2000); 121 | runUnityTests(); 122 | } 123 | 124 | void loop() { 125 | } 126 | #else 127 | int main() { 128 | return runUnityTests(); 129 | } 130 | #endif -------------------------------------------------------------------------------- /src/MCPServer.cpp: -------------------------------------------------------------------------------- 1 | #include "MCPServer.h" 2 | #include "MCPTypes.h" 3 | 4 | using namespace mcp; 5 | 6 | MCPServer::MCPServer(uint16_t port) : port_(port) {} 7 | 8 | void MCPServer::begin(bool isConnected) { 9 | // Initialization logic here 10 | } 11 | 12 | void MCPServer::handleClient() { 13 | // Handle client logic 14 | } 15 | 16 | void MCPServer::handleInitialize(uint8_t clientId, const RequestId &id, const JsonObject ¶ms) { 17 | JsonDocument doc; 18 | JsonObject result = doc["result"].to(); 19 | result["serverName"] = serverInfo.name; 20 | result["serverVersion"] = serverInfo.version; 21 | 22 | sendResponse(clientId, id, MCPResponse(true, "Initialized", result)); 23 | } 24 | 25 | void MCPServer::handleResourcesList(uint8_t clientId, const RequestId &id, const JsonObject ¶ms) { 26 | JsonDocument doc; 27 | JsonArray resourcesArray = doc["resources"].to(); 28 | 29 | JsonObject resObj = resourcesArray.add(); 30 | resObj["name"] = "Resource1"; 31 | resObj["type"] = "Type1"; 32 | 33 | sendResponse(clientId, id, MCPResponse(true, "Resources Listed", doc.as())); 34 | } 35 | 36 | void MCPServer::handleResourceRead(uint8_t clientId, const RequestId &id, const JsonObject ¶ms) { 37 | if (!params["uri"].is()) { 38 | sendError(clientId, id, 400, "Invalid URI"); 39 | return; 40 | } 41 | 42 | JsonDocument doc; 43 | JsonArray contents = doc["contents"].to(); 44 | JsonObject content = contents.add(); 45 | content["data"] = "Sample Data"; 46 | 47 | sendResponse(clientId, id, MCPResponse(true, "Resource Read", doc.as())); 48 | } 49 | 50 | void MCPServer::handleSubscribe(uint8_t clientId, const RequestId &id, const JsonObject ¶ms) { 51 | if (!params["uri"].is()) { 52 | sendError(clientId, id, 400, "Invalid URI"); 53 | return; 54 | } 55 | 56 | sendResponse(clientId, id, MCPResponse(true, "Subscribed", JsonVariant())); 57 | } 58 | 59 | void MCPServer::handleUnsubscribe(uint8_t clientId, const RequestId &id, const JsonObject ¶ms) { 60 | if (!params["uri"].is()) { 61 | sendError(clientId, id, 400, "Invalid URI"); 62 | return; 63 | } 64 | 65 | sendResponse(clientId, id, MCPResponse(true, "Unsubscribed", JsonVariant())); 66 | } 67 | 68 | void MCPServer::unregisterResource(const std::string &uri) { 69 | JsonDocument doc; 70 | JsonObject resource = doc.to(); 71 | resource["uri"] = uri; 72 | } 73 | 74 | void MCPServer::sendResponse(uint8_t clientId, const RequestId &id, const MCPResponse &response) { 75 | JsonDocument doc; 76 | doc["id"] = id; 77 | doc["success"] = response.success; 78 | doc["message"] = response.message; 79 | doc["data"] = response.data; 80 | 81 | std::string jsonResponse; 82 | serializeJson(doc, jsonResponse); 83 | // Transmit response 84 | } 85 | 86 | void MCPServer::sendError(uint8_t clientId, const RequestId &id, int code, const std::string &message) { 87 | JsonDocument doc; 88 | JsonObject error = doc["error"].to(); 89 | error["code"] = code; 90 | error["message"] = message; 91 | 92 | std::string jsonError; 93 | serializeJson(doc, jsonError); 94 | // Transmit error 95 | } 96 | 97 | void MCPServer::broadcastResourceUpdate(const std::string &uri) { 98 | JsonDocument doc; 99 | JsonObject params = doc["params"].to(); 100 | params["uri"] = uri; 101 | 102 | // Broadcast logic 103 | } 104 | 105 | MCPRequest MCPServer::parseRequest(const std::string &json) { 106 | JsonDocument doc; 107 | deserializeJson(doc, json); 108 | 109 | MCPRequest request; 110 | request.type = MCPRequestType::INITIALIZE; 111 | request.id = doc["id"]; 112 | request.params = doc["params"].as(); 113 | return request; 114 | } 115 | 116 | std::string MCPServer::serializeResponse(const RequestId &id, const MCPResponse &response) { 117 | JsonDocument doc; 118 | doc["id"] = id; 119 | doc["success"] = response.success; 120 | doc["message"] = response.message; 121 | doc["data"] = response.data; 122 | 123 | std::string jsonResponse; 124 | serializeJson(doc, jsonResponse); 125 | return jsonResponse; 126 | } 127 | -------------------------------------------------------------------------------- /ai/README.md: -------------------------------------------------------------------------------- 1 | # AI Development Tools 2 | 3 | This directory contains tools and scripts for AI-assisted development of the ESP32 MCP Server project. 4 | 5 | ## Directory Structure 6 | 7 | ``` 8 | ai/ 9 | ├── README.md # This file 10 | ├── scripts/ # Python scripts for AI automation 11 | │ ├── __init__.py 12 | │ └── ai_dev_helper.py # Main AI development helper script 13 | ├── templates/ # Templates for AI-generated content 14 | │ ├── docs/ 15 | │ ├── tests/ 16 | │ └── pr/ 17 | └── ai-dev # Shell script wrapper for easy usage 18 | ``` 19 | 20 | ## Setup 21 | 22 | 1. Ensure Python 3.7+ is installed 23 | 2. Install dependencies: 24 | ```bash 25 | pip install -r ai/requirements.txt 26 | ``` 27 | 3. Make the wrapper script executable: 28 | ```bash 29 | chmod +x ai/ai-dev 30 | ``` 31 | 4. (Optional) Add to your PATH: 32 | ```bash 33 | export PATH=$PATH:/path/to/project/ai 34 | ``` 35 | 36 | ## Usage 37 | 38 | ### Create New Feature 39 | ```bash 40 | ai-dev feature add-metrics-visualization 41 | ``` 42 | This will: 43 | - Create a feature branch 44 | - Setup documentation structure 45 | - Generate test templates 46 | - Prepare development notes 47 | 48 | ### Generate PR Description 49 | ```bash 50 | ai-dev pr feature/my-feature 51 | ``` 52 | Generates a PR description including: 53 | - Feature overview 54 | - Changes list 55 | - AI usage documentation 56 | - Testing checklist 57 | 58 | ### Generate Tests 59 | ```bash 60 | ai-dev test MetricsSystem 61 | ``` 62 | Creates: 63 | - Test file structure 64 | - Basic test cases 65 | - Setup/teardown methods 66 | 67 | ### Update Documentation 68 | ```bash 69 | ai-dev docs NetworkManager 70 | ``` 71 | Generates/updates: 72 | - API documentation 73 | - Usage examples 74 | - Integration guides 75 | 76 | ### Review Code 77 | ```bash 78 | ai-dev review --pattern "src/*.cpp" 79 | ``` 80 | Performs: 81 | - Code review 82 | - Best practices check 83 | - Improvement suggestions 84 | 85 | ## Configuration 86 | 87 | The tools use `.aidev.json` for configuration: 88 | 89 | ```json 90 | { 91 | "prompts": { 92 | "pr": "PR description template", 93 | "test": "Test generation template", 94 | "docs": "Documentation template" 95 | }, 96 | "history": [ 97 | { 98 | "timestamp": "ISO-8601", 99 | "tool": "tool-name", 100 | "prompt": "prompt-used", 101 | "output": "ai-output" 102 | } 103 | ], 104 | "templates": { 105 | "pr": "PR template", 106 | "feature": "Feature template" 107 | } 108 | } 109 | ``` 110 | 111 | ## Best Practices 112 | 113 | 1. **AI Review** 114 | - Always review AI-generated code 115 | - Test thoroughly 116 | - Document AI usage 117 | 118 | 2. **Iterative Development** 119 | - Use AI for initial implementation 120 | - Review and refine 121 | - Document changes 122 | 123 | 3. **Documentation** 124 | - Keep AI prompts in version control 125 | - Document successful patterns 126 | - Share learning with team 127 | 128 | 4. **Security** 129 | - Never share sensitive data with AI 130 | - Review security implications 131 | - Validate cryptographic code 132 | 133 | ## Examples 134 | 135 | ### Feature Development 136 | ```bash 137 | # Create new feature 138 | ai-dev feature add-metrics-export 139 | 140 | # Generate tests 141 | ai-dev test MetricsExporter 142 | 143 | # Update documentation 144 | ai-dev docs MetricsExporter 145 | 146 | # Review changes 147 | ai-dev review --pattern "src/metrics_exporter.*" 148 | 149 | # Generate PR 150 | ai-dev pr feature/metrics-export 151 | ``` 152 | 153 | ### Documentation Update 154 | ```bash 155 | # Update component docs 156 | ai-dev docs NetworkManager 157 | 158 | # Review changes 159 | ai-dev review --pattern "docs/*.md" 160 | ``` 161 | 162 | ### Test Development 163 | ```bash 164 | # Generate test structure 165 | ai-dev test NewComponent 166 | 167 | # Run tests 168 | pio test -e native 169 | ``` 170 | 171 | ## Troubleshooting 172 | 173 | ### Common Issues 174 | 175 | 1. **Script not found** 176 | ```bash 177 | export PATH=$PATH:/path/to/project/ai 178 | ``` 179 | 180 | 2. **Python dependencies** 181 | ```bash 182 | pip install -r ai/requirements.txt 183 | ``` 184 | 185 | 3. **Permission denied** 186 | ```bash 187 | chmod +x ai/ai-dev 188 | chmod +x ai/scripts/ai_dev_helper.py 189 | ``` 190 | 191 | ### Getting Help 192 | 193 | 1. Check the logs in `.aidev.json` 194 | 2. Review AI interaction history 195 | 3. Check script output for errors 196 | 4. Consult project documentation 197 | 198 | ## Contributing 199 | 200 | 1. Add new AI tools to `scripts/` 201 | 2. Update templates in `templates/` 202 | 3. Document usage in README.md 203 | 4. Share successful prompts -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ESP32 MCP Server 3 | 4 | A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) implementation for ESP32 microcontrollers, providing a WebSocket-based interface for resource discovery and monitoring. 5 | 6 | ## Status: Not Compiling, initial commit as is from the model 7 | 8 | Created with Claude 3.5 Sonet on the commit date (with minor obvioud fixes with automatic formating, etc.) 9 | 10 | ![architecture](docs/img/ESP32MCPServer.png) 11 | 12 | ## Features 13 | 14 | - MCP protocol implementation (v0.1.0) 15 | - WebSocket server for real-time updates 16 | - Resource discovery and monitoring 17 | - WiFi configuration via web interface 18 | - Thread-safe request handling 19 | - Comprehensive test suite 20 | - AsyncWebServer integration 21 | - LittleFS support for configuration storage 22 | 23 | ## Prerequisites 24 | 25 | ### Hardware 26 | 27 | - ESP32 S3 DevKitC-1 board 28 | - USB cable for programming 29 | 30 | ### Software 31 | 32 | - PlatformIO Core (CLI) or PlatformIO IDE 33 | - Python 3.7 or higher 34 | - Git 35 | 36 | ## Architecture 37 | 38 | ```mermaid 39 | flowchart TD 40 | Start[Start Application] --> Setup[Setup] 41 | Setup -->|Initialize Filesystem| InitFS[Initialize LittleFS] 42 | Setup -->|Start Network| StartNetwork[Initialize Network Manager] 43 | Setup -->|Create Tasks| CreateTasks[Create and Assign Tasks] 44 | 45 | subgraph Network 46 | StartNetwork --> APCheck[Check AP or Connect Mode] 47 | APCheck -->|Credentials Exist| Connect[Connect to WiFi] 48 | APCheck -->|No Credentials| StartAP[Start Access Point] 49 | Connect --> NetworkReady[Network Ready] 50 | StartAP --> NetworkReady 51 | end 52 | 53 | subgraph MCP_Server 54 | MCP[Start MCP Server] --> HandleClient[Handle Client Connections] 55 | HandleClient --> HandleRequest[Handle Requests] 56 | HandleRequest -->|WebSocket Events| WebSocket[Handle WebSocket] 57 | HandleRequest -->|HTTP Endpoints| HTTP[Process HTTP Requests] 58 | end 59 | 60 | subgraph Metrics 61 | self[Start Metrics System] --> InitMetrics[Initialize System Metrics] 62 | InitMetrics --> CollectMetrics[Collect Metrics Periodically] 63 | CollectMetrics --> SaveMetrics[Save Metrics to Filesystem] 64 | end 65 | 66 | subgraph Logger 67 | self[Start uLogger] --> LogMetrics[Log Metrics Data] 68 | LogMetrics --> CompactLogs[Compact Logs if Necessary] 69 | CompactLogs -->|Rotates Logs| LogRotation 70 | end 71 | 72 | CreateTasks -->|Network Task| NetworkTask[Run Network Task on Core] 73 | CreateTasks -->|MCP Task| MCPTask[Run MCP Server Task on Core] 74 | NetworkTask --> Network 75 | MCPTask --> MCP_Server 76 | MCP_Server --> Metrics 77 | Metrics --> Logger 78 | 79 | 80 | ``` 81 | 82 | ## Installation 83 | 84 | 1. Clone the repository: 85 | 86 | ```bash 87 | git clone https://github.com/yourusername/esp32-mcp-server.git 88 | cd esp32-mcp-server 89 | ``` 90 | 91 | 2. Install dependencies: 92 | 93 | ```bash 94 | pio pkg install 95 | ``` 96 | 97 | 3. Build and upload the filesystem: 98 | 99 | ```bash 100 | pio run -t uploadfs 101 | ``` 102 | 103 | 4. Build and upload the firmware: 104 | 105 | ```bash 106 | pio run -t upload 107 | ``` 108 | 109 | ## Usage 110 | 111 | ### Initial Setup 112 | 113 | 1. Power on the ESP32. It will create an access point named "ESP32_XXXXXX" 114 | 2. Connect to the access point 115 | 3. Navigate to 116 | 4. Configure your WiFi credentials 117 | 5. The device will connect to your network 118 | 119 | ### MCP Connection 120 | 121 | Connect to the MCP server using WebSocket on port 9000: 122 | 123 | ```javascript 124 | const ws = new WebSocket('ws://YOUR_ESP32_IP:9000'); 125 | 126 | // Initialize connection 127 | ws.send(JSON.stringify({ 128 | jsonrpc: "2.0", 129 | method: "initialize", 130 | id: 1 131 | })); 132 | 133 | // List available resources 134 | ws.send(JSON.stringify({ 135 | jsonrpc: "2.0", 136 | method: "resources/list", 137 | id: 2 138 | })); 139 | ``` 140 | 141 | ## Testing 142 | 143 | Run the test suite: 144 | 145 | ```bash 146 | # Run all tests 147 | pio test -e native 148 | 149 | # Run specific test 150 | pio test -e native -f test_request_queue 151 | 152 | # Run with coverage 153 | pio test -e native --coverage 154 | ``` 155 | 156 | ## Contributing 157 | 158 | 1. Fork the repository 159 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 160 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 161 | 4. Push to the branch (`git push origin feature/amazing-feature`) 162 | 5. Open a Pull Request 163 | 164 | ## License 165 | 166 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 167 | -------------------------------------------------------------------------------- /data/wifi_setup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ESP32 Network Setup 5 | 6 | 67 | 68 | 69 |
70 |

Network Setup

71 |
72 |
73 | 74 | 75 |
76 |
77 | 78 | 79 |
80 | 81 |
82 |
83 |
84 | 85 | 154 | 155 | -------------------------------------------------------------------------------- /test/test_mock_little_fs.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "mock/MockLittleFS.h" 3 | #include 4 | 5 | MockLittleFS MockFS; 6 | 7 | void setUp() { 8 | MockFS.begin(true); 9 | } 10 | 11 | void tearDown() { 12 | MockFS.reset(); 13 | } 14 | 15 | void test_file_write_read() { 16 | const char* testPath = "/test.txt"; 17 | const char* testData = "Hello, World!"; 18 | size_t dataLen = strlen(testData); 19 | 20 | // Write test 21 | MockFile file = MockFS.open(testPath, "w"); 22 | TEST_ASSERT_TRUE(file.isOpen()); 23 | TEST_ASSERT_EQUAL(dataLen, file.write((uint8_t*)testData, dataLen)); 24 | file.close(); 25 | 26 | // Read test 27 | file = MockFS.open(testPath, "r"); 28 | TEST_ASSERT_TRUE(file.isOpen()); 29 | uint8_t buffer[64] = {0}; 30 | TEST_ASSERT_EQUAL(dataLen, file.read(buffer, sizeof(buffer))); 31 | TEST_ASSERT_EQUAL_STRING(testData, (char*)buffer); 32 | file.close(); 33 | } 34 | 35 | void test_file_append() { 36 | const char* testPath = "/append.txt"; 37 | const char* data1 = "First line\n"; 38 | const char* data2 = "Second line"; 39 | 40 | // Write initial data 41 | MockFile file = MockFS.open(testPath, "w"); 42 | file.write((uint8_t*)data1, strlen(data1)); 43 | file.close(); 44 | 45 | // Append data 46 | file = MockFS.open(testPath, "a"); 47 | file.write((uint8_t*)data2, strlen(data2)); 48 | file.close(); 49 | 50 | // Verify combined data 51 | file = MockFS.open(testPath, "r"); 52 | char buffer[64] = {0}; 53 | size_t totalLen = strlen(data1) + strlen(data2); 54 | TEST_ASSERT_EQUAL(totalLen, file.read((uint8_t*)buffer, sizeof(buffer))); 55 | TEST_ASSERT_TRUE(strstr(buffer, data1) != nullptr); 56 | TEST_ASSERT_TRUE(strstr(buffer, data2) != nullptr); 57 | file.close(); 58 | } 59 | 60 | void test_file_seek() { 61 | const char* testPath = "/seek.txt"; 62 | const char* testData = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 63 | 64 | // Write test data 65 | MockFile file = MockFS.open(testPath, "w"); 66 | file.write((uint8_t*)testData, strlen(testData)); 67 | file.close(); 68 | 69 | // Test seeking 70 | file = MockFS.open(testPath, "r"); 71 | TEST_ASSERT_TRUE(file.seek(5)); 72 | TEST_ASSERT_EQUAL('F', file.read()); 73 | 74 | TEST_ASSERT_TRUE(file.seek(10)); 75 | TEST_ASSERT_EQUAL('K', file.read()); 76 | 77 | TEST_ASSERT_FALSE(file.seek(100)); // Beyond file size 78 | file.close(); 79 | } 80 | 81 | void test_file_operations() { 82 | const char* sourcePath = "/source.txt"; 83 | const char* destPath = "/dest.txt"; 84 | const char* testData = "Test data"; 85 | 86 | // Create file 87 | MockFile file = MockFS.open(sourcePath, "w"); 88 | file.write((uint8_t*)testData, strlen(testData)); 89 | file.close(); 90 | 91 | // Test exists 92 | TEST_ASSERT_TRUE(MockFS.exists(sourcePath)); 93 | TEST_ASSERT_FALSE(MockFS.exists(destPath)); 94 | 95 | // Test rename 96 | TEST_ASSERT_TRUE(MockFS.rename(sourcePath, destPath)); 97 | TEST_ASSERT_FALSE(MockFS.exists(sourcePath)); 98 | TEST_ASSERT_TRUE(MockFS.exists(destPath)); 99 | 100 | // Test remove 101 | TEST_ASSERT_TRUE(MockFS.remove(destPath)); 102 | TEST_ASSERT_FALSE(MockFS.exists(destPath)); 103 | } 104 | 105 | void test_directory_operations() { 106 | const char* dirPath = "/testdir"; 107 | 108 | TEST_ASSERT_TRUE(MockFS.mkdir(dirPath)); 109 | TEST_ASSERT_TRUE(MockFS.rmdir(dirPath)); 110 | } 111 | 112 | void test_file_modes() { 113 | const char* testPath = "/modes.txt"; 114 | const char* testData = "Test data"; 115 | uint8_t buffer[64]; 116 | 117 | // Test write-only mode 118 | MockFile file = MockFS.open(testPath, "w"); 119 | TEST_ASSERT_TRUE(file.isOpen()); 120 | TEST_ASSERT_EQUAL(0, file.read(buffer, sizeof(buffer))); // Should fail 121 | TEST_ASSERT_GREATER_THAN(0, file.write((uint8_t*)testData, strlen(testData))); 122 | file.close(); 123 | 124 | // Test read-only mode 125 | file = MockFS.open(testPath, "r"); 126 | TEST_ASSERT_TRUE(file.isOpen()); 127 | TEST_ASSERT_EQUAL(0, file.write((uint8_t*)"New data", 8)); // Should fail 128 | TEST_ASSERT_GREATER_THAN(0, file.read(buffer, sizeof(buffer))); 129 | file.close(); 130 | 131 | // Test read-write mode 132 | file = MockFS.open(testPath, "r+"); 133 | TEST_ASSERT_TRUE(file.isOpen()); 134 | TEST_ASSERT_GREATER_THAN(0, file.read(buffer, sizeof(buffer))); 135 | TEST_ASSERT_GREATER_THAN(0, file.write((uint8_t*)"New data", 8)); 136 | file.close(); 137 | } 138 | 139 | int runUnityTests() { 140 | UNITY_BEGIN(); 141 | 142 | RUN_TEST(test_file_write_read); 143 | RUN_TEST(test_file_append); 144 | RUN_TEST(test_file_seek); 145 | RUN_TEST(test_file_operations); 146 | RUN_TEST(test_directory_operations); 147 | RUN_TEST(test_file_modes); 148 | 149 | return UNITY_END(); 150 | } 151 | 152 | #ifdef ARDUINO 153 | void setup() { 154 | delay(2000); 155 | runUnityTests(); 156 | } 157 | 158 | void loop() { 159 | } 160 | #else 161 | int main() { 162 | return runUnityTests(); 163 | } 164 | #endif -------------------------------------------------------------------------------- /HOWTO.md: -------------------------------------------------------------------------------- 1 | # ESP32 MCP Implementation Guide 2 | 3 | This document provides instructions for building, testing, and using the ESP32 Model Context Protocol (MCP) implementation. 4 | 5 | ## Table of Contents 6 | - [Prerequisites](#prerequisites) 7 | - [Project Structure](#project-structure) 8 | - [Building](#building) 9 | - [Testing](#testing) 10 | - [Development Guide](#development-guide) 11 | - [API Reference](#api-reference) 12 | - [Troubleshooting](#troubleshooting) 13 | 14 | ## Prerequisites 15 | 16 | ### Required Software 17 | - PlatformIO Core (CLI) or PlatformIO IDE 18 | - Python 3.7 or higher 19 | - Git 20 | 21 | ### Required Hardware 22 | - ESP32 S3 DevKitC-1 board 23 | - USB cable for programming 24 | - (Optional) NeoPixel LED strip for LED control functionality 25 | 26 | ### Dependencies 27 | All required libraries are managed through PlatformIO's dependency system: 28 | - FastLED 29 | - ArduinoJson 30 | - LittleFS 31 | - ESPAsyncWebServer 32 | - AsyncTCP 33 | - Unity (for testing) 34 | 35 | ## Project Structure 36 | 37 | ``` 38 | project_root/ 39 | ├── platformio.ini # Project configuration 40 | ├── HOWTO.md # This file 41 | ├── data/ 42 | │ └── wifi_setup.html # Web interface file 43 | ├── include/ 44 | │ ├── NetworkManager.h # Network manager header 45 | │ ├── RequestQueue.h # Thread-safe queue header 46 | │ ├── MCPServer.h # MCP server header 47 | │ └── MCPTypes.h # MCP types header 48 | ├── src/ 49 | │ ├── main.cpp # Main application file 50 | │ ├── NetworkManager.cpp # Network manager implementation 51 | │ └── MCPServer.cpp # MCP server implementation 52 | └── test/ 53 | ├── test_main.cpp 54 | ├── test_request_queue.cpp 55 | ├── test_network_manager.cpp 56 | ├── test_mcp_server.cpp 57 | └── mock/ # Mock implementations for testing 58 | ├── mock_wifi.h 59 | ├── mock_websocket.h 60 | └── mock_littlefs.h 61 | ``` 62 | 63 | ## Building 64 | 65 | ### Initial Setup 66 | 1. Clone the repository: 67 | ```bash 68 | git clone 69 | cd 70 | ``` 71 | 72 | 2. Install project dependencies: 73 | ```bash 74 | pio pkg install 75 | ``` 76 | 77 | 3. Build the filesystem: 78 | ```bash 79 | pio run -t uploadfs 80 | ``` 81 | 82 | ### Building the Project 83 | 1. For development build: 84 | ```bash 85 | pio run 86 | ``` 87 | 88 | 2. For release build: 89 | ```bash 90 | pio run -e release 91 | ``` 92 | 93 | 3. Build and upload to device: 94 | ```bash 95 | pio run -t upload 96 | ``` 97 | 98 | ## Testing 99 | 100 | ### Running Tests 101 | 1. Run all tests: 102 | ```bash 103 | pio test -e native 104 | ``` 105 | 106 | 2. Run specific test file: 107 | ```bash 108 | pio test -e native -f test_request_queue 109 | ``` 110 | 111 | 3. Run tests with verbose output: 112 | ```bash 113 | pio test -e native -v 114 | ``` 115 | 116 | ### Test Coverage 117 | To generate test coverage report: 118 | ```bash 119 | pio test -e native --coverage 120 | ``` 121 | 122 | ## Development Guide 123 | 124 | ### Adding New MCP Resources 125 | 1. Define the resource in MCPTypes.h 126 | 2. Implement the resource handler in MCPServer 127 | 3. Register the resource in main.cpp 128 | 129 | Example: 130 | ```cpp 131 | // In main.cpp 132 | mcp::MCPResource timeResource{ 133 | "system_time", 134 | "system://time", 135 | "application/json", 136 | "System time information" 137 | }; 138 | mcpServer.registerResource(timeResource); 139 | ``` 140 | 141 | ### Network Configuration 142 | The device starts in AP mode if no WiFi credentials are configured: 143 | 1. Connect to the ESP32 AP (SSID format: ESP32_XXXXXX) 144 | 2. Navigate to http://192.168.4.1 145 | 3. Enter WiFi credentials 146 | 4. Device will attempt to connect to the network 147 | 148 | ### MCP Protocol Implementation 149 | The implementation follows the MCP specification with support for: 150 | - Resource discovery 151 | - Resource reading 152 | - Resource updates via WebSocket 153 | - Subscription system 154 | 155 | ## API Reference 156 | 157 | ### NetworkManager 158 | - `begin()`: Initializes network management 159 | - `isConnected()`: Checks WiFi connection status 160 | - `getIPAddress()`: Gets current IP address 161 | - `getSSID()`: Gets current network SSID 162 | 163 | ### MCPServer 164 | - `begin(bool isNetworkConnected)`: Starts MCP server 165 | - `registerResource(const MCPResource& resource)`: Registers new resource 166 | - `unregisterResource(const std::string& uri)`: Removes resource 167 | - `handleClient()`: Processes client requests 168 | 169 | ### RequestQueue 170 | Thread-safe queue implementation for handling requests: 171 | - `push(const T& item)`: Adds item to queue 172 | - `pop(T& item)`: Removes and returns item from queue 173 | - `empty()`: Checks if queue is empty 174 | - `size()`: Returns number of items in queue 175 | 176 | ## Troubleshooting 177 | 178 | ### Common Issues 179 | 1. **Upload Failed** 180 | - Check USB connection 181 | - Verify board selection in platformio.ini 182 | - Try pressing the BOOT button during upload 183 | 184 | 2. **Network Connection Failed** 185 | - Verify WiFi credentials 186 | - Check signal strength 187 | - Ensure network supports ESP32 188 | 189 | 3. **Test Failures** 190 | - Run tests with -v flag for detailed output 191 | - Check mock implementations match real hardware behavior 192 | - Verify test dependencies are installed 193 | 194 | ### Debug Logging 195 | Enable debug logging in platformio.ini: 196 | ```ini 197 | build_flags = 198 | -D CORE_DEBUG_LEVEL=5 199 | ``` 200 | 201 | ### Support 202 | For additional support: 203 | 1. Check issue tracker 204 | 2. Review MCP protocol documentation 205 | 3. Contact development team 206 | 207 | ## License 208 | [License information here] -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ESP32 MCP Server 2 | 3 | We welcome contributions and encourage the use of AI assistants to help with development. This guide outlines how to effectively contribute while leveraging AI tools. 4 | 5 | ## Using AI for Development 6 | 7 | ### Recommended AI Tools 8 | - GitHub Copilot for code completion and suggestions 9 | - ChatGPT/Claude for code review, documentation, and problem-solving 10 | - GPT-4 for complex architectural decisions and refactoring suggestions 11 | 12 | ### Best Practices for AI-Assisted Development 13 | 14 | #### 1. Code Generation 15 | - Use AI to generate initial code implementations 16 | - Always review and test AI-generated code 17 | - Include test cases in your AI prompts 18 | - Ask AI to explain complex parts of generated code 19 | 20 | Example prompt: 21 | ``` 22 | Create a new feature for [component] that does [functionality]. 23 | Include: 24 | - Header and implementation files 25 | - Unit tests 26 | - Documentation 27 | - Integration example 28 | ``` 29 | 30 | #### 2. Documentation 31 | - Use AI to generate and maintain documentation 32 | - Ask AI to explain existing code and create documentation 33 | - Have AI review and improve existing documentation 34 | - Generate examples and usage scenarios 35 | 36 | Example prompt: 37 | ``` 38 | Review this code and generate: 39 | - API documentation 40 | - Usage examples 41 | - Common pitfalls 42 | - Performance considerations 43 | ``` 44 | 45 | #### 3. Code Review 46 | - Ask AI to review your changes before submitting 47 | - Use AI to suggest improvements and optimizations 48 | - Have AI check for common issues and best practices 49 | - Request security review from AI 50 | 51 | Example prompt: 52 | ``` 53 | Review this code change for: 54 | - Potential bugs 55 | - Performance issues 56 | - Security vulnerabilities 57 | - Style guide compliance 58 | ``` 59 | 60 | #### 4. Testing 61 | - Generate test cases using AI 62 | - Ask AI to identify edge cases 63 | - Use AI to create mock objects and test fixtures 64 | - Have AI suggest testing strategies 65 | 66 | Example prompt: 67 | ``` 68 | Generate unit tests for this class covering: 69 | - Normal operation 70 | - Edge cases 71 | - Error conditions 72 | - Performance benchmarks 73 | ``` 74 | 75 | ### AI Usage Guidelines 76 | 77 | 1. **Always Review Output** 78 | - Verify AI-generated code works as intended 79 | - Test thoroughly before committing 80 | - Check for security implications 81 | - Ensure generated code follows project style 82 | 83 | 2. **Document AI Usage** 84 | - Comment sections generated by AI 85 | - Include prompt used if relevant 86 | - Explain any modifications made to AI output 87 | - Document review process 88 | 89 | 3. **Iterative Improvement** 90 | - Use AI for multiple iterations 91 | - Ask for explanations of complex parts 92 | - Have AI suggest improvements 93 | - Use AI for refactoring suggestions 94 | 95 | 4. **Security Considerations** 96 | - Never share sensitive information with AI 97 | - Review AI-generated code for security issues 98 | - Use AI to audit security concerns 99 | - Validate cryptographic implementations 100 | 101 | ## Contribution Process 102 | 103 | 1. **Prepare Changes** 104 | ```bash 105 | git checkout -b feature/your-feature 106 | ``` 107 | 108 | 2. **Use AI for Development** 109 | - Generate initial implementation 110 | - Create tests 111 | - Write documentation 112 | - Review changes 113 | 114 | 3. **Local Testing** 115 | ```bash 116 | pio test -e native # Run unit tests 117 | pio run # Build firmware 118 | ``` 119 | 120 | 4. **Submit Changes** 121 | ```bash 122 | git add . 123 | git commit -m "feat: description of changes" 124 | git push origin feature/your-feature 125 | ``` 126 | 127 | 5. **Create Pull Request** 128 | - Use AI to generate PR description 129 | - Include test results 130 | - Document AI usage 131 | - Link related issues 132 | 133 | ## Example AI Workflow 134 | 135 | 1. **Feature Planning** 136 | ``` 137 | Prompt: "Help plan implementation of [feature] including: 138 | - Required components 139 | - API design 140 | - Data structures 141 | - Error handling strategy" 142 | ``` 143 | 144 | 2. **Implementation** 145 | ``` 146 | Prompt: "Generate implementation for [component] based on: 147 | - These requirements 148 | - Our coding style 149 | - Project architecture 150 | - Including tests" 151 | ``` 152 | 153 | 3. **Documentation** 154 | ``` 155 | Prompt: "Create documentation for [component] including: 156 | - API reference 157 | - Usage examples 158 | - Integration guide 159 | - Troubleshooting" 160 | ``` 161 | 162 | 4. **Review** 163 | ``` 164 | Prompt: "Review this implementation for: 165 | - Code quality 166 | - Performance 167 | - Security 168 | - Test coverage" 169 | ``` 170 | 171 | ## Commit Messages 172 | 173 | Use AI to help generate clear commit messages: 174 | 175 | ``` 176 | Prompt: "Generate a commit message for these changes: 177 | [paste diff or description] 178 | Following conventional commits format" 179 | ``` 180 | 181 | Format: 182 | ``` 183 | type(scope): description 184 | 185 | [body] 186 | 187 | [footer] 188 | ``` 189 | 190 | Example: 191 | ``` 192 | feat(metrics): add network latency monitoring 193 | 194 | - Add RTT measurement for WiFi connection 195 | - Implement histogram for latency tracking 196 | - Add metrics endpoint for latency data 197 | - Include documentation and tests 198 | 199 | Closes #123 200 | ``` 201 | 202 | ## Getting Help 203 | 204 | - Use project AI tools for assistance 205 | - Include relevant code/logs in AI prompts 206 | - Share AI suggestions in discussions 207 | - Document successful AI usage patterns 208 | 209 | Remember to: 210 | - Review AI output carefully 211 | - Test thoroughly 212 | - Document clearly 213 | - Share learning -------------------------------------------------------------------------------- /test/test_metrics_system.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "MetricsSystem.h" 3 | #include 4 | 5 | void setUp(void) { 6 | LittleFS.begin(true); 7 | METRICS.begin(); 8 | METRICS.resetBootMetrics(); 9 | } 10 | 11 | void tearDown(void) { 12 | METRICS.end(); 13 | LittleFS.end(); 14 | } 15 | 16 | void test_counter_metrics() { 17 | const char* metric_name = "test.counter"; 18 | METRICS.registerCounter(metric_name, "Test counter"); 19 | 20 | METRICS.incrementCounter(metric_name); 21 | METRICS.incrementCounter(metric_name, 2); 22 | 23 | auto value = METRICS.getMetric(metric_name, true); 24 | TEST_ASSERT_EQUAL(3, value.counter); 25 | 26 | // Test persistence 27 | METRICS.saveMetrics(); 28 | METRICS.resetBootMetrics(); 29 | METRICS.loadMetrics(); 30 | 31 | value = METRICS.getMetric(metric_name, true); 32 | TEST_ASSERT_EQUAL(3, value.counter); 33 | } 34 | 35 | void test_gauge_metrics() { 36 | const char* metric_name = "test.gauge"; 37 | METRICS.registerGauge(metric_name, "Test gauge"); 38 | 39 | METRICS.setGauge(metric_name, 42.5); 40 | auto value = METRICS.getMetric(metric_name, true); 41 | TEST_ASSERT_EQUAL_FLOAT(42.5, value.gauge); 42 | 43 | METRICS.setGauge(metric_name, 50.0); 44 | value = METRICS.getMetric(metric_name, true); 45 | TEST_ASSERT_EQUAL_FLOAT(50.0, value.gauge); 46 | } 47 | 48 | void test_histogram_metrics() { 49 | const char* metric_name = "test.histogram"; 50 | METRICS.registerHistogram(metric_name, "Test histogram"); 51 | 52 | // Record some values 53 | METRICS.recordHistogram(metric_name, 10.0); 54 | METRICS.recordHistogram(metric_name, 20.0); 55 | METRICS.recordHistogram(metric_name, 30.0); 56 | 57 | auto value = METRICS.getMetric(metric_name, true); 58 | TEST_ASSERT_EQUAL(3, value.histogram.count); 59 | TEST_ASSERT_EQUAL_FLOAT(10.0, value.histogram.min); 60 | TEST_ASSERT_EQUAL_FLOAT(30.0, value.histogram.max); 61 | TEST_ASSERT_EQUAL_FLOAT(20.0, value.histogram.value); // mean 62 | TEST_ASSERT_EQUAL_FLOAT(60.0, value.histogram.sum); 63 | } 64 | 65 | void test_metric_history() { 66 | const char* metric_name = "test.history"; 67 | METRICS.registerCounter(metric_name, "Test history"); 68 | 69 | // Add some values over time 70 | for (int i = 0; i < 5; i++) { 71 | METRICS.incrementCounter(metric_name); 72 | delay(100); 73 | } 74 | 75 | auto history = METRICS.getMetricHistory(metric_name, 1); // Last second 76 | TEST_ASSERT_EQUAL(5, history.size()); 77 | 78 | // Test with specific time window 79 | history = METRICS.getMetricHistory(metric_name, 0); // All time 80 | TEST_ASSERT_GREATER_OR_EQUAL(5, history.size()); 81 | } 82 | 83 | void test_system_metrics() { 84 | // Test system metrics registration 85 | METRICS.updateSystemMetrics(); 86 | 87 | auto wifi = METRICS.getMetric("system.wifi.signal", true); 88 | auto heap = METRICS.getMetric("system.heap.free", true); 89 | 90 | TEST_ASSERT_NOT_EQUAL(0, heap.gauge); 91 | 92 | // If WiFi is connected, signal should be negative 93 | if (WiFi.status() == WL_CONNECTED) { 94 | TEST_ASSERT_LESS_THAN(0, wifi.gauge); 95 | } 96 | } 97 | 98 | void test_metric_timer() { 99 | const char* metric_name = "test.timer"; 100 | METRICS.registerHistogram(metric_name, "Test timer"); 101 | 102 | { 103 | MetricTimer timer(metric_name); 104 | delay(100); // Simulate work 105 | } 106 | 107 | auto value = METRICS.getMetric(metric_name, true); 108 | TEST_ASSERT_GREATER_OR_EQUAL(100, value.histogram.value); 109 | TEST_ASSERT_LESS_THAN(150, value.histogram.value); 110 | } 111 | 112 | void test_error_handling() { 113 | // Test invalid metric name 114 | METRICS.incrementCounter("nonexistent"); 115 | METRICS.setGauge("nonexistent", 1.0); 116 | METRICS.recordHistogram("nonexistent", 1.0); 117 | 118 | // Test wrong metric type 119 | const char* counter_name = "test.counter.type"; 120 | const char* gauge_name = "test.gauge.type"; 121 | METRICS.registerCounter(counter_name, "Test counter"); 122 | METRICS.registerGauge(gauge_name, "Test gauge"); 123 | 124 | METRICS.setGauge(counter_name, 1.0); // Should be ignored 125 | METRICS.incrementCounter(gauge_name); // Should be ignored 126 | 127 | auto counter_value = METRICS.getMetric(counter_name, true); 128 | auto gauge_value = METRICS.getMetric(gauge_name, true); 129 | 130 | TEST_ASSERT_EQUAL(0, counter_value.counter); 131 | TEST_ASSERT_EQUAL(0.0, gauge_value.gauge); 132 | } 133 | 134 | void test_concurrent_access() { 135 | // This test simulates concurrent access as much as possible in a single thread 136 | const char* metric_name = "test.concurrent"; 137 | METRICS.registerCounter(metric_name, "Test concurrent"); 138 | 139 | for (int i = 0; i < 1000; i++) { 140 | METRICS.incrementCounter(metric_name); 141 | if (i % 100 == 0) { 142 | METRICS.getMetric(metric_name, true); 143 | METRICS.saveMetrics(); 144 | } 145 | } 146 | 147 | auto value = METRICS.getMetric(metric_name, true); 148 | TEST_ASSERT_EQUAL(1000, value.counter); 149 | } 150 | 151 | int runUnityTests() { 152 | UNITY_BEGIN(); 153 | 154 | RUN_TEST(test_counter_metrics); 155 | RUN_TEST(test_gauge_metrics); 156 | RUN_TEST(test_histogram_metrics); 157 | RUN_TEST(test_metric_history); 158 | RUN_TEST(test_system_metrics); 159 | RUN_TEST(test_metric_timer); 160 | RUN_TEST(test_error_handling); 161 | RUN_TEST(test_concurrent_access); 162 | 163 | return UNITY_END(); 164 | } 165 | 166 | #ifdef ARDUINO 167 | void setup() { 168 | delay(2000); 169 | runUnityTests(); 170 | } 171 | 172 | void loop() { 173 | } 174 | #else 175 | int main() { 176 | return runUnityTests(); 177 | } 178 | #endif -------------------------------------------------------------------------------- /test/test_mcp_server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "MCPServer.h" 3 | #include 4 | #include 5 | #include "mock/mock_websocket.h" 6 | 7 | using namespace mcp; 8 | 9 | MCPServer* server = nullptr; 10 | MockWebSocket* mockWs = nullptr; 11 | 12 | void setUp(void) { 13 | mockWs = new MockWebSocket(); 14 | server = new MCPServer(9000); 15 | server->begin(true); 16 | } 17 | 18 | void tearDown(void) { 19 | delete server; 20 | delete mockWs; 21 | } 22 | 23 | void test_server_initialization() { 24 | // Test basic initialization 25 | TEST_ASSERT_NOT_NULL(server); 26 | 27 | // Verify server capabilities 28 | const char* initRequest = R"({ 29 | "jsonrpc": "2.0", 30 | "method": "initialize", 31 | "id": 1 32 | })"; 33 | 34 | std::string response = mockWs->simulateMessage(1, initRequest); 35 | 36 | // Verify response contains expected fields 37 | TEST_ASSERT_TRUE(response.find("protocolVersion") != std::string::npos); 38 | TEST_ASSERT_TRUE(response.find("serverInfo") != std::string::npos); 39 | TEST_ASSERT_TRUE(response.find("capabilities") != std::string::npos); 40 | } 41 | 42 | void test_resource_registration() { 43 | MCPResource testResource("test", "test://resource", "text/plain", "Test resource"); 44 | server->registerResource(testResource); 45 | 46 | // List resources 47 | const char* listRequest = R"({ 48 | "jsonrpc": "2.0", 49 | "method": "resources/list", 50 | "id": 2 51 | })"; 52 | 53 | std::string response = mockWs->simulateMessage(1, listRequest); 54 | 55 | // Verify test resource is in the list 56 | TEST_ASSERT_TRUE(response.find("test://resource") != std::string::npos); 57 | TEST_ASSERT_TRUE(response.find("Test resource") != std::string::npos); 58 | } 59 | 60 | void test_resource_read() { 61 | MCPResource testResource("test", "test://data", "application/json", "Test data"); 62 | server->registerResource(testResource); 63 | 64 | const char* readRequest = R"({ 65 | "jsonrpc": "2.0", 66 | "method": "resources/read", 67 | "params": {"uri": "test://data"}, 68 | "id": 3 69 | })"; 70 | 71 | std::string response = mockWs->simulateMessage(1, readRequest); 72 | 73 | // Verify response structure 74 | TEST_ASSERT_TRUE(response.find("success") != std::string::npos); 75 | TEST_ASSERT_TRUE(response.find("test://data") != std::string::npos); 76 | } 77 | 78 | void test_resource_subscription() { 79 | MCPResource testResource("test", "test://subscribe", "application/json", "Test subscription"); 80 | server->registerResource(testResource); 81 | 82 | // Subscribe to resource 83 | const char* subscribeRequest = R"({ 84 | "jsonrpc": "2.0", 85 | "method": "resources/subscribe", 86 | "params": {"uri": "test://subscribe"}, 87 | "id": 4 88 | })"; 89 | 90 | std::string response = mockWs->simulateMessage(1, subscribeRequest); 91 | TEST_ASSERT_TRUE(response.find("success") != std::string::npos); 92 | 93 | // Verify notification is sent when resource updates 94 | server->broadcastResourceUpdate("test://subscribe"); 95 | std::string notification = mockWs->getLastNotification(); 96 | TEST_ASSERT_TRUE(notification.find("notifications/resources/updated") != std::string::npos); 97 | } 98 | 99 | void test_error_handling() { 100 | // Test invalid method 101 | const char* invalidRequest = R"({ 102 | "jsonrpc": "2.0", 103 | "method": "invalid_method", 104 | "id": 5 105 | })"; 106 | 107 | std::string response = mockWs->simulateMessage(1, invalidRequest); 108 | TEST_ASSERT_TRUE(response.find("error") != std::string::npos); 109 | TEST_ASSERT_TRUE(response.find("Method not found") != std::string::npos); 110 | 111 | // Test invalid resource URI 112 | const char* invalidResourceRequest = R"({ 113 | "jsonrpc": "2.0", 114 | "method": "resources/read", 115 | "params": {"uri": "invalid://uri"}, 116 | "id": 6 117 | })"; 118 | 119 | response = mockWs->simulateMessage(1, invalidResourceRequest); 120 | TEST_ASSERT_TRUE(response.find("error") != std::string::npos); 121 | TEST_ASSERT_TRUE(response.find("Resource not found") != std::string::npos); 122 | } 123 | 124 | void test_concurrent_clients() { 125 | MCPResource testResource("test", "test://concurrent", "application/json", "Test concurrent access"); 126 | server->registerResource(testResource); 127 | 128 | // Subscribe multiple clients 129 | const char* subscribeRequest = R"({ 130 | "jsonrpc": "2.0", 131 | "method": "resources/subscribe", 132 | "params": {"uri": "test://concurrent"}, 133 | "id": 7 134 | })"; 135 | 136 | std::string response1 = mockWs->simulateMessage(1, subscribeRequest); 137 | std::string response2 = mockWs->simulateMessage(2, subscribeRequest); 138 | 139 | TEST_ASSERT_TRUE(response1.find("success") != std::string::npos); 140 | TEST_ASSERT_TRUE(response2.find("success") != std::string::npos); 141 | 142 | // Verify both clients receive updates 143 | server->broadcastResourceUpdate("test://concurrent"); 144 | std::vector notifications = mockWs->getAllNotifications(); 145 | TEST_ASSERT_EQUAL(2, notifications.size()); 146 | } 147 | 148 | int runUnityTests() { 149 | UNITY_BEGIN(); 150 | 151 | RUN_TEST(test_server_initialization); 152 | RUN_TEST(test_resource_registration); 153 | RUN_TEST(test_resource_read); 154 | RUN_TEST(test_resource_subscription); 155 | RUN_TEST(test_error_handling); 156 | RUN_TEST(test_concurrent_clients); 157 | 158 | return UNITY_END(); 159 | } 160 | 161 | #ifdef ARDUINO 162 | void setup() { 163 | delay(2000); 164 | runUnityTests(); 165 | } 166 | 167 | void loop() { 168 | } 169 | #else 170 | int main() { 171 | return runUnityTests(); 172 | } 173 | #endif -------------------------------------------------------------------------------- /test/mock/MockLittleFS.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // Mock File class 11 | class MockFile { 12 | public: 13 | MockFile() : position(0), mode(""), open(false) {} 14 | 15 | bool isOpen() const { return open; } 16 | 17 | size_t write(const uint8_t* buf, size_t size) { 18 | if (!open || (mode != "w" && mode != "w+" && mode != "a+" && mode != "r+")) { 19 | return 0; 20 | } 21 | 22 | if (mode == "a+" || mode == "a") { 23 | position = data.size(); 24 | } 25 | 26 | if (position + size > data.size()) { 27 | data.resize(position + size); 28 | } 29 | 30 | std::copy(buf, buf + size, data.begin() + position); 31 | position += size; 32 | return size; 33 | } 34 | 35 | size_t write(uint8_t b) { 36 | return write(&b, 1); 37 | } 38 | 39 | size_t read(uint8_t* buf, size_t size) { 40 | if (!open || (mode != "r" && mode != "r+" && mode != "w+" && mode != "a+")) { 41 | return 0; 42 | } 43 | 44 | size_t available_size = std::min(size, data.size() - position); 45 | if (available_size > 0) { 46 | std::copy(data.begin() + position, 47 | data.begin() + position + available_size, 48 | buf); 49 | position += available_size; 50 | } 51 | return available_size; 52 | } 53 | 54 | int read() { 55 | if (!open || position >= data.size()) { 56 | return -1; 57 | } 58 | return data[position++]; 59 | } 60 | 61 | bool seek(size_t pos) { 62 | if (!open || pos > data.size()) { 63 | return false; 64 | } 65 | position = pos; 66 | return true; 67 | } 68 | 69 | size_t position; 70 | size_t size() const { return data.size(); } 71 | bool available() { return position < data.size(); } 72 | void close() { open = false; } 73 | 74 | // For testing 75 | std::vector& getData() { return data; } 76 | const std::string& getMode() const { return mode; } 77 | 78 | private: 79 | friend class MockLittleFS; 80 | std::vector data; 81 | std::string mode; 82 | bool open; 83 | 84 | bool openFile(const char* m) { 85 | mode = m; 86 | open = true; 87 | if (mode == "w" || mode == "w+") { 88 | data.clear(); 89 | position = 0; 90 | } else if (mode == "a" || mode == "a+") { 91 | position = data.size(); 92 | } else { 93 | position = 0; 94 | } 95 | return true; 96 | } 97 | }; 98 | 99 | // Mock LittleFS class 100 | class MockLittleFS { 101 | public: 102 | MockLittleFS() : mounted(false) {} 103 | 104 | bool begin(bool formatOnFail = false) { 105 | mounted = true; 106 | return true; 107 | } 108 | 109 | void end() { 110 | mounted = false; 111 | files.clear(); 112 | } 113 | 114 | MockFile open(const char* path, const char* mode) { 115 | if (!mounted) { 116 | MockFile f; 117 | return f; 118 | } 119 | 120 | MockFile file; 121 | if (strcmp(mode, "r") == 0 || strcmp(mode, "r+") == 0) { 122 | auto it = files.find(path); 123 | if (it == files.end()) { 124 | return file; 125 | } 126 | file = it->second; 127 | } else { 128 | file = files[path]; 129 | } 130 | 131 | file.openFile(mode); 132 | return file; 133 | } 134 | 135 | bool exists(const char* path) { 136 | return files.find(path) != files.end(); 137 | } 138 | 139 | bool remove(const char* path) { 140 | return files.erase(path) > 0; 141 | } 142 | 143 | bool rename(const char* pathFrom, const char* pathTo) { 144 | auto it = files.find(pathFrom); 145 | if (it == files.end()) { 146 | return false; 147 | } 148 | files[pathTo] = it->second; 149 | files.erase(it); 150 | return true; 151 | } 152 | 153 | bool mkdir(const char* path) { 154 | directories.insert(path); 155 | return true; 156 | } 157 | 158 | bool rmdir(const char* path) { 159 | return directories.erase(path) > 0; 160 | } 161 | 162 | // Testing helpers 163 | void reset() { 164 | files.clear(); 165 | directories.clear(); 166 | mounted = false; 167 | } 168 | 169 | size_t fileCount() const { 170 | return files.size(); 171 | } 172 | 173 | bool isMounted() const { 174 | return mounted; 175 | } 176 | 177 | MockFile* getFile(const char* path) { 178 | auto it = files.find(path); 179 | if (it != files.end()) { 180 | return &it->second; 181 | } 182 | return nullptr; 183 | } 184 | 185 | private: 186 | std::map files; 187 | std::set directories; 188 | bool mounted; 189 | }; 190 | 191 | // Global instance for testing 192 | extern MockLittleFS MockFS; 193 | 194 | // Mock FS class for compatibility 195 | class FS { 196 | public: 197 | MockFile open(const char* path, const char* mode) { 198 | return MockFS.open(path, mode); 199 | } 200 | 201 | bool exists(const char* path) { 202 | return MockFS.exists(path); 203 | } 204 | 205 | bool remove(const char* path) { 206 | return MockFS.remove(path); 207 | } 208 | 209 | bool rename(const char* pathFrom, const char* pathTo) { 210 | return MockFS.rename(pathFrom, pathTo); 211 | } 212 | 213 | bool mkdir(const char* path) { 214 | return MockFS.mkdir(path); 215 | } 216 | 217 | bool rmdir(const char* path) { 218 | return MockFS.rmdir(path); 219 | } 220 | }; 221 | 222 | // For test code using LittleFS directly 223 | #define LittleFS MockFS -------------------------------------------------------------------------------- /examples/client/client.ts: -------------------------------------------------------------------------------- 1 | class MCPClient { 2 | constructor(url) { 3 | this.url = url; 4 | this.ws = null; 5 | this.requestId = 1; 6 | this.callbacks = new Map(); 7 | this.subscriptions = new Map(); 8 | this.reconnectAttempts = 0; 9 | this.maxReconnectAttempts = 5; 10 | this.reconnectDelay = 1000; 11 | this.heartbeatInterval = null; 12 | } 13 | 14 | connect() { 15 | return new Promise((resolve, reject) => { 16 | this.ws = new WebSocket(this.url); 17 | 18 | this.ws.onopen = () => { 19 | console.log('Connected to MCP server'); 20 | this.reconnectAttempts = 0; 21 | this.startHeartbeat(); 22 | this.initialize().then(resolve).catch(reject); 23 | }; 24 | 25 | this.ws.onmessage = (event) => { 26 | try { 27 | const message = JSON.parse(event.data); 28 | this.handleMessage(message); 29 | } catch (error) { 30 | console.error('Error parsing message:', error); 31 | } 32 | }; 33 | 34 | this.ws.onclose = () => { 35 | console.log('Connection closed'); 36 | this.stopHeartbeat(); 37 | this.handleReconnect(); 38 | }; 39 | 40 | this.ws.onerror = (error) => { 41 | console.error('WebSocket error:', error); 42 | reject(error); 43 | }; 44 | }); 45 | } 46 | 47 | disconnect() { 48 | if (this.ws) { 49 | this.ws.close(); 50 | this.stopHeartbeat(); 51 | } 52 | } 53 | 54 | async initialize() { 55 | const response = await this.sendRequest('initialize', {}); 56 | console.log('Server initialized:', response); 57 | return response; 58 | } 59 | 60 | async listResources() { 61 | const response = await this.sendRequest('resources/list', {}); 62 | return response.result.data.resources; 63 | } 64 | 65 | async readResource(uri) { 66 | const response = await this.sendRequest('resources/read', { uri }); 67 | return response.result.data; 68 | } 69 | 70 | async subscribe(uri, callback) { 71 | const response = await this.sendRequest('resources/subscribe', { uri }); 72 | if (response.result.success) { 73 | this.subscriptions.set(uri, callback); 74 | } 75 | return response.result.success; 76 | } 77 | 78 | async unsubscribe(uri) { 79 | const response = await this.sendRequest('resources/unsubscribe', { uri }); 80 | if (response.result.success) { 81 | this.subscriptions.delete(uri); 82 | } 83 | return response.result.success; 84 | } 85 | 86 | sendRequest(method, params) { 87 | return new Promise((resolve, reject) => { 88 | if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { 89 | reject(new Error('WebSocket is not connected')); 90 | return; 91 | } 92 | 93 | const id = this.requestId++; 94 | const request = { 95 | jsonrpc: '2.0', 96 | method: method, 97 | params: params, 98 | id: id 99 | }; 100 | 101 | this.callbacks.set(id, { resolve, reject }); 102 | this.ws.send(JSON.stringify(request)); 103 | 104 | // Set timeout for request 105 | setTimeout(() => { 106 | if (this.callbacks.has(id)) { 107 | this.callbacks.delete(id); 108 | reject(new Error('Request timeout')); 109 | } 110 | }, 5000); 111 | }); 112 | } 113 | 114 | handleMessage(message) { 115 | // Handle notifications 116 | if (message.method && message.method.startsWith('notifications/')) { 117 | this.handleNotification(message); 118 | return; 119 | } 120 | 121 | // Handle responses 122 | if (message.id && this.callbacks.has(message.id)) { 123 | const { resolve, reject } = this.callbacks.get(message.id); 124 | this.callbacks.delete(message.id); 125 | 126 | if (message.error) { 127 | reject(new Error(message.error.message)); 128 | } else { 129 | resolve(message); 130 | } 131 | } 132 | } 133 | 134 | handleNotification(message) { 135 | if (message.method === 'notifications/resources/updated') { 136 | const uri = message.params.uri; 137 | const callback = this.subscriptions.get(uri); 138 | if (callback) { 139 | this.readResource(uri).then(callback); 140 | } 141 | } 142 | } 143 | 144 | handleReconnect() { 145 | if (this.reconnectAttempts >= this.maxReconnectAttempts) { 146 | console.error('Max reconnection attempts reached'); 147 | return; 148 | } 149 | 150 | this.reconnectAttempts++; 151 | const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); 152 | 153 | console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); 154 | setTimeout(() => this.connect(), delay); 155 | } 156 | 157 | startHeartbeat() { 158 | this.heartbeatInterval = setInterval(() => { 159 | if (this.ws && this.ws.readyState === WebSocket.OPEN) { 160 | this.ws.send(JSON.stringify({ type: 'ping' })); 161 | } 162 | }, 30000); 163 | } 164 | 165 | stopHeartbeat() { 166 | if (this.heartbeatInterval) { 167 | clearInterval(this.heartbeatInterval); 168 | this.heartbeatInterval = null; 169 | } 170 | } 171 | } 172 | 173 | // Example usage: 174 | async function connectToMCP() { 175 | const client = new MCPClient('ws://your-esp32-ip:9000'); 176 | 177 | try { 178 | // Connect and initialize 179 | await client.connect(); 180 | 181 | // List available resources 182 | const resources = await client.listResources(); 183 | console.log('Available resources:', resources); 184 | 185 | // Subscribe to system info updates 186 | await client.subscribe('system://info', (data) => { 187 | console.log('System info updated:', data); 188 | }); 189 | 190 | // Read current network status 191 | const networkStatus = await client.readResource('system://network'); 192 | console.log('Network status:', networkStatus); 193 | 194 | } catch (error) { 195 | console.error('Error:', error); 196 | } 197 | } -------------------------------------------------------------------------------- /include/MetricsSystem.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "uLogger.h" 10 | 11 | namespace mcp{ 12 | struct MetricValue { 13 | uint64_t timestamp; 14 | union { 15 | int64_t counter; 16 | double gauge; 17 | struct { 18 | double value; // Current/last value 19 | double min; // Minimum value 20 | double max; // Maximum value 21 | double sum; // Sum of all values 22 | uint32_t count; // Number of values 23 | } histogram; 24 | }; 25 | }; 26 | 27 | class MetricsSystem { 28 | public: 29 | // Metric types supported by the system 30 | enum class MetricType { 31 | COUNTER, // Incremental values 32 | GAUGE, // Point-in-time values 33 | HISTOGRAM // Statistical distribution 34 | }; 35 | 36 | // Structure to hold metric values 37 | 38 | // Metadata about a metric 39 | struct MetricInfo { 40 | String name; 41 | MetricType type; 42 | String description; 43 | String unit; // Optional unit of measurement 44 | String category; // Optional grouping category 45 | }; 46 | 47 | // Singleton instance access 48 | static MetricsSystem& getInstance() { 49 | static MetricsSystem instance; 50 | return instance; 51 | } 52 | 53 | /** 54 | * Initialize the metrics system 55 | * @return true if initialization successful 56 | */ 57 | bool begin(); 58 | 59 | /** 60 | * Shutdown the metrics system 61 | */ 62 | void end(); 63 | 64 | /** 65 | * Register a new counter metric 66 | * @param name Unique metric identifier 67 | * @param description Human-readable description 68 | * @param unit Optional unit of measurement 69 | * @param category Optional grouping category 70 | */ 71 | void registerCounter(const String& name, const String& description, 72 | const String& unit = "", const String& category = ""); 73 | 74 | /** 75 | * Register a new gauge metric 76 | * @param name Unique metric identifier 77 | * @param description Human-readable description 78 | * @param unit Optional unit of measurement 79 | * @param category Optional grouping category 80 | */ 81 | void registerGauge(const String& name, const String& description, 82 | const String& unit = "", const String& category = ""); 83 | 84 | /** 85 | * Register a new histogram metric 86 | * @param name Unique metric identifier 87 | * @param description Human-readable description 88 | * @param unit Optional unit of measurement 89 | * @param category Optional grouping category 90 | */ 91 | void registerHistogram(const String& name, const String& description, 92 | const String& unit = "", const String& category = ""); 93 | 94 | /** 95 | * Increment a counter metric 96 | * @param name Metric identifier 97 | * @param value Amount to increment by (default: 1) 98 | */ 99 | void incrementCounter(const String& name, int64_t value = 1); 100 | 101 | /** 102 | * Set a gauge metric value 103 | * @param name Metric identifier 104 | * @param value New gauge value 105 | */ 106 | void setGauge(const String& name, double value); 107 | 108 | /** 109 | * Record a value in a histogram metric 110 | * @param name Metric identifier 111 | * @param value Value to record 112 | */ 113 | void recordHistogram(const String& name, double value); 114 | 115 | /** 116 | * Get current value of a metric 117 | * @param name Metric identifier 118 | * @param fromBoot If true, return value since last boot, otherwise all-time 119 | * @return Current metric value 120 | */ 121 | MetricValue getMetric(const String& name, bool fromBoot = true); 122 | 123 | /** 124 | * Get historical values for a metric 125 | * @param name Metric identifier 126 | * @param seconds Time window in seconds (0 for all time) 127 | * @return Vector of metric values 128 | */ 129 | std::vector getMetricHistory(const String& name, uint32_t seconds = 0); 130 | 131 | /** 132 | * Get information about all registered metrics 133 | * @param category Optional category filter 134 | * @return Map of metric names to their information 135 | */ 136 | std::map getMetrics(const String& category = ""); 137 | 138 | /** 139 | * Update system metrics (called periodically) 140 | */ 141 | void updateSystemMetrics(); 142 | 143 | /** 144 | * Reset boot-time metrics 145 | */ 146 | void resetBootMetrics(); 147 | 148 | /** 149 | * Save current metrics state 150 | * @return true if save successful 151 | */ 152 | bool saveBootMetrics(); 153 | 154 | /** 155 | * Load saved metrics state 156 | * @return true if load successful 157 | */ 158 | bool loadBootMetrics(); 159 | 160 | /** 161 | * Clear all historical metric data 162 | */ 163 | void clearHistory(); 164 | 165 | /** 166 | * Check if metrics system is initialized 167 | * @return true if initialized 168 | */ 169 | bool isInitialized() const; 170 | 171 | private: 172 | MetricsSystem(); 173 | ~MetricsSystem(); 174 | MetricsSystem(const MetricsSystem&) = delete; 175 | MetricsSystem& operator=(const MetricsSystem&) = delete; 176 | 177 | static std::mutex metricsMutex; 178 | bool initialized; 179 | uint32_t lastSaveTime; 180 | 181 | std::map metrics; 182 | std::map bootMetrics; 183 | uLogger logger; 184 | 185 | void initializeSystemMetrics(); 186 | void registerMetric(const String& name, MetricType type, const String& description, 187 | const String& unit = "", const String& category = ""); 188 | MetricValue calculateHistogram(const std::vector& values); 189 | }; 190 | 191 | /** 192 | * Helper class for timing operations and recording them as histogram metrics 193 | */ 194 | class MetricTimer { 195 | public: 196 | /** 197 | * Start timing an operation 198 | * @param metricName Name of histogram metric to record to 199 | */ 200 | MetricTimer(const String& metricName) 201 | : name(metricName), startTime(micros()) {} 202 | 203 | /** 204 | * Stop timing and record duration 205 | */ 206 | ~MetricTimer() { 207 | uint32_t duration = micros() - startTime; 208 | MetricsSystem::getInstance().recordHistogram(name, duration / 1000.0); // Convert to ms 209 | } 210 | 211 | private: 212 | String name; 213 | uint32_t startTime; 214 | }; 215 | 216 | // Macro for timing a scoped operation 217 | #define METRIC_TIMER(name) MetricTimer __timer(name) 218 | } // namespace mcp -------------------------------------------------------------------------------- /src/uLogger.cpp: -------------------------------------------------------------------------------- 1 | #include "uLogger.h" 2 | 3 | uLogger::uLogger() : initialized(false) {} 4 | 5 | uLogger::~uLogger() { 6 | end(); 7 | } 8 | 9 | bool uLogger::begin(const char* logFile) { 10 | std::lock_guard lock(mutex); 11 | 12 | if (initialized) { 13 | return true; 14 | } 15 | 16 | logFilePath = logFile; 17 | 18 | // Try to open existing log file 19 | if (!openLog("r+")) { 20 | // If file doesn't exist, create it 21 | if (!openLog("w+")) { 22 | log_e("Failed to create log file"); 23 | return false; 24 | } 25 | } 26 | 27 | closeLog(); 28 | initialized = true; 29 | return true; 30 | } 31 | 32 | void uLogger::end() { 33 | std::lock_guard lock(mutex); 34 | closeLog(); 35 | initialized = false; 36 | } 37 | 38 | bool uLogger::logMetric(const char* name, const void* data, size_t dataSize) { 39 | std::lock_guard lock(mutex); 40 | 41 | if (!initialized || !name || !data || dataSize > MAX_DATA_LENGTH) { 42 | return false; 43 | } 44 | 45 | Record record; 46 | record.timestamp = millis(); 47 | strncpy(record.name, name, MAX_NAME_LENGTH - 1); 48 | record.dataSize = static_cast(dataSize); 49 | memcpy(record.data, data, dataSize); 50 | 51 | if (!openLog("a+")) { 52 | return false; 53 | } 54 | 55 | bool success = writeRecord(record); 56 | 57 | // Check if we need to rotate the log 58 | if (logFile.size() >= MAX_FILE_SIZE) { 59 | rotateLog(); 60 | } 61 | 62 | closeLog(); 63 | return success; 64 | } 65 | 66 | size_t uLogger::queryMetrics(const char* name, uint64_t startTime, std::vector& records) { 67 | std::lock_guard lock(mutex); 68 | 69 | if (!initialized || !openLog("r")) { 70 | return 0; 71 | } 72 | 73 | size_t count = 0; 74 | Record record; 75 | 76 | while (readRecord(record)) { 77 | if (record.timestamp >= startTime && 78 | (name[0] == '\0' || strcmp(record.name, name) == 0)) { 79 | records.push_back(record); 80 | count++; 81 | } 82 | } 83 | 84 | closeLog(); 85 | return count; 86 | } 87 | 88 | size_t uLogger::queryMetrics(std::function callback, 89 | const char* name, uint64_t startTime) { 90 | std::lock_guard lock(mutex); 91 | 92 | if (!initialized || !openLog("r")) { 93 | return 0; 94 | } 95 | 96 | size_t count = 0; 97 | Record record; 98 | 99 | while (readRecord(record)) { 100 | if (record.timestamp >= startTime && 101 | (name[0] == '\0' || strcmp(record.name, name) == 0)) { 102 | if (!callback(record)) { 103 | break; 104 | } 105 | count++; 106 | } 107 | } 108 | 109 | closeLog(); 110 | return count; 111 | } 112 | 113 | size_t uLogger::getRecordCount() { 114 | std::lock_guard lock(mutex); 115 | 116 | if (!initialized || !openLog("r")) { 117 | return 0; 118 | } 119 | 120 | size_t count = 0; 121 | Record record; 122 | 123 | while (readRecord(record)) { 124 | count++; 125 | } 126 | 127 | closeLog(); 128 | return count; 129 | } 130 | 131 | bool uLogger::clear() { 132 | std::lock_guard lock(mutex); 133 | 134 | closeLog(); 135 | return LittleFS.remove(logFilePath.c_str()); 136 | } 137 | 138 | bool uLogger::compact(uint64_t maxAge) { 139 | std::lock_guard lock(mutex); 140 | 141 | if (!initialized) { 142 | return false; 143 | } 144 | 145 | String tempPath = logFilePath + ".tmp"; 146 | File tempFile = LittleFS.open(tempPath.c_str(), "w+"); 147 | if (!tempFile) { 148 | return false; 149 | } 150 | 151 | if (!openLog("r")) { 152 | tempFile.close(); 153 | LittleFS.remove(tempPath.c_str()); 154 | return false; 155 | } 156 | 157 | uint64_t cutoffTime = millis() - maxAge; 158 | Record record; 159 | size_t count = 0; 160 | 161 | while (readRecord(record)) { 162 | if (record.timestamp >= cutoffTime) { 163 | size_t recordSize = sizeof(record.timestamp) + sizeof(record.dataSize) + 164 | strlen(record.name) + 1 + record.dataSize; 165 | if (tempFile.write((uint8_t*)&record, recordSize) != recordSize) { 166 | closeLog(); 167 | tempFile.close(); 168 | LittleFS.remove(tempPath.c_str()); 169 | return false; 170 | } 171 | count++; 172 | } 173 | } 174 | 175 | closeLog(); 176 | tempFile.close(); 177 | 178 | // Replace old file with new one 179 | LittleFS.remove(logFilePath.c_str()); 180 | LittleFS.rename(tempPath.c_str(), logFilePath.c_str()); 181 | 182 | return true; 183 | } 184 | 185 | bool uLogger::openLog(const char* mode) { 186 | if (logFile) { 187 | return true; 188 | } 189 | 190 | logFile = LittleFS.open(logFilePath.c_str(), mode); 191 | return logFile; 192 | } 193 | 194 | void uLogger::closeLog() { 195 | if (logFile) { 196 | logFile.close(); 197 | } 198 | } 199 | 200 | bool uLogger::writeRecord(const Record& record) { 201 | size_t recordSize = sizeof(record.timestamp) + sizeof(record.dataSize) + 202 | strlen(record.name) + 1 + record.dataSize; 203 | 204 | return logFile.write((uint8_t*)&record, recordSize) == recordSize; 205 | } 206 | 207 | bool uLogger::readRecord(Record& record) { 208 | if (!logFile.available()) { 209 | return false; 210 | } 211 | 212 | // Read timestamp and data size 213 | if (logFile.read((uint8_t*)&record.timestamp, sizeof(record.timestamp)) != sizeof(record.timestamp) || 214 | logFile.read((uint8_t*)&record.dataSize, sizeof(record.dataSize)) != sizeof(record.dataSize)) { 215 | return false; 216 | } 217 | 218 | // Read name 219 | size_t nameLen = 0; 220 | while (nameLen < MAX_NAME_LENGTH - 1) { 221 | char c = logFile.read(); 222 | if (c == '\0') break; 223 | record.name[nameLen++] = c; 224 | } 225 | record.name[nameLen] = '\0'; 226 | 227 | // Read data 228 | if (record.dataSize > MAX_DATA_LENGTH || 229 | logFile.read(record.data, record.dataSize) != record.dataSize) { 230 | return false; 231 | } 232 | 233 | return true; 234 | } 235 | 236 | bool uLogger::seekToStart() { 237 | return logFile.seek(0); 238 | } 239 | 240 | bool uLogger::rotateLog() { 241 | // Create a temporary buffer for the most recent records 242 | std::vector recentRecords; 243 | size_t totalSize = 0; 244 | 245 | // Read records from the end until we reach half the maximum file size 246 | while (totalSize < MAX_FILE_SIZE / 2) { 247 | Record record; 248 | if (!readRecord(record)) { 249 | break; 250 | } 251 | recentRecords.push_back(record); 252 | totalSize += sizeof(record.timestamp) + sizeof(record.dataSize) + 253 | strlen(record.name) + 1 + record.dataSize; 254 | } 255 | 256 | // Clear the file 257 | closeLog(); 258 | if (!clear() || !openLog("w+")) { 259 | return false; 260 | } 261 | 262 | // Write back the recent records 263 | for (const auto& record : recentRecords) { 264 | if (!writeRecord(record)) { 265 | return false; 266 | } 267 | } 268 | 269 | return true; 270 | } -------------------------------------------------------------------------------- /ai/scripts/ai_dev_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | AI Development Helper Scripts 5 | This module provides automation for common AI-assisted development tasks. 6 | """ 7 | 8 | import argparse 9 | import json 10 | import os 11 | import re 12 | import subprocess 13 | import sys 14 | from datetime import datetime 15 | from typing import Dict, List, Optional 16 | 17 | class AIDevHelper: 18 | def __init__(self): 19 | self.config_file = '.aidev.json' 20 | self.load_config() 21 | 22 | def load_config(self): 23 | """Load AI development configuration.""" 24 | if os.path.exists(self.config_file): 25 | with open(self.config_file, 'r') as f: 26 | self.config = json.load(f) 27 | else: 28 | self.config = { 29 | 'prompts': {}, 30 | 'history': [], 31 | 'templates': {} 32 | } 33 | 34 | def save_config(self): 35 | """Save current configuration.""" 36 | with open(self.config_file, 'w') as f: 37 | json.dump(self.config, f, indent=2) 38 | 39 | def generate_pr_description(self, branch_name: str, changes: List[str]) -> str: 40 | """ 41 | Generate a PR description based on changes. 42 | 43 | Args: 44 | branch_name: Name of the feature branch 45 | changes: List of changed files 46 | 47 | Returns: 48 | Generated PR description 49 | """ 50 | template = self.config['templates'].get('pr', """ 51 | # Description 52 | {description} 53 | 54 | ## Changes 55 | {changes} 56 | 57 | ## AI Usage 58 | This PR was developed with AI assistance: 59 | {ai_usage} 60 | 61 | ## Testing 62 | - [ ] Unit tests added/updated 63 | - [ ] Integration tests verified 64 | - [ ] AI review completed 65 | 66 | ## Documentation 67 | - [ ] API documentation updated 68 | - [ ] Examples added/updated 69 | - [ ] AI-assisted documentation review completed 70 | """) 71 | 72 | # TODO: Integrate with actual AI service for better descriptions 73 | description = f"Implementation of {branch_name.replace('feature/', '')}" 74 | changes_list = "\n".join([f"- {change}" for change in changes]) 75 | ai_usage = "- Used AI for implementation and testing\n- AI-assisted code review completed" 76 | 77 | return template.format( 78 | description=description, 79 | changes=changes_list, 80 | ai_usage=ai_usage 81 | ) 82 | 83 | def create_feature_branch(self, feature_name: str): 84 | """Create a new feature branch with AI-assisted setup.""" 85 | branch_name = f"feature/{feature_name}" 86 | subprocess.run(['git', 'checkout', '-b', branch_name]) 87 | 88 | # Create feature development structure 89 | os.makedirs(f'docs/features/{feature_name}', exist_ok=True) 90 | 91 | # Create feature documentation template 92 | with open(f'docs/features/{feature_name}/README.md', 'w') as f: 93 | f.write(f"""# {feature_name} 94 | 95 | ## Overview 96 | [Feature description] 97 | 98 | ## Implementation Details 99 | [Technical details] 100 | 101 | ## AI Development Notes 102 | - [List AI tools used] 103 | - [Key development decisions] 104 | - [AI prompts used] 105 | 106 | ## Testing 107 | - [ ] Unit tests 108 | - [ ] Integration tests 109 | - [ ] AI-assisted testing 110 | 111 | ## Documentation 112 | - [ ] API documentation 113 | - [ ] Usage examples 114 | - [ ] Integration guide 115 | """) 116 | 117 | print(f"Created feature branch and documentation structure for {feature_name}") 118 | 119 | def log_ai_interaction(self, tool: str, prompt: str, output: str): 120 | """Log AI interaction for documentation.""" 121 | interaction = { 122 | 'timestamp': datetime.now().isoformat(), 123 | 'tool': tool, 124 | 'prompt': prompt, 125 | 'output': output 126 | } 127 | self.config['history'].append(interaction) 128 | self.save_config() 129 | 130 | def generate_test_structure(self, component: str): 131 | """Generate test structure for a component.""" 132 | test_template = '''#include 133 | #include "{component}.h" 134 | 135 | void setUp(void) {{ 136 | // Setup code 137 | }} 138 | 139 | void tearDown(void) {{ 140 | // Cleanup code 141 | }} 142 | 143 | void test_{component}_initialization(void) {{ 144 | // Test initialization 145 | }} 146 | 147 | void test_{component}_basic_operation(void) {{ 148 | // Test basic operations 149 | }} 150 | 151 | void test_{component}_error_handling(void) {{ 152 | // Test error conditions 153 | }} 154 | 155 | void test_{component}_edge_cases(void) {{ 156 | // Test edge cases 157 | }} 158 | 159 | int runUnityTests(void) {{ 160 | UNITY_BEGIN(); 161 | RUN_TEST(test_{component}_initialization); 162 | RUN_TEST(test_{component}_basic_operation); 163 | RUN_TEST(test_{component}_error_handling); 164 | RUN_TEST(test_{component}_edge_cases); 165 | return UNITY_END(); 166 | }} 167 | 168 | #ifdef ARDUINO 169 | void setup() {{ 170 | delay(2000); 171 | runUnityTests(); 172 | }} 173 | 174 | void loop() {{ 175 | }} 176 | #else 177 | int main() {{ 178 | return runUnityTests(); 179 | }} 180 | #endif 181 | ''' 182 | 183 | test_file = f'test/test_{component}.cpp' 184 | with open(test_file, 'w') as f: 185 | f.write(test_template.format(component=component)) 186 | 187 | print(f"Generated test structure for {component}") 188 | 189 | def update_documentation(self, component: str): 190 | """Update documentation using AI assistance.""" 191 | # TODO: Integrate with actual AI service 192 | docs_template = '''# {component} Documentation 193 | 194 | ## Overview 195 | [Component description] 196 | 197 | ## API Reference 198 | [API details] 199 | 200 | ## Usage Examples 201 | [Code examples] 202 | 203 | ## Integration Guide 204 | [Integration steps] 205 | 206 | ## Performance Considerations 207 | [Performance notes] 208 | 209 | ## Security Considerations 210 | [Security notes] 211 | 212 | ## Testing 213 | [Testing guide] 214 | 215 | ## AI Development Notes 216 | [AI usage notes] 217 | ''' 218 | 219 | docs_file = f'docs/components/{component}.md' 220 | os.makedirs('docs/components', exist_ok=True) 221 | with open(docs_file, 'w') as f: 222 | f.write(docs_template.format(component=component)) 223 | 224 | print(f"Generated documentation structure for {component}") 225 | 226 | def review_changes(self, file_pattern: Optional[str] = None): 227 | """Run AI-assisted code review.""" 228 | # Get changed files 229 | if file_pattern: 230 | files = subprocess.check_output(['git', 'ls-files', file_pattern]).decode().splitlines() 231 | else: 232 | files = subprocess.check_output(['git', 'diff', '--name-only']).decode().splitlines() 233 | 234 | for file in files: 235 | if not os.path.exists(file): 236 | continue 237 | 238 | print(f"\nReviewing {file}...") 239 | # TODO: Integrate with actual AI service 240 | print("Suggested improvements:") 241 | print("1. [AI suggestions would appear here]") 242 | print("2. [More suggestions]") 243 | 244 | def main(): 245 | parser = argparse.ArgumentParser(description='AI Development Helper') 246 | subparsers = parser.add_subparsers(dest='command', help='Command to run') 247 | 248 | # Feature branch command 249 | feature_parser = subparsers.add_parser('feature', help='Create new feature branch') 250 | feature_parser.add_argument('name', help='Feature name') 251 | 252 | # PR command 253 | pr_parser = subparsers.add_parser('pr', help='Generate PR description') 254 | pr_parser.add_argument('branch', help='Branch name') 255 | 256 | # Test command 257 | test_parser = subparsers.add_parser('test', help='Generate test structure') 258 | test_parser.add_argument('component', help='Component name') 259 | 260 | # Docs command 261 | docs_parser = subparsers.add_parser('docs', help='Update documentation') 262 | docs_parser.add_argument('component', help='Component name') 263 | 264 | # Review command 265 | review_parser = subparsers.add_parser('review', help='Review changes') 266 | review_parser.add_argument('--pattern', help='File pattern to review') 267 | 268 | args = parser.parse_args() 269 | helper = AIDevHelper() 270 | 271 | if args.command == 'feature': 272 | helper.create_feature_branch(args.name) 273 | elif args.command == 'pr': 274 | changes = subprocess.check_output(['git', 'diff', '--name-only']).decode().splitlines() 275 | print(helper.generate_pr_description(args.branch, changes)) 276 | elif args.command == 'test': 277 | helper.generate_test_structure(args.component) 278 | elif args.command == 'docs': 279 | helper.update_documentation(args.component) 280 | elif args.command == 'review': 281 | helper.review_changes(args.pattern) 282 | else: 283 | parser.print_help() 284 | 285 | if __name__ == '__main__': 286 | main() -------------------------------------------------------------------------------- /src/MetricsSystem.cpp: -------------------------------------------------------------------------------- 1 | #include "MetricsSystem.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace mcp; 8 | 9 | // Constants 10 | static const char* BOOT_METRICS_FILE = "/boot_metrics.bin"; 11 | static const char* CONFIG_FILE = "/metrics_config.json"; 12 | static const uint32_t SAVE_INTERVAL = 60000; // 1 minute 13 | static const size_t MAX_METRICS = 50; 14 | 15 | // Static members initialization 16 | std::mutex MetricsSystem::metricsMutex; 17 | 18 | MetricsSystem::MetricsSystem() 19 | : lastSaveTime(0) 20 | , initialized(false) { 21 | } 22 | 23 | MetricsSystem::~MetricsSystem() { 24 | end(); 25 | } 26 | 27 | bool MetricsSystem::begin() { 28 | std::lock_guard lock(metricsMutex); 29 | 30 | if (initialized) { 31 | return true; 32 | } 33 | 34 | if (!LittleFS.begin(true)) { 35 | log_e("Failed to mount filesystem"); 36 | return false; 37 | } 38 | 39 | // Initialize database 40 | if (!logger.begin()) { 41 | log_e("Failed to initialize logger"); 42 | return false; 43 | } 44 | 45 | // Load boot metrics if they exist 46 | if (!loadBootMetrics()) { 47 | resetBootMetrics(); 48 | } 49 | 50 | // Register system metrics 51 | initializeSystemMetrics(); 52 | 53 | initialized = true; 54 | lastSaveTime = millis(); 55 | return true; 56 | } 57 | 58 | void MetricsSystem::end() { 59 | std::lock_guard lock(metricsMutex); 60 | if (initialized) { 61 | saveBootMetrics(); 62 | logger.end(); 63 | initialized = false; 64 | } 65 | } 66 | 67 | void MetricsSystem::registerMetric(const String& name, MetricType type, const String& description, 68 | const String& unit, const String& category) { 69 | std::lock_guard lock(metricsMutex); 70 | 71 | if (metrics.size() >= MAX_METRICS) { 72 | log_w("Max metrics limit reached, ignoring: %s", name.c_str()); 73 | return; 74 | } 75 | 76 | MetricInfo info = {name, type, description, unit, category}; 77 | metrics[name] = info; 78 | 79 | MetricValue value = {millis(), {}}; 80 | switch (type) { 81 | case MetricType::COUNTER: 82 | value.counter = 0; 83 | break; 84 | case MetricType::GAUGE: 85 | value.gauge = 0.0; 86 | break; 87 | case MetricType::HISTOGRAM: 88 | value.histogram = {0.0, 0.0, 0.0, 0.0, 0}; 89 | break; 90 | } 91 | bootMetrics[name] = value; 92 | } 93 | 94 | void MetricsSystem::registerCounter(const String& name, const String& description, 95 | const String& unit, const String& category) { 96 | registerMetric(name, MetricType::COUNTER, description, unit, category); 97 | } 98 | 99 | void MetricsSystem::registerGauge(const String& name, const String& description, 100 | const String& unit, const String& category) { 101 | registerMetric(name, MetricType::GAUGE, description, unit, category); 102 | } 103 | 104 | void MetricsSystem::registerHistogram(const String& name, const String& description, 105 | const String& unit, const String& category) { 106 | registerMetric(name, MetricType::HISTOGRAM, description, unit, category); 107 | } 108 | MetricValue MetricsSystem::getMetric(const String& name, bool fromBoot) { 109 | std::lock_guard lock(metricsMutex); 110 | 111 | auto it = metrics.find(name); 112 | if (it == metrics.end()) { 113 | return MetricValue{}; 114 | } 115 | 116 | if (fromBoot) { 117 | return bootMetrics[name]; 118 | } 119 | 120 | std::vector records; 121 | logger.queryMetrics(name.c_str(), 0, records); // Fetch records 122 | 123 | if (records.empty()) { 124 | return MetricValue{}; 125 | } 126 | 127 | MetricValue result = {millis(), {}}; 128 | switch (it->second.type) { 129 | case MetricType::COUNTER: 130 | result.counter = 0; 131 | for (const auto& record : records) { 132 | int64_t value; 133 | memcpy(&value, record.data, sizeof(value)); 134 | result.counter += value; 135 | } 136 | break; 137 | case MetricType::GAUGE: 138 | memcpy(&result.gauge, records.back().data, sizeof(result.gauge)); // Most recent value 139 | break; 140 | case MetricType::HISTOGRAM: 141 | std::vector histValues; 142 | for (const auto& record : records) { 143 | MetricValue hist; 144 | memcpy(&hist.histogram, record.data, sizeof(hist.histogram)); 145 | histValues.push_back(hist); 146 | } 147 | result = calculateHistogram(histValues); 148 | break; 149 | } 150 | 151 | return result; 152 | } 153 | 154 | 155 | MetricValue MetricsSystem::calculateHistogram(const std::vector& values) { 156 | MetricValue result = {millis(), {.histogram = {0.0, 0.0, 0.0, 0.0, 0}}}; 157 | 158 | if (values.empty()) { 159 | return result; 160 | } 161 | 162 | auto& hist = result.histogram; 163 | hist.min = values[0].histogram.value; 164 | hist.max = values[0].histogram.value; 165 | hist.sum = 0; 166 | hist.count = values.size(); 167 | 168 | for (const auto& v : values) { 169 | hist.min = std::min(hist.min, v.histogram.value); 170 | hist.max = std::max(hist.max, v.histogram.value); 171 | hist.sum += v.histogram.value; 172 | } 173 | 174 | hist.value = hist.sum / hist.count; 175 | return result; 176 | } 177 | 178 | void MetricsSystem::updateSystemMetrics() { 179 | std::lock_guard lock(metricsMutex); 180 | 181 | // Update WiFi signal strength if connected 182 | if (WiFi.status() == WL_CONNECTED) { 183 | setGauge("system.wifi.signal", WiFi.RSSI()); 184 | } 185 | 186 | // Update heap metrics 187 | setGauge("system.heap.free", ESP.getFreeHeap()); 188 | setGauge("system.heap.min", ESP.getMinFreeHeap()); 189 | setGauge("system.uptime", millis()); 190 | 191 | // Check if it's time to save boot metrics 192 | uint32_t now = millis(); 193 | if (now - lastSaveTime >= SAVE_INTERVAL) { 194 | saveBootMetrics(); 195 | lastSaveTime = now; 196 | } 197 | } 198 | bool MetricsSystem::saveBootMetrics() { 199 | std::lock_guard lock(metricsMutex); 200 | 201 | File file = LittleFS.open(BOOT_METRICS_FILE, "w"); 202 | if (!file) { 203 | log_e("Failed to open boot metrics file for writing"); 204 | return false; 205 | } 206 | 207 | // Use StaticJsonDocument if the size is known at compile time. 208 | JsonDocument doc; 209 | JsonObject root = doc.to(); 210 | 211 | for (const auto& pair : metrics) { 212 | JsonObject metric = root[pair.first].to(); 213 | metric["type"] = static_cast(pair.second.type); 214 | metric["description"] = pair.second.description; 215 | metric["unit"] = pair.second.unit; 216 | metric["category"] = pair.second.category; 217 | } 218 | 219 | if (serializeJson(doc, file) == 0) { 220 | log_e("Failed to write metrics configuration"); 221 | file.close(); 222 | return false; 223 | } 224 | 225 | file.close(); 226 | return true; 227 | } 228 | 229 | bool MetricsSystem::loadBootMetrics() { 230 | std::lock_guard lock(metricsMutex); 231 | 232 | File file = LittleFS.open(BOOT_METRICS_FILE, "r"); 233 | if (!file) { 234 | return false; 235 | } 236 | 237 | // Load metrics configuration 238 | JsonDocument doc; 239 | DeserializationError error = deserializeJson(doc, file); 240 | if (error) { 241 | log_e("Failed to parse metrics configuration"); 242 | file.close(); 243 | return false; 244 | } 245 | 246 | metrics.clear(); 247 | JsonObject root = doc.as(); 248 | for (JsonPair pair : root) { 249 | MetricInfo info; 250 | info.name = pair.key().c_str(); 251 | info.type = static_cast(pair.value()["type"].as()); 252 | info.description = pair.value()["description"].as(); 253 | metrics[info.name] = info; 254 | } 255 | 256 | // Load boot metrics values 257 | if (!file.read((uint8_t*)&bootMetrics, sizeof(bootMetrics))) { 258 | log_e("Failed to read boot metrics values"); 259 | file.close(); 260 | return false; 261 | } 262 | 263 | file.close(); 264 | return true; 265 | } 266 | 267 | void MetricsSystem::resetBootMetrics() { 268 | std::lock_guard lock(metricsMutex); 269 | 270 | bootMetrics.clear(); 271 | for (const auto& pair : metrics) { 272 | MetricValue value = {millis(), {}}; 273 | switch (pair.second.type) { 274 | case MetricType::COUNTER: 275 | value.counter = 0; 276 | break; 277 | case MetricType::GAUGE: 278 | value.gauge = 0.0; 279 | break; 280 | case MetricType::HISTOGRAM: 281 | value.histogram = {0.0, 0.0, 0.0, 0.0, 0}; 282 | break; 283 | } 284 | bootMetrics[pair.first] = value; 285 | } 286 | 287 | saveBootMetrics(); 288 | } 289 | 290 | bool MetricsSystem::isInitialized() const { 291 | return initialized; 292 | } 293 | 294 | void MetricsSystem::clearHistory() { 295 | std::lock_guard lock(metricsMutex); 296 | logger.clear(); 297 | resetBootMetrics(); 298 | } -------------------------------------------------------------------------------- /src/NetworkManager.cpp: -------------------------------------------------------------------------------- 1 | #include "NetworkManager.h" 2 | #include 3 | #include 4 | 5 | 6 | NetworkManager::NetworkManager() 7 | : state(NetworkState::INIT), 8 | server(80), 9 | ws("/ws"), 10 | connectAttempts(0), 11 | lastConnectAttempt(0) { 12 | } 13 | 14 | void NetworkManager::begin() { 15 | // Initialize LittleFS if not already initialized 16 | if (!LittleFS.begin(false)) { 17 | Serial.println("LittleFS Mount Failed - Formatting..."); 18 | if (!LittleFS.begin(true)) { 19 | Serial.println("LittleFS Mount Failed Even After Format!"); 20 | return; 21 | } 22 | } 23 | 24 | WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { 25 | if (static_cast(event) == SYSTEM_EVENT_STA_DISCONNECTED) { 26 | if (state == NetworkState::CONNECTED) { 27 | state = NetworkState::CONNECTION_FAILED; 28 | queueRequest(NetworkRequest::Type::CHECK_CONNECTION); 29 | } 30 | } 31 | }); 32 | 33 | // Create network task 34 | xTaskCreatePinnedToCore( 35 | networkTaskCode, 36 | "NetworkTask", 37 | 8192, 38 | this, 39 | 1, 40 | &networkTaskHandle, 41 | 0 42 | ); 43 | 44 | if (loadCredentials()) { 45 | queueRequest(NetworkRequest::Type::CONNECT); 46 | } else { 47 | queueRequest(NetworkRequest::Type::START_AP); 48 | } 49 | 50 | setupWebServer(); 51 | } 52 | 53 | void NetworkManager::setupWebServer() { 54 | ws.onEvent([this](AsyncWebSocket* server, AsyncWebSocketClient* client, 55 | AwsEventType type, void* arg, uint8_t* data, size_t len) { 56 | this->onWebSocketEvent(server, client, type, arg, data, len); 57 | }); 58 | 59 | server.addHandler(&ws); 60 | 61 | server.on("/", HTTP_GET, [this](AsyncWebServerRequest *request) { 62 | this->handleRoot(request); 63 | }); 64 | 65 | server.on("/save", HTTP_POST, [this](AsyncWebServerRequest *request) { 66 | this->handleSave(request); 67 | }); 68 | 69 | server.on("/status", HTTP_GET, [this](AsyncWebServerRequest *request) { 70 | this->handleStatus(request); 71 | }); 72 | 73 | server.begin(); 74 | } 75 | 76 | void NetworkManager::handleRoot(AsyncWebServerRequest *request) { 77 | if (LittleFS.exists(SETUP_PAGE_PATH)) { 78 | request->send(LittleFS, SETUP_PAGE_PATH, "text/html"); 79 | } else { 80 | request->send(500, "text/plain", "Setup page not found in filesystem"); 81 | } 82 | } 83 | 84 | void NetworkManager::handleSave(AsyncWebServerRequest *request) { 85 | if (!request->hasParam("ssid", true) || !request->hasParam("password", true)) { 86 | request->send(400, "text/plain", "Missing parameters"); 87 | return; 88 | } 89 | 90 | String ssid = request->getParam("ssid", true)->value(); 91 | String password = request->getParam("password", true)->value(); 92 | 93 | if (ssid.isEmpty()) { 94 | request->send(400, "text/plain", "SSID cannot be empty"); 95 | return; 96 | } 97 | 98 | saveCredentials(ssid, password); 99 | request->send(200, "text/plain", "Credentials saved"); 100 | } 101 | 102 | void NetworkManager::handleStatus(AsyncWebServerRequest *request) { 103 | AsyncResponseStream *response = request->beginResponseStream("application/json"); 104 | response->print(getNetworkStatusJson(state, getSSID(), getIPAddress())); 105 | request->send(response); 106 | } 107 | 108 | void NetworkManager::onWebSocketEvent(AsyncWebSocket* server, 109 | AsyncWebSocketClient* client, 110 | AwsEventType type, 111 | void* arg, 112 | uint8_t* data, 113 | size_t len) { 114 | switch (type) { 115 | case WS_EVT_CONNECT: 116 | client->text(getNetworkStatusJson(state, getSSID(), getIPAddress())); 117 | break; 118 | case WS_EVT_DISCONNECT: 119 | break; 120 | case WS_EVT_ERROR: 121 | break; 122 | case WS_EVT_DATA: 123 | break; 124 | } 125 | } 126 | 127 | void NetworkManager::networkTaskCode(void* parameter) { 128 | NetworkManager* manager = static_cast(parameter); 129 | manager->networkTask(); 130 | } 131 | 132 | void NetworkManager::networkTask() { 133 | NetworkRequest request; 134 | TickType_t lastCheck = xTaskGetTickCount(); 135 | 136 | while (true) { 137 | // Process any pending requests 138 | if (requestQueue.pop(request)) { 139 | handleRequest(request); 140 | } 141 | 142 | // Periodic connection check 143 | if (state == NetworkState::CONNECTED && 144 | (xTaskGetTickCount() - lastCheck) >= pdMS_TO_TICKS(RECONNECT_INTERVAL)) { 145 | queueRequest(NetworkRequest::Type::CHECK_CONNECTION); 146 | lastCheck = xTaskGetTickCount(); 147 | } 148 | 149 | // Yield to other tasks 150 | vTaskDelay(pdMS_TO_TICKS(100)); 151 | } 152 | } 153 | 154 | void NetworkManager::handleRequest(const NetworkRequest& request) { 155 | switch (request.type) { 156 | case NetworkRequest::Type::CONNECT: 157 | connect(); 158 | break; 159 | case NetworkRequest::Type::START_AP: 160 | startAP(); 161 | break; 162 | case NetworkRequest::Type::CHECK_CONNECTION: 163 | checkConnection(); 164 | break; 165 | } 166 | } 167 | 168 | void NetworkManager::connect() { 169 | if (!credentials.valid || connectAttempts >= MAX_CONNECT_ATTEMPTS) { 170 | startAP(); 171 | return; 172 | } 173 | 174 | state = NetworkState::CONNECTING; 175 | WiFi.mode(WIFI_STA); 176 | WiFi.begin(credentials.ssid.c_str(), credentials.password.c_str()); 177 | 178 | connectAttempts++; 179 | lastConnectAttempt = millis(); 180 | 181 | // Schedule a connection check 182 | queueRequest(NetworkRequest::Type::CHECK_CONNECTION); 183 | } 184 | 185 | void NetworkManager::checkConnection() { 186 | if (state == NetworkState::CONNECTING) { 187 | if (WiFi.status() == WL_CONNECTED) { 188 | state = NetworkState::CONNECTED; 189 | connectAttempts = 0; 190 | ws.textAll(getNetworkStatusJson(state, getSSID(), getIPAddress())); 191 | } else if (millis() - lastConnectAttempt >= CONNECT_TIMEOUT) { 192 | state = NetworkState::CONNECTION_FAILED; 193 | queueRequest(NetworkRequest::Type::CONNECT); 194 | } else { 195 | queueRequest(NetworkRequest::Type::CHECK_CONNECTION); 196 | } 197 | } else if (state == NetworkState::CONNECTED) { 198 | if (WiFi.status() != WL_CONNECTED) { 199 | state = NetworkState::CONNECTION_FAILED; 200 | queueRequest(NetworkRequest::Type::CONNECT); 201 | } 202 | } 203 | } 204 | 205 | void NetworkManager::startAP() { 206 | state = NetworkState::AP_MODE; 207 | WiFi.mode(WIFI_AP); 208 | 209 | if (apSSID.isEmpty()) { 210 | apSSID = generateUniqueSSID(); 211 | } 212 | 213 | WiFi.softAP(apSSID.c_str()); 214 | ws.textAll(getNetworkStatusJson(state, apSSID, WiFi.softAPIP().toString())); 215 | } 216 | 217 | String NetworkManager::generateUniqueSSID() { 218 | uint32_t chipId = (uint32_t)esp_random(); 219 | char ssid[32]; 220 | snprintf(ssid, sizeof(ssid), "ESP32_%08X", chipId); 221 | return String(ssid); 222 | } 223 | 224 | bool NetworkManager::loadCredentials() { 225 | preferences.begin("network", true); 226 | credentials.ssid = preferences.getString("ssid", ""); 227 | credentials.password = preferences.getString("pass", ""); 228 | preferences.end(); 229 | 230 | credentials.valid = !credentials.ssid.isEmpty(); 231 | return credentials.valid; 232 | } 233 | 234 | void NetworkManager::saveCredentials(const String& ssid, const String& password) { 235 | preferences.begin("network", false); 236 | preferences.putString("ssid", ssid); 237 | preferences.putString("pass", password); 238 | preferences.end(); 239 | 240 | credentials.ssid = ssid; 241 | credentials.password = password; 242 | credentials.valid = true; 243 | 244 | connectAttempts = 0; 245 | queueRequest(NetworkRequest::Type::CONNECT); 246 | } 247 | 248 | void NetworkManager::clearCredentials() { 249 | preferences.begin("network", false); 250 | preferences.clear(); 251 | preferences.end(); 252 | 253 | credentials.ssid = ""; 254 | credentials.password = ""; 255 | credentials.valid = false; 256 | } 257 | String NetworkManager::getNetworkStatusJson(NetworkState state, const String& ssid, const String& ip) { 258 | JsonDocument doc; // Ensure you include 259 | 260 | switch (state) { 261 | case NetworkState::CONNECTED: 262 | doc["status"] = "connected"; 263 | break; 264 | case NetworkState::CONNECTING: 265 | doc["status"] = "connecting"; 266 | break; 267 | case NetworkState::AP_MODE: 268 | doc["status"] = "ap_mode"; 269 | break; 270 | case NetworkState::CONNECTION_FAILED: 271 | doc["status"] = "connection_failed"; 272 | break; 273 | default: 274 | doc["status"] = "initializing"; 275 | } 276 | 277 | doc["ssid"] = ssid; 278 | doc["ip"] = ip; 279 | 280 | String response; 281 | serializeJson(doc, response); 282 | return response; 283 | } 284 | 285 | 286 | bool NetworkManager::isConnected() { 287 | return state == NetworkState::CONNECTED && WiFi.status() == WL_CONNECTED; 288 | } 289 | 290 | String NetworkManager::getIPAddress() { 291 | return state == NetworkState::AP_MODE ? 292 | WiFi.softAPIP().toString() : 293 | WiFi.localIP().toString(); 294 | } 295 | 296 | String NetworkManager::getSSID() { 297 | return state == NetworkState::AP_MODE ? 298 | apSSID : 299 | credentials.ssid; 300 | } 301 | 302 | void NetworkManager::queueRequest(NetworkRequest::Type type, const String &message) { 303 | if (!requestQueue.push({type, message})) { 304 | Serial.println("Request queue is full!"); 305 | } 306 | } -------------------------------------------------------------------------------- /data/metrics_stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ESP32 MCP Server Statistics 5 | 6 | 126 | 127 | 128 |
129 |
130 | 133 |

System Statistics

134 |
135 | 136 |
137 |
138 | 139 | 140 | 141 |
142 |
143 | 144 |
145 | 146 |
147 |

Request Statistics

148 |
149 | Total Requests 150 | - 151 |
152 |
153 | Error Rate 154 | - 155 |
156 |
157 | Timeout Rate 158 | - 159 |
160 |
161 | Avg Response Time 162 | - 163 |
164 |
165 | Max Response Time 166 | - 167 |
168 |
169 | 170 | 171 |
172 |

System Status

173 |
174 | WiFi Signal 175 |
176 |
177 | - 178 |
179 |
180 |
181 | Free Heap 182 | - 183 |
184 |
185 | Min Free Heap 186 | - 187 |
188 |
189 | Uptime 190 | - 191 |
192 |
193 |
194 |
195 | 196 | 293 | 294 | --------------------------------------------------------------------------------