firmware-base/scripts/generate_rest_server.py

393 lines
12 KiB
Python

#!/usr/bin/env python3
import yaml
import os
import re
import sys
from jinja2 import Template
# Templates for code generation
REST_SERVER_H_TEMPLATE = """
#ifndef REST_SERVER_H
#define REST_SERVER_H
#include <ESPAsyncWebServer.h>
#include <AsyncJson.h>
#include <ArduinoJson.h>
#include <ArduinoLog.h>
#include <ModbusIP_ESP8266.h>
#include "enums.h"
// Forward declarations
class ModbusIP;
/**
* @brief RESTful API server generated from Swagger spec.
* This class implements a RESTful API server that interfaces with the Modbus system.
*/
class RESTServer {
private:
AsyncWebServer *server;
ModbusIP *modbus;
// Handler methods
{% for handler in handlers %}
void {{ handler.name }}Handler(AsyncWebServerRequest *request);
{% endfor %}
public:
/**
* @brief Construct a new RESTServer object
*
* @param port The port to run the server on
* @param _modbus Pointer to the ModbusIP instance
*/
RESTServer(IPAddress ip, int port, ModbusIP *_modbus);
/**
* @brief Destroy the RESTServer object
*/
~RESTServer();
/**
* @brief Run periodically to handle server tasks
*/
void loop();
};
#endif // REST_SERVER_H
"""
REST_SERVER_CPP_TEMPLATE = """
#include "RestServer.h"
#include <Arduino.h>
#include <ArduinoJson.h>
#include <WiFi.h>
#include <ESPmDNS.h>
RESTServer::RESTServer(IPAddress ip, int port, ModbusIP *_modbus) {
server = new AsyncWebServer(port);
modbus = _modbus;
// Set up CORS
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type");
// Register routes
{% for route in routes %}
server->on("{{ route.path }}", {{ route.method }}, [this](AsyncWebServerRequest *request) {
this->{{ route.handler }}Handler(request);
});
{% endfor %}
// Handle OPTIONS requests for CORS
server->onNotFound([](AsyncWebServerRequest *request) {
if (request->method() == HTTP_OPTIONS) {
request->send(200);
} else {
request->send(404, "text/plain", "Not found");
}
});
// Start mDNS responder
if (MDNS.begin("modbus-esp32")) {
Log.verboseln("MDNS responder started. Device can be reached at http://modbus-esp32.local");
}
// Start server
server->begin();
Log.verboseln("HTTP server started on port %d", port);
}
RESTServer::~RESTServer() {
if (server) {
server->end();
delete server;
server = nullptr;
}
}
void RESTServer::loop() {
// Any periodic handling needed
}
{% for handler in handlers %}
void RESTServer::{{ handler.name }}Handler(AsyncWebServerRequest *request) {
{{ handler.code }}
}
{% endfor %}
"""
# Handler template for system info
SYSTEM_INFO_HANDLER = """
// Create JSON response with system info
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
doc["version"] = "1.0.0";
doc["board"] = BOARD_NAME;
doc["uptime"] = millis() / 1000;
doc["timestamp"] = millis();
serializeJson(doc, *response);
request->send(response);
"""
# Handler template for getting coils
GET_COILS_HANDLER = """
int start = 0;
int count = 50;
// Get query parameters
if (request->hasParam("start")) {
start = request->getParam("start")->value().toInt();
}
if (request->hasParam("count")) {
count = request->getParam("count")->value().toInt();
if (count > 100) count = 100; // Limit to prevent large responses
}
// Create JSON response
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
JsonArray coilsArray = doc.createNestedArray("coils");
for (int i = 0; i < count; i++) {
coilsArray.add(modbus->Coil(start + i));
}
serializeJson(doc, *response);
request->send(response);
"""
# Handler template for getting a specific coil
GET_COIL_HANDLER = """
// Get path parameter
String addressStr = request->pathArg(0);
int address = addressStr.toInt();
bool value = modbus->Coil(address);
// Create JSON response
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
doc["address"] = address;
doc["value"] = value;
serializeJson(doc, *response);
request->send(response);
"""
# Handler template for setting a coil
SET_COIL_HANDLER = """
// Get path parameter
String addressStr = request->pathArg(0);
int address = addressStr.toInt();
// Check if we have a valid body
if (request->hasParam("body", true)) {
String body = request->getParam("body", true)->value();
JsonDocument doc;
DeserializationError error = deserializeJson(doc, body);
if (!error && doc.containsKey("value")) {
bool value = doc["value"];
modbus->Coil(address, value);
// Create JSON response
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument responseDoc;
responseDoc["success"] = true;
responseDoc["address"] = address;
responseDoc["value"] = value;
serializeJson(responseDoc, *response);
request->send(response);
} else {
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid JSON or missing value\"}");
}
} else {
request->send(400, "application/json", "{\"success\":false,\"error\":\"Missing body\"}");
}
"""
# Handler template for getting registers
GET_REGISTERS_HANDLER = """
int start = 0;
int count = 10;
// Get query parameters
if (request->hasParam("start")) {
start = request->getParam("start")->value().toInt();
}
if (request->hasParam("count")) {
count = request->getParam("count")->value().toInt();
if (count > 50) count = 50; // Limit to prevent large responses
}
// Create JSON response
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
JsonArray registersArray = doc.createNestedArray("registers");
for (int i = 0; i < count; i++) {
registersArray.add(modbus->Hreg(start + i));
}
serializeJson(doc, *response);
request->send(response);
"""
# Handler template for getting a specific register
GET_REGISTER_HANDLER = """
// Get path parameter
String addressStr = request->pathArg(0);
int address = addressStr.toInt();
int value = modbus->Hreg(address);
// Create JSON response
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
doc["address"] = address;
doc["value"] = value;
serializeJson(doc, *response);
request->send(response);
"""
# Handler template for setting a register
SET_REGISTER_HANDLER = """
// Get path parameter
String addressStr = request->pathArg(0);
int address = addressStr.toInt();
// Check if we have a valid body
if (request->hasParam("body", true)) {
String body = request->getParam("body", true)->value();
JsonDocument doc;
DeserializationError error = deserializeJson(doc, body);
if (!error && doc.containsKey("value")) {
int value = doc["value"];
modbus->Hreg(address, value);
// Create JSON response
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument responseDoc;
responseDoc["success"] = true;
responseDoc["address"] = address;
responseDoc["value"] = value;
serializeJson(responseDoc, *response);
request->send(response);
} else {
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid JSON or missing value\"}");
}
} else {
request->send(400, "application/json", "{\"success\":false,\"error\":\"Missing body\"}");
}
"""
# Handler template for test relays
TEST_RELAYS_HANDLER = """
// Create JSON response
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
doc["success"] = true;
doc["message"] = "Relay test initiated";
// Call the test relays method on PHApp via a callback or direct call
// This would typically be done via a static callback or global instance
// For this example, we'll assume the test is successful without actual implementation
serializeJson(doc, *response);
request->send(response);
"""
def generate_handlers_from_swagger(swagger_file):
with open(swagger_file, 'r') as f:
swagger = yaml.safe_load(f)
handlers = []
routes = []
for path, methods in swagger['paths'].items():
for method, operation in methods.items():
handler_name = operation['operationId']
# Determine the handler code based on the operation ID
handler_code = ""
if handler_name == "getSystemInfo":
handler_code = SYSTEM_INFO_HANDLER
elif handler_name == "getCoils":
handler_code = GET_COILS_HANDLER
elif handler_name == "getCoil":
handler_code = GET_COIL_HANDLER
elif handler_name == "setCoil":
handler_code = SET_COIL_HANDLER
elif handler_name == "getRegisters":
handler_code = GET_REGISTERS_HANDLER
elif handler_name == "getRegister":
handler_code = GET_REGISTER_HANDLER
elif handler_name == "setRegister":
handler_code = SET_REGISTER_HANDLER
elif handler_name == "testRelays":
handler_code = TEST_RELAYS_HANDLER
# Add the handler if we have code for it
if handler_code:
handlers.append({
'name': handler_name,
'code': handler_code
})
# Determine the HTTP method
http_method = "HTTP_GET"
if method == "post":
http_method = "HTTP_POST"
# Format the path for ESP-IDF
esp_path = path.replace("{", "").replace("}", "")
# Add the route
routes.append({
'path': "/api" + esp_path,
'method': http_method,
'handler': handler_name
})
return handlers, routes
def generate_rest_server_files(swagger_file, output_dir):
handlers, routes = generate_handlers_from_swagger(swagger_file)
# Ensure output directory exists
os.makedirs(output_dir, exist_ok=True)
# Generate header file
header_template = Template(REST_SERVER_H_TEMPLATE)
header_content = header_template.render(handlers=handlers)
with open(os.path.join(output_dir, "RestServer.h"), 'w') as f:
f.write(header_content)
# Generate cpp file
cpp_template = Template(REST_SERVER_CPP_TEMPLATE)
cpp_content = cpp_template.render(routes=routes, handlers=handlers)
with open(os.path.join(output_dir, "RestServer.cpp"), 'w') as f:
f.write(cpp_content)
print(f"Generated RESTful server files in {output_dir}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python generate_rest_server.py <swagger_file> [output_dir]")
sys.exit(1)
swagger_file = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else "src"
generate_rest_server_files(swagger_file, output_dir)