- metrics.lua: drop vif-sta0 station + campground-gateway sections (roof is a dumb AP since 2026-06-07); add skyweave_ap_* families — client count, per-client signal/SNR/bitrates/idle/association-time from iwinfo assoclist, noise floor, txpower, channel. Ping targets now chateau_gateway + talos_via_tunnel + internet (DNS). - skyweave-push.sh: PUT instead of POST so departed-client series and retired families don't linger on the pushgateway; skip push when the local metrics fetch fails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skyweave API Framework
1. Overview
This document describes the refactored Skyweave API framework, a lightweight Lua micro-framework designed for the RVLink MV2400 Roof Unit (COMFAST CF-E5 derivative) running a legacy OpenWrt version with Lua 5.1. It replaces the original monolithic skyweave.lua script with a more modular, extensible, and maintainable architecture.
The primary goal of this refactoring is to decouple request routing and handling from the business logic of individual API endpoints, making it easier to add, remove, or modify endpoints.
2. Architecture
The framework is structured across several key directories and files:
- /app/cgi_scripts/skyweave.lua (source): This is the main CGI script. For deployment, it should be placed at
/www/cgi-bin/skyweave.luaand be made executable. It is run by theuhttpdweb server (with Lua CGI support enabled via/etc/config/uhttpd).- Note for Deployment: Ensure
uhttpdis configured to handle.luascripts as CGI, and theluainterpreter is available at#!/usr/bin/lua.
- Note for Deployment: Ensure
- /app/skyweave/ (source): This directory contains all core framework modules. For deployment, this entire directory should be copied to
/usr/lib/lua/skyweave/on the target system.router.lua: The routing engine. It maps incoming requests to the appropriate handler functions.request.lua: Handles parsing of incoming HTTP requests (method, query, headers, body) into arequest_contexttable.response.lua: Provides helper functions for sending standardized JSON responses (success and error).utils.lua: A collection of common utility functions (e.g., command execution, UCI interaction, string parsing).
- /app/skyweave/handlers/: This directory houses the business logic for individual API endpoints. Each
.luafile typically groups related handlers.- Examples:
network.lua,system.lua,wifi.lua.
- Examples:
- /app/lib/: Contains external Lua libraries.
json.lua: The rxi/json.lua library. For deployment, this file (from/app/lib/json.luain the repository) should be copied to/usr/lib/lua/json.luaon the target system.
Lua Module Loading & Execution
The main CGI script (/www/cgi-bin/skyweave.lua after deployment) relies on Lua's standard package.path mechanism to find modules. It uses require calls like require("skyweave.router") and require("json"). This requires:
- The
skyweaveframework modules to be located at/usr/lib/lua/skyweave/. - The
json.lualibrary to be located at/usr/lib/lua/json.lua. - The OpenWrt Lua interpreter's
package.pathto include/usr/lib/lua/?.luaand/usr/lib/lua/?/init.lua(which is typical).
The script does not perform any local package.path manipulation.
3. Key Framework Components
3.1. Router (skyweave/router.lua)
The router is responsible for directing incoming requests to the correct handler function. It maintains a routing table where each route is defined by an HTTP method (e.g., "GET", "POST") and a conceptual path key (e.g., "/network_config").
A key feature for backward compatibility is its ability to translate query parameters from the original API style (e.g., ?get_config=status or ?set_config=wifi) into these internal method/path keys for routing.
The router now also supports dispatching based on the URL path (via the CGI PATH_INFO variable). If a request like /cgi-bin/skyweave.lua/network/config is made, and PATH_INFO is correctly passed by the uhttpd server as /network/config, the router will use this path directly to find a matching route. This PATH_INFO-based routing is prioritized over the query parameter method.
3.2. Request Context (skyweave/request.lua)
Instead of direct CGI variable manipulation, all handler functions receive a single Lua table called request_context. This table contains pre-parsed information about the request:
request.method: The HTTP request method (e.g., "GET", "POST").request.path: The PATH_INFO component of the URL (if provided byuhttpdfrom the web server configuration).request.query: A table of parsed query string parameters.request.headers: A table of HTTP headers.request.raw_body: The raw request body as a string.request.body: If theContent-Typeisapplication/jsonand a JSON library is available, this field contains the parsed JSON body as a Lua table. Otherwise, it'snil.request.body_parse_error: Contains an error message if JSON parsing of the body failed.
3.3. Response Helpers (skyweave/response.lua)
To ensure consistent API output, two main helper functions are provided:
response.json(data_table, http_status_code): Sends a successful JSON response.http_status_codedefaults to 200.response.error(error_message, http_status_code, internal_error_code): Sends a JSON error response.http_status_codedefaults to 500, andinternal_error_codedefaults to -1.
Both functions automatically set the Content-Type: application/json header and the CGI Status: header (e.g., "Status: 200 OK").
4. API Endpoints
This section describes how to add new API endpoints and how they can be accessed.
4.1. Create/Update Handler Function
- Location: Create a new Lua module or open an existing one in the
/app/skyweave/handlers/directory (e.g.,/app/skyweave/handlers/myfeature.lua). - Structure:
-- /app/skyweave/handlers/myfeature.lua local myfeature_handler = {} -- Handler function function myfeature_handler.get_custom_data(request_context) -- Access request data via request_context local item_id = request_context.query.id if not item_id then return { errCode = -100, errMsg = "Item ID is required" } end -- Your business logic here... local data = { id = item_id, value = "Some data for " .. item_id } return data -- This will be sent as a JSON response end return myfeature_handler - Function Signature: The handler function must accept a single argument,
request_context. - Return Value:
- For a successful synchronous response, return a Lua table. This table will be automatically encoded as JSON.
- To indicate an error handled within the function, return a table with
errCodeanderrMsgfields. - To trigger a 204 No Content response, return
nil. - For asynchronous tasks, see Section 5.
4.2. Register Route
- Edit Main Dispatcher: Open
/app/cgi_scripts/skyweave.lua. - Require Handler Module: Add a
requirestatement for your new handler module:local myfeature_handlers = require("skyweave.handlers.myfeature") - Add Route: In the "Register Routes" section, call
router:add_route():The-- Example: Map GET ?get_config=custom_data to myfeature_handler.get_custom_data router:add_route("GET", "/custom_data", myfeature_handlers.get_custom_data)path_key(e.g.,"/custom_data") will be matched if the router derives this key either from aPATH_INFOlike/custom_datain the URL (e.g./www/cgi-bin/skyweave.lua/custom_data), or from a query parameter like?get_config=custom_data(as per the logic indetermine_route_key_from_request).
5. Asynchronous Task Pattern
For operations that may take a long time (e.g., restarting services, applying complex configurations), handlers can initiate an asynchronous task. This prevents the HTTP request from timing out.
To use this pattern, a handler function should return a special Lua table:
function network_handler.set_connect_wifi(request_context)
-- ... (validation and command setup) ...
local uci_commands = "uci set wireless.wwan.ssid='MyNetwork'; uci commit wireless; wifi"
return {
async = true, -- Required: signals an asynchronous task
command = uci_commands, -- Required: the shell command to execute
response = { -- Required: the immediate JSON response
errCode = 0,
errMsg = "WiFi connection command accepted. Will apply in background."
}
}
end
The framework will:
- Immediately send the content of the
responsefield as a JSON response to the client (typically with an HTTP 202 Accepted status). - Execute the
commandstring in the system shell in the background. The execution is wrapped like this:(sleep 2; <your_command_here>) >/dev/null 2>&1 &. Thesleep 2provides a brief buffer before execution.
6. Dependencies
- Lua 5.1
- JSON Library: The framework modules now use
require("json")to load the JSON library. It is assumed thatlua-cjsonis not available or not preferred. For deployment, the file/app/lib/json.lua(which is rxi/json.lua) must be copied to a standard Lua path on the target OpenWrt system and namedjson.lua(e.g., copy/app/lib/json.luato/usr/lib/lua/json.lua).
7. Original Script Analysis Summary
The original skyweave.lua was a monolithic script handling all API logic. This refactoring introduced:
- Decoupled Routing Engine: Clear separation of routing from handler logic.
- Request Abstraction: Unified
request_contextobject. - Standardized Response Helpers: Consistent JSON output.
- Formalized Asynchronous Task Pattern: Standard way for handlers to perform background tasks.
- Modular Code Structure: Improved organization and maintainability.
8. Example Refactored Handler: set_connect_wifi
The set_connect_wifi handler in /app/skyweave/handlers/network.lua demonstrates using the request_context.body for POST data and the asynchronous task return pattern:
-- In /app/skyweave/handlers/network.lua
local utils = require("skyweave.utils")
local network_handler = {}
-- ... (other handlers like get_network_config) ...
function network_handler.set_connect_wifi(request_context)
local body = request_context.body
if not body then
return { errCode = -29, errMsg = "Request body is missing or not valid JSON.", configDone = false }
end
local ssid = body.ssid
-- ... (retrieve other params: bssid, encryption_type, key, device from body) ...
-- ... (validation logic for ssid, encryption_type, key) ...
-- Example: if not ssid or ssid == "" then return { errCode = -30, errMsg = "SSID is required." } end
local commands = {}
local function add_cmd(cmd) table.insert(commands, cmd) end
-- ... (Build UCI commands as strings using add_cmd) ...
-- Example: add_cmd("uci set wireless.wwan.ssid='" .. ssid:gsub("'", "'\\''") .. "'")
add_cmd("uci set wireless.wwan.disabled=0")
add_cmd("uci set wireless.wwan.mode=sta")
-- Add all other necessary UCI commands for setting up Wi-Fi connection
add_cmd("uci commit wireless")
add_cmd("uci commit network")
add_cmd("/etc/init.d/network restartall")
local command_string = table.concat(commands, "; ")
local response_msg = "OK (Configuration accepted. Device will attempt to connect to SSID: " .. ssid .. ")"
-- ... (append BSSID to response_msg if present) ...
return {
async = true,
command = command_string,
response = {
errCode = 0,
errMsg = response_msg,
configDone = true
}
}
end
return network_handler
This example illustrates how to access POST data from request_context.body and how to structure the return for an asynchronous command execution.
4.3. Accessing API Endpoints
API endpoints can be accessed in two ways:
A. Path-Based URLs (Recommended for clarity): This method uses the path part of the URL after the script name.
- Example GET:
curl http://<device_ip>/cgi-bin/skyweave.lua/network_config - Example POST:
curl -X POST -d '{\"ssid\":\"MyNet\"}' http://<device_ip>/cgi-bin/skyweave.lua/connect_wifiThis relies on your uhttpd server correctly setting thePATH_INFOenvironment variable (see Section 9: uhttpd Server Configuration).
B. Query Parameter-Based URLs (Legacy/Alternative):
This method uses query parameters like get_config or set_config.
- Example GET:
curl http://<device_ip>/cgi-bin/skyweave.lua?get_config=network_config - Example POST:
curl -X POST -d '{\"ssid\":\"MyNet\"}' http://<device_ip>/cgi-bin/skyweave.lua?set_config=connect_wifi
9. uhttpd Server Configuration for PATH_INFO
The path-based URL feature (e.g., /cgi-bin/skyweave.lua/network/config) depends on the uhttpd
web server correctly interpreting the part of the URL after the script name as PATH_INFO
and passing it as an environment variable to the skyweave.lua script.
Standard OpenWrt uhttpd configurations for CGI scripts typically support this by default.
Ensure your /etc/config/uhttpd includes:
config uhttpd main
# ... other main settings ...
option cgi_prefix '/cgi-bin'
config uhttpd cgi
list interpreter '.lua=/usr/bin/lua'
# For OpenWrt 21.02+ or uhttpd versions with rfc_cgi_pathinfo support,
# explicitly enabling it can ensure compliance, though often not needed:
# option rfc_cgi_pathinfo '1'
Usually, no special configuration for PATH_INFO itself is required beyond enabling CGI for Lua.
Verifying PATH_INFO:
If you suspect PATH_INFO is not being set, you can temporarily add the following
at the beginning of your /www/cgi-bin/skyweave.lua script (after deployment):
print("Content-Type: text/plain")
print("")
print("DEBUG PATH_INFO: " .. (os.getenv("PATH_INFO") or "PATH_INFO is nil"))
os.exit()
Then, access a URL like http://<device_ip>/cgi-bin/skyweave.lua/test/path. The output
should show /test/path. Remember to remove this debugging code afterwards.