-
-
-
-]],
-
--- board status section on front page
-["@DYNAMIC/board_status.shtml"] = [[
-
-
Firmware
-
GIT Hash
-
Uptime
-
IP
-
Netmask
-
Gateway
-
MCU Temperature
-
-]]
-}
-
-reboot_counter = 0
-
-local ACTION_PAGES = {
- ["/?FWUPDATE"] = function()
- periph:can_printf("Rebooting for firmware update")
- reboot_counter = 50
- end
-}
-
---[[
- builtin javascript library functions
---]]
-JS_LIBRARY = {
- ["dynamic_load"] = [[
- function dynamic_load(div_id, uri, period_ms) {
- var xhr = new XMLHttpRequest();
- xhr.open('GET', uri);
-
- xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0");
- xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT");
- xhr.setRequestHeader("Pragma", "no-cache");
-
- xhr.onload = function () {
- if (xhr.status === 200) {
- var output = document.getElementById(div_id);
- if (uri.endsWith('.shtml') || uri.endsWith('.html')) {
- output.innerHTML = xhr.responseText;
- } else {
- output.textContent = xhr.responseText;
- }
- }
- setTimeout(function() { dynamic_load(div_id,uri, period_ms); }, period_ms);
- }
- xhr.send();
- }
-]]
-}
-
-if not sock_listen:bind("0.0.0.0", WEB_BIND_PORT:get()) then
- periph:can_printf(string.format("WebServer: failed to bind to TCP %u", WEB_BIND_PORT:get()))
- return
-end
-
-if not sock_listen:listen(20) then
- periph:can_printf("WebServer: failed to listen")
- return
-end
-
-function hms_uptime()
- local s = (millis()/1000):toint()
- local min = math.floor(s / 60) % 60
- local hr = math.floor(s / 3600)
- return string.format("%u hours %u minutes %u seconds", hr, min, s%60)
-end
-
---[[
- split string by pattern
---]]
-local function split(str, pattern)
- local ret = {}
- for s in string.gmatch(str, pattern) do
- table.insert(ret, s)
- end
- return ret
-end
-
---[[
- return true if a string ends in the 2nd string
---]]
-local function endswith(str, s)
- local len1 = #str
- local len2 = #s
- return string.sub(str,1+len1-len2,len1) == s
-end
-
---[[
- return true if a string starts with the 2nd string
---]]
-local function startswith(str, s)
- return string.sub(str,1,#s) == s
-end
-
-local debug_count=0
-
-function DEBUG(txt)
- if WEB_DEBUG:get() ~= 0 then
- periph:can_printf(txt .. string.format(" [%u]", debug_count))
- debug_count = debug_count + 1
- end
-end
-
---[[
- return index of element in a table
---]]
-function table_index(t,el)
- for i,v in ipairs(t) do
- if v == el then
- return i
- end
- end
- return nil
-end
-
---[[
- return true if a table contains a given element
---]]
-function table_contains(t,el)
- local i = table_index(t, el)
- return i ~= nil
-end
-
-function is_hidden_dir(path)
- return table_contains(HIDDEN_FOLDERS, path)
-end
-
-local DAYS = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }
-local MONTHS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }
-
-function isdirectory(path)
- local s = fs:stat(path)
- return s and s:is_directory()
-end
-
---[[
- time string for directory listings
---]]
-function file_timestring(path)
- local s = fs:stat(path)
- if not s then
- return ""
- end
- local mtime = s:mtime()
- local year, month, day, hour, min, sec, _ = rtc:clock_s_to_date_fields(mtime)
- if not year then
- return ""
- end
- return string.format("%04u-%02u-%02u %02u:%02u", year, month+1, day, hour, min, sec)
-end
-
---[[
- time string for Last-Modified
---]]
-function file_timestring_http(mtime)
- local year, month, day, hour, min, sec, wday = rtc:clock_s_to_date_fields(mtime)
- if not year then
- return ""
- end
- return string.format("%s, %02u %s %u %02u:%02u:%02u GMT",
- DAYS[wday+1],
- day,
- MONTHS[month+1],
- year,
- hour,
- min,
- sec)
-end
-
---[[
- parse a http time string to a uint32_t seconds timestamp
---]]
-function file_timestring_http_parse(tstring)
- local dayname, day, monthname, year, hour, min, sec =
- string.match(tstring,
- '(%w%w%w), (%d+) (%w%w%w) (%d%d%d%d) (%d%d):(%d%d):(%d%d) GMT')
- if not dayname then
- return nil
- end
- local mon = table_index(MONTHS, monthname)
- return rtc:date_fields_to_clock_s(year, mon-1, day, hour, min, sec)
-end
-
---[[
- return true if path exists and is not a directory
---]]
-function file_exists(path)
- local s = fs:stat(path)
- if not s then
- return false
- end
- return not s:is_directory()
-end
-
---[[
- substitute variables of form {xxx} from a table
- from http://lua-users.org/wiki/StringInterpolation
---]]
-function substitute_vars(s, vars)
- s = (string.gsub(s, "({([^}]+)})",
- function(whole,i)
- return vars[i] or whole
- end))
- return s
-end
-
---[[
- lat or lon as a string, working around limited type in ftoa_engine
---]]
-function latlon_str(ll)
- local ipart = tonumber(string.match(tostring(ll*1.0e-7), '(.*[.]).*'))
- local fpart = math.abs(ll - ipart*10000000)
- return string.format("%d.%u", ipart, fpart, ipart*10000000, ll)
-end
-
---[[
- location string for home page
---]]
-function location_string(loc)
- return substitute_vars([[{lat} {lon} {alt}]],
- { ["lat"] = latlon_str(loc:lat()),
- ["lon"] = latlon_str(loc:lng()),
- ["alt"] = string.format("%.1fm", loc:alt()*1.0e-2) })
-end
-
---[[
- client class for open connections
---]]
-local function Client(_sock, _idx)
- local self = {}
-
- self.closed = false
-
- local sock = _sock
- local idx = _idx
- local have_header = false
- local header = ""
- local header_lines = {}
- local header_vars = {}
- local run = nil
- local protocol = nil
- local file = nil
- local start_time = millis()
- local offset = 0
-
- function self.read_header()
- local s = sock:recv(2048)
- if not s then
- local now = millis()
- if not sock:is_connected() or now - start_time > WEB_TIMEOUT:get()*1000 then
- -- EOF while looking for header
- DEBUG(string.format("%u: EOF", idx))
- self.remove()
- return false
- end
- return false
- end
- if not s or #s == 0 then
- return false
- end
- header = header .. s
- local eoh = string.find(s, '\r\n\r\n')
- if eoh then
- DEBUG(string.format("%u: got header", idx))
- have_header = true
- header_lines = split(header, "[^\r\n]+")
- -- blocking for reply
- sock:set_blocking(true)
- return true
- end
- return false
- end
-
- function self.sendstring(s)
- sock:send(s, #s)
- end
-
- function self.sendline(s)
- self.sendstring(s .. "\r\n")
- end
-
- --[[
- send a string with variable substitution using {varname}
- --]]
- function self.sendstring_vars(s, vars)
- self.sendstring(substitute_vars(s, vars))
- end
-
- function self.send_header(code, codestr, vars)
- self.sendline(string.format("%s %u %s", protocol, code, codestr))
- self.sendline(string.format("Server: %s", SERVER_VERSION))
- for k,v in pairs(vars) do
- self.sendline(string.format("%s: %s", k, v))
- end
- self.sendline("Connection: close")
- self.sendline("")
- end
-
- -- get size of a file
- function self.file_size(fname)
- local s = fs:stat(fname)
- if not s then
- return 0
- end
- local ret = s:size():toint()
- DEBUG(string.format("%u: size of '%s' -> %u", idx, fname, ret))
- return ret
- end
-
-
- --[[
- return full path with .. resolution
- --]]
- function self.full_path(path, name)
- DEBUG(string.format("%u: full_path(%s,%s)", idx, path, name))
- local ret = path
- if path == "/" and startswith(name,"@") then
- return name
- end
- if name == ".." then
- if path == "/" then
- return "/"
- end
- if endswith(path,"/") then
- path = string.sub(path, 1, #path-1)
- end
- local dir, _ = string.match(path, '(.*/)(.*)')
- if not dir then
- return path
- end
- return dir
- end
- if not endswith(ret, "/") then
- ret = ret .. "/"
- end
- ret = ret .. name
- DEBUG(string.format("%u: full_path(%s,%s) -> %s", idx, path, name, ret))
- return ret
- end
-
- function self.directory_list(path)
- sock:set_blocking(true)
- if startswith(path, "/@") then
- path = string.sub(path, 2, #path-1)
- end
- DEBUG(string.format("%u: directory_list(%s)", idx, path))
- local dlist = dirlist(path)
- if not dlist then
- dlist = {}
- end
- if not table_contains(dlist, "..") then
- -- on ChibiOS we don't get ..
- table.insert(dlist, "..")
- end
- if path == "/" then
- for _,v in ipairs(HIDDEN_FOLDERS) do
- table.insert(dlist, v)
- end
- end
-
- table.sort(dlist)
- self.send_header(200, "OK", {["Content-Type"]=CONTENT_TEXT_HTML})
- self.sendline(DOCTYPE)
- self.sendstring_vars([[
-
-
- Index of {path}
-
-
-
Index of {path}
-
-
Name
Last modified
Size
-]], {path=path})
- for _,d in ipairs(dlist) do
- local skip = d == "."
- if not skip then
- local fullpath = self.full_path(path, d)
- local name = d
- local sizestr = "0"
- local stat = fs:stat(fullpath)
- local size = stat and stat:size() or 0
- if is_hidden_dir(fullpath) or (stat and stat:is_directory()) then
- name = name .. "/"
- elseif size >= 100*1000*1000 then
- sizestr = string.format("%uM", (size/(1000*1000)):toint())
- else
- sizestr = tostring(size)
- end
- local modtime = file_timestring(fullpath)
- self.sendstring_vars([[
-]], { name=name, size=sizestr, modtime=modtime })
- end
- end
- self.sendstring([[
-
-
-
-]])
- end
-
- -- send file content
- function self.send_file()
- if not sock:pollout(0) then
- return
- end
- local chunk = WEB_BLOCK_SIZE:get()
- local b = file:read(chunk)
- sock:set_blocking(true)
- if b and #b > 0 then
- local sent = sock:send(b, #b)
- if sent == -1 then
- run = nil
- self.remove()
- return
- end
- if sent < #b then
- file:seek(offset+sent)
- end
- offset = offset + sent
- end
- if not b or #b < chunk then
- -- EOF
- DEBUG(string.format("%u: sent file", idx))
- run = nil
- self.remove()
- return
- end
- end
-
- --[[
- load whole file as a string
- --]]
- function self.load_file()
- local chunk = WEB_BLOCK_SIZE:get()
- local ret = ""
- while true do
- local b = file:read(chunk)
- if not b or #b == 0 then
- break
- end
- ret = ret .. b
- end
- return ret
- end
-
- --[[
- evaluate some lua code and return as a string
- --]]
- function self.evaluate(code)
- local eval_code = "function eval_func()\n" .. code .. "\nend\n"
- local f, errloc, err = load(eval_code, "eval_func", "t", _ENV)
- if not f then
- DEBUG(string.format("load failed: err=%s errloc=%s", err, errloc))
- return nil
- end
- local success, err2 = pcall(f)
- if not success then
- DEBUG(string.format("pcall failed: err=%s", err2))
- return nil
- end
- local ok, s2 = pcall(eval_func)
- eval_func = nil
- if ok then
- return s2
- end
- return nil
- end
-
- --[[
- process a file as a lua CGI
- --]]
- function self.send_cgi()
- sock:set_blocking(true)
- local contents = self.load_file()
- local s = self.evaluate(contents)
- if s then
- self.sendstring(s)
- end
- self.remove()
- end
-
- --[[
- send file content with server side processsing
- files ending in .shtml can have embedded lua lika this:
-
-
-
- Using 'lstr' a return tostring(yourcode) is added to the code
- automatically
- --]]
- function self.send_processed_file(dynamic_page)
- sock:set_blocking(true)
- local contents
- if dynamic_page then
- contents = file
- else
- contents = self.load_file()
- end
- while #contents > 0 do
- local pat1 = "(.-)[<][?]lua[ \n](.-)[?][>](.*)"
- local pat2 = "(.-)[<][?]lstr[ \n](.-)[?][>](.*)"
- local p1, p2, p3 = string.match(contents, pat1)
- if not p1 then
- p1, p2, p3 = string.match(contents, pat2)
- if not p1 then
- break
- end
- p2 = "return tostring(" .. p2 .. ")"
- end
- self.sendstring(p1)
- local s2 = self.evaluate(p2)
- if s2 then
- self.sendstring(s2)
- end
- contents = p3
- end
- self.sendstring(contents)
- self.remove()
- end
-
- -- return a content type
- function self.content_type(path)
- if path == "/" then
- return MIME_TYPES["html"]
- end
- local _, ext = string.match(path, '(.*[.])(.*)')
- ext = string.lower(ext)
- local ret = MIME_TYPES[ext]
- if not ret then
- return CONTENT_OCTET_STREAM
- end
- return ret
- end
-
- -- perform a file download
- function self.file_download(path)
- if startswith(path, "/@") then
- path = string.sub(path, 2, #path)
- end
- DEBUG(string.format("%u: file_download(%s)", idx, path))
- file = DYNAMIC_PAGES[path]
- dynamic_page = file ~= nil
- if not dynamic_page then
- file = io.open(path,"rb")
- if not file then
- DEBUG(string.format("%u: Failed to open '%s'", idx, path))
- return false
- end
- end
- local vars = {["Content-Type"]=self.content_type(path)}
- local cgi_processing = startswith(path, "/cgi-bin/") and endswith(path, ".lua")
- local server_side_processing = endswith(path, ".shtml")
- local stat = fs:stat(path)
- if not startswith(path, "@") and
- not server_side_processing and
- not cgi_processing and stat and
- not dynamic_page then
- local fsize = stat:size()
- local mtime = stat:mtime()
- vars["Content-Length"]= tostring(fsize)
- local modtime = file_timestring_http(mtime)
- if modtime then
- vars["Last-Modified"] = modtime
- end
- local if_modified_since = header_vars['If-Modified-Since']
- if if_modified_since then
- local tsec = file_timestring_http_parse(if_modified_since)
- if tsec and tsec >= mtime then
- DEBUG(string.format("%u: Not modified: %s %s", idx, modtime, if_modified_since))
- self.send_header(304, "Not Modified", vars)
- return true
- end
- end
- end
- self.send_header(200, "OK", vars)
- if server_side_processing or dynamic_page then
- DEBUG(string.format("%u: shtml processing %s", idx, path))
- run = self.send_processed_file(dynamic_page)
- elseif cgi_processing then
- DEBUG(string.format("%u: CGI processing %s", idx, path))
- run = self.send_cgi
- elseif stat and
- WEB_SENDFILE_MIN:get() > 0 and
- stat:size() >= WEB_SENDFILE_MIN:get() and
- sock:sendfile(file) then
- return true
- else
- run = self.send_file
- end
- return true
- end
-
- function self.not_found()
- self.send_header(404, "Not found", {})
- end
-
- function self.moved_permanently(relpath)
- if not startswith(relpath, "/") then
- relpath = "/" .. relpath
- end
- local location = string.format("http://%s%s", header_vars['Host'], relpath)
- DEBUG(string.format("%u: Redirect -> %s", idx, location))
- self.send_header(301, "Moved Permanently", {["Location"]=location})
- end
-
- -- process a single request
- function self.process_request()
- local h1 = header_lines[1]
- if not h1 or #h1 == 0 then
- DEBUG(string.format("%u: empty request", idx))
- return
- end
- local cmd = split(header_lines[1], "%S+")
- if not cmd or #cmd < 3 then
- DEBUG(string.format("bad request: %s", header_lines[1]))
- return
- end
- if cmd[1] ~= "GET" then
- DEBUG(string.format("bad op: %s", cmd[1]))
- return
- end
- protocol = cmd[3]
- if protocol ~= "HTTP/1.0" and protocol ~= "HTTP/1.1" then
- DEBUG(string.format("bad protocol: %s", protocol))
- return
- end
- local path = cmd[2]
- DEBUG(string.format("%u: path='%s'", idx, path))
-
- -- extract header variables
- for i = 2,#header_lines do
- local key, var = string.match(header_lines[i], '(.*): (.*)')
- if key then
- header_vars[key] = var
- end
- end
-
- if ACTION_PAGES[path] ~= nil then
- DEBUG(string.format("Running ACTION %s", path))
- local fn = ACTION_PAGES[path]
- self.send_header(200, "OK", {["Content-Type"]=CONTENT_TEXT_HTML})
- self.sendstring([[
-
-
-
-
-
-]])
- fn()
- return
- end
-
- if DYNAMIC_PAGES[path] ~= nil then
- self.file_download(path)
- return
- end
-
- if path == MNT_PREFIX then
- path = "/"
- end
- if startswith(path, MNT_PREFIX2) then
- path = string.sub(path,#MNT_PREFIX2,#path)
- end
-
- if isdirectory(path) and
- not endswith(path,"/") and
- header_vars['Host'] and
- not is_hidden_dir(path) then
- self.moved_permanently(path .. "/")
- return
- end
-
- if path ~= "/" and endswith(path,"/") then
- path = string.sub(path, 1, #path-1)
- end
-
- if startswith(path,"/@") then
- path = string.sub(path, 2, #path)
- end
-
- -- see if we have an index file
- if isdirectory(path) and file_exists(path .. "/index.html") then
- DEBUG(string.format("%u: found index.html", idx))
- if self.file_download(path .. "/index.html") then
- return
- end
- end
-
- -- see if it is a directory
- if (path == "/" or
- DYNAMIC_PAGES[path] == nil) and
- (endswith(path,"/") or
- isdirectory(path) or
- is_hidden_dir(path)) then
- self.directory_list(path)
- return
- end
-
- -- or a file
- if self.file_download(path) then
- return
- end
- self.not_found(path)
- end
-
- -- update the client
- function self.update()
- if run then
- run()
- return
- end
- if not have_header then
- if not self.read_header() then
- return
- end
- end
- self.process_request()
- if not run then
- -- nothing more to do
- self.remove()
- end
- end
-
- function self.remove()
- DEBUG(string.format("%u: removing client OFFSET=%u", idx, offset))
- if self.closed then
- return
- end
- sock:close()
- self.closed = true
- end
-
- -- return the instance
- return self
-end
-
---[[
- see if any new clients want to connect
---]]
-local function check_new_clients()
- while sock_listen:pollin(0) do
- local sock = sock_listen:accept()
- if not sock then
- return
- end
- -- non-blocking for header read
- sock:set_blocking(false)
- -- find free client slot
- for i = 1, #clients+1 do
- if clients[i] == nil then
- local idx = i
- local client = Client(sock, idx)
- DEBUG(string.format("%u: New client", idx))
- clients[idx] = client
- end
- end
- end
-end
-
---[[
- check for client activity
---]]
-local function check_clients()
- for idx,client in ipairs(clients) do
- if not client.closed then
- client.update()
- end
- if client.closed then
- table.remove(clients,idx)
- end
- end
-end
-
-local function update()
- check_new_clients()
- check_clients()
- if reboot_counter then
- reboot_counter = reboot_counter - 1
- if reboot_counter == 0 then
- periph:can_printf("Rebooting")
- periph:reboot(true)
- end
- end
- return update,5
-end
-
-return update,100
+web.init(printf)
\ No newline at end of file
diff --git a/Tools/ardupilotwaf/boards.py b/Tools/ardupilotwaf/boards.py
index c8d1bbbc42b2e..e1b62ff85bc94 100644
--- a/Tools/ardupilotwaf/boards.py
+++ b/Tools/ardupilotwaf/boards.py
@@ -513,6 +513,9 @@ def configure_env(self, cfg, env):
"-D__AP_LINE__=__LINE__",
]
+ if not cfg.options.disable_networking:
+ env.ROMFS_FILES += [('scripts/modules/web.lua','libraries/AP_Scripting/modules/webserver.lua'),
+ ('scripts/webgui.lua','libraries/AP_Scripting/applets/net_webserver.lua')]
# add files from ROMFS_custom
custom_dir = 'ROMFS_custom'
if os.path.exists(custom_dir):
diff --git a/libraries/AP_HAL_ChibiOS/hwdef/CubeRedPrimary/hwdef.dat b/libraries/AP_HAL_ChibiOS/hwdef/CubeRedPrimary/hwdef.dat
index d6e7dfa8e38aa..04de5c0c48f24 100644
--- a/libraries/AP_HAL_ChibiOS/hwdef/CubeRedPrimary/hwdef.dat
+++ b/libraries/AP_HAL_ChibiOS/hwdef/CubeRedPrimary/hwdef.dat
@@ -288,3 +288,11 @@ define HAL_FORWARD_OTG2_SERIAL 7
define HAL_HAVE_DUAL_USB_CDC 1
define DEFAULT_SERIAL7_PROTOCOL SerialProtocol_MAVLink2
define DEFAULT_SERIAL7_BAUD 2000000
+
+// ROMFS filesystem only
+define AP_FILESYSTEM_ROMFS_ENABLED 1
+
+// allow scripts to add parameters
+define AP_PARAM_DYNAMIC_ENABLED 1
+
+ROMFS_DIRECTORY libraries/AP_Networking/Web
diff --git a/libraries/AP_Scripting/applets/net_webserver.lua b/libraries/AP_Scripting/applets/net_webserver.lua
index e8f7e2ad0fc52..7a759f3d3d5de 100644
--- a/libraries/AP_Scripting/applets/net_webserver.lua
+++ b/libraries/AP_Scripting/applets/net_webserver.lua
@@ -1,974 +1,17 @@
--[[
- example script to test lua socket API
+ example script to test lua webserver API
--]]
----@diagnostic disable: param-type-mismatch
----@diagnostic disable: undefined-field
----@diagnostic disable: need-check-nil
----@diagnostic disable: redundant-parameter
-local MAV_SEVERITY = {EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFO=6, DEBUG=7}
+local web = require("web")
-PARAM_TABLE_KEY = 47
-PARAM_TABLE_PREFIX = "WEB_"
-
--- add a parameter and bind it to a variable
-function bind_add_param(name, idx, default_value)
- assert(param:add_param(PARAM_TABLE_KEY, idx, name, default_value), string.format('could not add param %s', name))
- return Parameter(PARAM_TABLE_PREFIX .. name)
-end
-
--- Setup Parameters
-assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 6), 'net_test: could not add param table')
-
---[[
- // @Param: WEB_ENABLE
- // @DisplayName: enable web server
- // @Description: enable web server
- // @Values: 0:Disabled,1:Enabled
- // @User: Standard
---]]
-local WEB_ENABLE = bind_add_param('ENABLE', 1, 1)
-
---[[
- // @Param: WEB_BIND_PORT
- // @DisplayName: web server TCP port
- // @Description: web server TCP port
- // @Range: 1 65535
- // @User: Standard
---]]
-local WEB_BIND_PORT = bind_add_param('BIND_PORT', 2, 8080)
-
---[[
- // @Param: WEB_DEBUG
- // @DisplayName: web server debugging
- // @Description: web server debugging
- // @Values: 0:Disabled,1:Enabled
- // @User: Advanced
---]]
-local WEB_DEBUG = bind_add_param('DEBUG', 3, 0)
-
---[[
- // @Param: WEB_BLOCK_SIZE
- // @DisplayName: web server block size
- // @Description: web server block size for download
- // @Range: 1 65535
- // @User: Advanced
---]]
-local WEB_BLOCK_SIZE = bind_add_param('BLOCK_SIZE', 4, 10240)
-
---[[
- // @Param: WEB_TIMEOUT
- // @DisplayName: web server timeout
- // @Description: timeout for inactive connections
- // @Units: s
- // @Range: 0.1 60
- // @User: Advanced
---]]
-local WEB_TIMEOUT = bind_add_param('TIMEOUT', 5, 2.0)
-
---[[
- // @Param: WEB_SENDFILE_MIN
- // @DisplayName: web server minimum file size for sendfile
- // @Description: sendfile is an offloading mechanism for faster file download. If this is non-zero and the file is larger than this size then sendfile will be used for file download
- // @Range: 0 10000000
- // @User: Advanced
---]]
-local WEB_SENDFILE_MIN = bind_add_param('SENDFILE_MIN', 6, 100000)
-
-if WEB_ENABLE:get() ~= 1 then
- gcs:send_text(MAV_SEVERITY.INFO, "WebServer: disabled")
- return
-end
-
-local BRD_RTC_TZ_MIN = Parameter("BRD_RTC_TZ_MIN")
-
-gcs:send_text(MAV_SEVERITY.INFO, string.format("WebServer: starting on port %u", WEB_BIND_PORT:get()))
-
-local sock_listen = Socket(0)
-local clients = {}
-
-local DOCTYPE = ""
-local SERVER_VERSION = "net_webserver 1.0"
-local CONTENT_TEXT_HTML = "text/html;charset=UTF-8"
-local CONTENT_OCTET_STREAM = "application/octet-stream"
-
-local HIDDEN_FOLDERS = { "@SYS", "@ROMFS", "@MISSION", "@PARAM" }
-
-local MNT_PREFIX = "/mnt"
-local MNT_PREFIX2 = MNT_PREFIX .. "/"
-
-local MIME_TYPES = {
- ["apj"] = CONTENT_OCTET_STREAM,
- ["dat"] = CONTENT_OCTET_STREAM,
- ["o"] = CONTENT_OCTET_STREAM,
- ["obj"] = CONTENT_OCTET_STREAM,
- ["lua"] = "text/x-lua",
- ["py"] = "text/x-python",
- ["shtml"] = CONTENT_TEXT_HTML,
- ["js"] = "text/javascript",
- -- thanks to https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
- ["aac"] = "audio/aac",
- ["abw"] = "application/x-abiword",
- ["arc"] = "application/x-freearc",
- ["avif"] = "image/avif",
- ["avi"] = "video/x-msvideo",
- ["azw"] = "application/vnd.amazon.ebook",
- ["bin"] = "application/octet-stream",
- ["bmp"] = "image/bmp",
- ["bz"] = "application/x-bzip",
- ["bz2"] = "application/x-bzip2",
- ["cda"] = "application/x-cdf",
- ["csh"] = "application/x-csh",
- ["css"] = "text/css",
- ["csv"] = "text/csv",
- ["doc"] = "application/msword",
- ["docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- ["eot"] = "application/vnd.ms-fontobject",
- ["epub"] = "application/epub+zip",
- ["gz"] = "application/gzip",
- ["gif"] = "image/gif",
- ["htm"] = CONTENT_TEXT_HTML,
- ["html"] = CONTENT_TEXT_HTML,
- ["ico"] = "image/vnd.microsoft.icon",
- ["ics"] = "text/calendar",
- ["jar"] = "application/java-archive",
- ["jpeg"] = "image/jpeg",
- ["json"] = "application/json",
- ["jsonld"] = "application/ld+json",
- ["mid"] = "audio/x-midi",
- ["mjs"] = "text/javascript",
- ["mp3"] = "audio/mpeg",
- ["mp4"] = "video/mp4",
- ["mpeg"] = "video/mpeg",
- ["mpkg"] = "application/vnd.apple.installer+xml",
- ["odp"] = "application/vnd.oasis.opendocument.presentation",
- ["ods"] = "application/vnd.oasis.opendocument.spreadsheet",
- ["odt"] = "application/vnd.oasis.opendocument.text",
- ["oga"] = "audio/ogg",
- ["ogv"] = "video/ogg",
- ["ogx"] = "application/ogg",
- ["opus"] = "audio/opus",
- ["otf"] = "font/otf",
- ["png"] = "image/png",
- ["pdf"] = "application/pdf",
- ["php"] = "application/x-httpd-php",
- ["ppt"] = "application/vnd.ms-powerpoint",
- ["pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation",
- ["rar"] = "application/vnd.rar",
- ["rtf"] = "application/rtf",
- ["sh"] = "application/x-sh",
- ["svg"] = "image/svg+xml",
- ["tar"] = "application/x-tar",
- ["tif"] = "image/tiff",
- ["tiff"] = "image/tiff",
- ["ts"] = "video/mp2t",
- ["ttf"] = "font/ttf",
- ["txt"] = "text/plain",
- ["vsd"] = "application/vnd.visio",
- ["wav"] = "audio/wav",
- ["weba"] = "audio/webm",
- ["webm"] = "video/webm",
- ["webp"] = "image/webp",
- ["woff"] = "font/woff",
- ["woff2"] = "font/woff2",
- ["xhtml"] = "application/xhtml+xml",
- ["xls"] = "application/vnd.ms-excel",
- ["xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- ["xml"] = "default.",
- ["xul"] = "application/vnd.mozilla.xul+xml",
- ["zip"] = "application/zip",
- ["3gp"] = "video",
- ["3g2"] = "video",
- ["7z"] = "application/x-7z-compressed",
-}
-
---[[
- builtin dynamic pages
---]]
-local DYNAMIC_PAGES = {
-
--- main home page
-["/"] = [[
-
-
-
-
-
- ArduPilot
-
-
-
-
-
-
-
-]],
-
--- board status section on front page
-["@DYNAMIC/board_status.shtml"] = [[
-
-
Firmware
-
GIT Hash
-
Uptime
-
Arm Status
-
AHRS Location
-
GPS Location
-
-]]
-}
-
---[[
- builtin javascript library functions
---]]
-JS_LIBRARY = {
- ["dynamic_load"] = [[
- function dynamic_load(div_id, uri, period_ms) {
- var xhr = new XMLHttpRequest();
- xhr.open('GET', uri);
-
- xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0");
- xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT");
- xhr.setRequestHeader("Pragma", "no-cache");
-
- xhr.onload = function () {
- if (xhr.status === 200) {
- var output = document.getElementById(div_id);
- if (uri.endsWith('.shtml') || uri.endsWith('.html')) {
- output.innerHTML = xhr.responseText;
- } else {
- output.textContent = xhr.responseText;
- }
- }
- setTimeout(function() { dynamic_load(div_id,uri, period_ms); }, period_ms);
- }
- xhr.send();
- }
-]]
-}
-
-if not sock_listen:bind("0.0.0.0", WEB_BIND_PORT:get()) then
- gcs:send_text(MAV_SEVERITY.ERROR, string.format("WebServer: failed to bind to TCP %u", WEB_BIND_PORT:get()))
- return
-end
-
-if not sock_listen:listen(20) then
- gcs:send_text(MAV_SEVERITY.ERROR, "WebServer: failed to listen")
- return
-end
-
-function hms_uptime()
- local s = (millis()/1000):toint()
- local min = math.floor(s / 60) % 60
- local hr = math.floor(s / 3600)
- return string.format("%u hours %u minutes %u seconds", hr, min, s%60)
-end
-
---[[
- split string by pattern
---]]
-local function split(str, pattern)
- local ret = {}
- for s in string.gmatch(str, pattern) do
- table.insert(ret, s)
- end
- return ret
-end
-
---[[
- return true if a string ends in the 2nd string
---]]
-local function endswith(str, s)
- local len1 = #str
- local len2 = #s
- return string.sub(str,1+len1-len2,len1) == s
-end
-
---[[
- return true if a string starts with the 2nd string
---]]
-local function startswith(str, s)
- return string.sub(str,1,#s) == s
-end
-
-local debug_count=0
-
-function DEBUG(txt)
- if WEB_DEBUG:get() ~= 0 then
- gcs:send_text(MAV_SEVERITY.DEBUG, txt .. string.format(" [%u]", debug_count))
- debug_count = debug_count + 1
- end
-end
-
---[[
- return index of element in a table
---]]
-function table_index(t,el)
- for i,v in ipairs(t) do
- if v == el then
- return i
- end
- end
- return nil
-end
-
---[[
- return true if a table contains a given element
---]]
-function table_contains(t,el)
- local i = table_index(t, el)
- return i ~= nil
-end
-
-function is_hidden_dir(path)
- return table_contains(HIDDEN_FOLDERS, path)
-end
-
-local DAYS = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }
-local MONTHS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }
-
-function isdirectory(path)
- local s = fs:stat(path)
- return s and s:is_directory()
-end
-
---[[
- time string for directory listings
---]]
-function file_timestring(path)
- local s = fs:stat(path)
- if not s then
- return ""
- end
- local mtime = s:mtime()
- mtime = mtime + BRD_RTC_TZ_MIN:get()*60
- local year, month, day, hour, min, sec, _ = rtc:clock_s_to_date_fields(mtime)
- if not year then
- return ""
- end
- return string.format("%04u-%02u-%02u %02u:%02u", year, month+1, day, hour, min, sec)
-end
-
---[[
- time string for Last-Modified
---]]
-function file_timestring_http(mtime)
- local year, month, day, hour, min, sec, wday = rtc:clock_s_to_date_fields(mtime)
- if not year then
- return ""
- end
- return string.format("%s, %02u %s %u %02u:%02u:%02u GMT",
- DAYS[wday+1],
- day,
- MONTHS[month+1],
- year,
- hour,
- min,
- sec)
-end
-
---[[
- parse a http time string to a uint32_t seconds timestamp
---]]
-function file_timestring_http_parse(tstring)
- local dayname, day, monthname, year, hour, min, sec =
- string.match(tstring,
- '(%w%w%w), (%d+) (%w%w%w) (%d%d%d%d) (%d%d):(%d%d):(%d%d) GMT')
- if not dayname then
- return nil
- end
- local mon = table_index(MONTHS, monthname)
- return rtc:date_fields_to_clock_s(year, mon-1, day, hour, min, sec)
-end
-
---[[
- return true if path exists and is not a directory
---]]
-function file_exists(path)
- local s = fs:stat(path)
- if not s then
- return false
- end
- return not s:is_directory()
-end
-
---[[
- substitute variables of form {xxx} from a table
- from http://lua-users.org/wiki/StringInterpolation
---]]
-function substitute_vars(s, vars)
- s = (string.gsub(s, "({([^}]+)})",
- function(whole,i)
- return vars[i] or whole
- end))
- return s
-end
-
---[[
- lat or lon as a string, working around limited type in ftoa_engine
---]]
-function latlon_str(ll)
- local ipart = tonumber(string.match(tostring(ll*1.0e-7), '(.*[.]).*'))
- local fpart = math.abs(ll - ipart*10000000)
- return string.format("%d.%u", ipart, fpart, ipart*10000000, ll)
-end
-
---[[
- location string for home page
---]]
-function location_string(loc)
- return substitute_vars([[{lat} {lon} {alt}]],
- { ["lat"] = latlon_str(loc:lat()),
- ["lon"] = latlon_str(loc:lng()),
- ["alt"] = string.format("%.1fm", loc:alt()*1.0e-2) })
-end
-
---[[
- client class for open connections
---]]
-local function Client(_sock, _idx)
- local self = {}
-
- self.closed = false
-
- local sock = _sock
- local idx = _idx
- local have_header = false
- local header = ""
- local header_lines = {}
- local header_vars = {}
- local run = nil
- local protocol = nil
- local file = nil
- local start_time = millis()
- local offset = 0
-
- function self.read_header()
- local s = sock:recv(2048)
- if not s then
- local now = millis()
- if not sock:is_connected() or now - start_time > WEB_TIMEOUT:get()*1000 then
- -- EOF while looking for header
- DEBUG(string.format("%u: EOF", idx))
- self.remove()
- return false
- end
- return false
- end
- if not s or #s == 0 then
- return false
- end
- header = header .. s
- local eoh = string.find(s, '\r\n\r\n')
- if eoh then
- DEBUG(string.format("%u: got header", idx))
- have_header = true
- header_lines = split(header, "[^\r\n]+")
- -- blocking for reply
- sock:set_blocking(true)
- return true
- end
- return false
- end
-
- function self.sendstring(s)
- sock:send(s, #s)
- end
-
- function self.sendline(s)
- self.sendstring(s .. "\r\n")
- end
-
- --[[
- send a string with variable substitution using {varname}
- --]]
- function self.sendstring_vars(s, vars)
- self.sendstring(substitute_vars(s, vars))
- end
-
- function self.send_header(code, codestr, vars)
- self.sendline(string.format("%s %u %s", protocol, code, codestr))
- self.sendline(string.format("Server: %s", SERVER_VERSION))
- for k,v in pairs(vars) do
- self.sendline(string.format("%s: %s", k, v))
- end
- self.sendline("Connection: close")
- self.sendline("")
- end
-
- -- get size of a file
- function self.file_size(fname)
- local s = fs:stat(fname)
- if not s then
- return 0
- end
- local ret = s:size():toint()
- DEBUG(string.format("%u: size of '%s' -> %u", idx, fname, ret))
- return ret
- end
-
-
- --[[
- return full path with .. resolution
- --]]
- function self.full_path(path, name)
- DEBUG(string.format("%u: full_path(%s,%s)", idx, path, name))
- local ret = path
- if path == "/" and startswith(name,"@") then
- return name
- end
- if name == ".." then
- if path == "/" then
- return "/"
- end
- if endswith(path,"/") then
- path = string.sub(path, 1, #path-1)
- end
- local dir, _ = string.match(path, '(.*/)(.*)')
- if not dir then
- return path
- end
- return dir
- end
- if not endswith(ret, "/") then
- ret = ret .. "/"
- end
- ret = ret .. name
- DEBUG(string.format("%u: full_path(%s,%s) -> %s", idx, path, name, ret))
- return ret
- end
-
- function self.directory_list(path)
- sock:set_blocking(true)
- if startswith(path, "/@") then
- path = string.sub(path, 2, #path-1)
- end
- DEBUG(string.format("%u: directory_list(%s)", idx, path))
- local dlist = dirlist(path)
- if not dlist then
- dlist = {}
- end
- if not table_contains(dlist, "..") then
- -- on ChibiOS we don't get ..
- table.insert(dlist, "..")
- end
- if path == "/" then
- for _,v in ipairs(HIDDEN_FOLDERS) do
- table.insert(dlist, v)
- end
- end
-
- table.sort(dlist)
- self.send_header(200, "OK", {["Content-Type"]=CONTENT_TEXT_HTML})
- self.sendline(DOCTYPE)
- self.sendstring_vars([[
-
-
- Index of {path}
-
-
-
Index of {path}
-
-
Name
Last modified
Size
-]], {path=path})
- for _,d in ipairs(dlist) do
- local skip = d == "."
- if not skip then
- local fullpath = self.full_path(path, d)
- local name = d
- local sizestr = "0"
- local stat = fs:stat(fullpath)
- local size = stat and stat:size() or 0
- if is_hidden_dir(fullpath) or (stat and stat:is_directory()) then
- name = name .. "/"
- elseif size >= 100*1000*1000 then
- sizestr = string.format("%uM", (size/(1000*1000)):toint())
- else
- sizestr = tostring(size)
- end
- local modtime = file_timestring(fullpath)
- self.sendstring_vars([[
-]], { name=name, size=sizestr, modtime=modtime })
- end
- end
- self.sendstring([[
-
-
-
-]])
- end
-
- -- send file content
- function self.send_file()
- if not sock:pollout(0) then
- return
- end
- local chunk = WEB_BLOCK_SIZE:get()
- local b = file:read(chunk)
- sock:set_blocking(true)
- if b and #b > 0 then
- local sent = sock:send(b, #b)
- if sent == -1 then
- run = nil
- self.remove()
- return
- end
- if sent < #b then
- file:seek(offset+sent)
- end
- offset = offset + sent
- end
- if not b or #b < chunk then
- -- EOF
- DEBUG(string.format("%u: sent file", idx))
- run = nil
- self.remove()
- return
- end
- end
-
- --[[
- load whole file as a string
- --]]
- function self.load_file()
- local chunk = WEB_BLOCK_SIZE:get()
- local ret = ""
- while true do
- local b = file:read(chunk)
- if not b or #b == 0 then
- break
- end
- ret = ret .. b
- end
- return ret
- end
-
- --[[
- evaluate some lua code and return as a string
- --]]
- function self.evaluate(code)
- local eval_code = "function eval_func()\n" .. code .. "\nend\n"
- local f, errloc, err = load(eval_code, "eval_func", "t", _ENV)
- if not f then
- DEBUG(string.format("load failed: err=%s errloc=%s", err, errloc))
- return nil
- end
- local success, err2 = pcall(f)
- if not success then
- DEBUG(string.format("pcall failed: err=%s", err2))
- return nil
- end
- local ok, s2 = pcall(eval_func)
- eval_func = nil
- if ok then
- return s2
- end
- return nil
- end
-
- --[[
- process a file as a lua CGI
- --]]
- function self.send_cgi()
- sock:set_blocking(true)
- local contents = self.load_file()
- local s = self.evaluate(contents)
- if s then
- self.sendstring(s)
- end
- self.remove()
- end
-
- --[[
- send file content with server side processsing
- files ending in .shtml can have embedded lua lika this:
-
-
-
- Using 'lstr' a return tostring(yourcode) is added to the code
- automatically
- --]]
- function self.send_processed_file(dynamic_page)
- sock:set_blocking(true)
- local contents
- if dynamic_page then
- contents = file
- else
- contents = self.load_file()
- end
- while #contents > 0 do
- local pat1 = "(.-)[<][?]lua[ \n](.-)[?][>](.*)"
- local pat2 = "(.-)[<][?]lstr[ \n](.-)[?][>](.*)"
- local p1, p2, p3 = string.match(contents, pat1)
- if not p1 then
- p1, p2, p3 = string.match(contents, pat2)
- if not p1 then
- break
- end
- p2 = "return tostring(" .. p2 .. ")"
- end
- self.sendstring(p1)
- local s2 = self.evaluate(p2)
- if s2 then
- self.sendstring(s2)
- end
- contents = p3
- end
- self.sendstring(contents)
- self.remove()
- end
-
- -- return a content type
- function self.content_type(path)
- if path == "/" then
- return MIME_TYPES["html"]
- end
- local _, ext = string.match(path, '(.*[.])(.*)')
- ext = string.lower(ext)
- local ret = MIME_TYPES[ext]
- if not ret then
- return CONTENT_OCTET_STREAM
- end
- return ret
- end
-
- -- perform a file download
- function self.file_download(path)
- if startswith(path, "/@") then
- path = string.sub(path, 2, #path)
- end
- DEBUG(string.format("%u: file_download(%s)", idx, path))
- file = DYNAMIC_PAGES[path]
- dynamic_page = file ~= nil
- if not dynamic_page then
- file = io.open(path,"rb")
- if not file then
- DEBUG(string.format("%u: Failed to open '%s'", idx, path))
- return false
- end
- end
- local vars = {["Content-Type"]=self.content_type(path)}
- local cgi_processing = startswith(path, "/cgi-bin/") and endswith(path, ".lua")
- local server_side_processing = endswith(path, ".shtml")
- local stat = fs:stat(path)
- if not startswith(path, "@") and
- not server_side_processing and
- not cgi_processing and stat and
- not dynamic_page then
- local fsize = stat:size()
- local mtime = stat:mtime()
- vars["Content-Length"]= tostring(fsize)
- local modtime = file_timestring_http(mtime)
- if modtime then
- vars["Last-Modified"] = modtime
- end
- local if_modified_since = header_vars['If-Modified-Since']
- if if_modified_since then
- local tsec = file_timestring_http_parse(if_modified_since)
- if tsec and tsec >= mtime then
- DEBUG(string.format("%u: Not modified: %s %s", idx, modtime, if_modified_since))
- self.send_header(304, "Not Modified", vars)
- return true
- end
- end
- end
- self.send_header(200, "OK", vars)
- if server_side_processing or dynamic_page then
- DEBUG(string.format("%u: shtml processing %s", idx, path))
- run = self.send_processed_file(dynamic_page)
- elseif cgi_processing then
- DEBUG(string.format("%u: CGI processing %s", idx, path))
- run = self.send_cgi
- elseif stat and
- WEB_SENDFILE_MIN:get() > 0 and
- stat:size() >= WEB_SENDFILE_MIN:get() and
- sock:sendfile(file) then
- return true
- else
- run = self.send_file
- end
- return true
- end
-
- function self.not_found()
- self.send_header(404, "Not found", {})
- end
-
- function self.moved_permanently(relpath)
- if not startswith(relpath, "/") then
- relpath = "/" .. relpath
- end
- local location = string.format("http://%s%s", header_vars['Host'], relpath)
- DEBUG(string.format("%u: Redirect -> %s", idx, location))
- self.send_header(301, "Moved Permanently", {["Location"]=location})
- end
-
- -- process a single request
- function self.process_request()
- local h1 = header_lines[1]
- if not h1 or #h1 == 0 then
- DEBUG(string.format("%u: empty request", idx))
- return
- end
- local cmd = split(header_lines[1], "%S+")
- if not cmd or #cmd < 3 then
- DEBUG(string.format("bad request: %s", header_lines[1]))
- return
- end
- if cmd[1] ~= "GET" then
- DEBUG(string.format("bad op: %s", cmd[1]))
- return
- end
- protocol = cmd[3]
- if protocol ~= "HTTP/1.0" and protocol ~= "HTTP/1.1" then
- DEBUG(string.format("bad protocol: %s", protocol))
- return
- end
- local path = cmd[2]
- DEBUG(string.format("%u: path='%s'", idx, path))
-
- -- extract header variables
- for i = 2,#header_lines do
- local key, var = string.match(header_lines[i], '(.*): (.*)')
- if key then
- header_vars[key] = var
- end
- end
-
- if DYNAMIC_PAGES[path] ~= nil then
- self.file_download(path)
- return
- end
-
- if path == MNT_PREFIX then
- path = "/"
- end
- if startswith(path, MNT_PREFIX2) then
- path = string.sub(path,#MNT_PREFIX2,#path)
- end
-
- if isdirectory(path) and
- not endswith(path,"/") and
- header_vars['Host'] and
- not is_hidden_dir(path) then
- self.moved_permanently(path .. "/")
- return
- end
-
- if path ~= "/" and endswith(path,"/") then
- path = string.sub(path, 1, #path-1)
- end
-
- if startswith(path,"/@") then
- path = string.sub(path, 2, #path)
- end
-
- -- see if we have an index file
- if isdirectory(path) and file_exists(path .. "/index.html") then
- DEBUG(string.format("%u: found index.html", idx))
- if self.file_download(path .. "/index.html") then
- return
- end
- end
-
- -- see if it is a directory
- if (path == "/" or
- DYNAMIC_PAGES[path] == nil) and
- (endswith(path,"/") or
- isdirectory(path) or
- is_hidden_dir(path)) then
- self.directory_list(path)
- return
- end
-
- -- or a file
- if self.file_download(path) then
- return
- end
- self.not_found(path)
- end
-
- -- update the client
- function self.update()
- if run then
- run()
- return
- end
- if not have_header then
- if not self.read_header() then
- return
- end
- end
- self.process_request()
- if not run then
- -- nothing more to do
- self.remove()
- end
- end
-
- function self.remove()
- DEBUG(string.format("%u: removing client OFFSET=%u", idx, offset))
- if sock then
- sock:close()
- sock = nil
- end
- self.closed = true
- end
-
- -- return the instance
- return self
-end
-
---[[
- see if any new clients want to connect
---]]
-local function check_new_clients()
- while sock_listen:pollin(0) do
- local sock = sock_listen:accept()
- if not sock then
- return
- end
- -- non-blocking for header read
- sock:set_blocking(false)
- -- find free client slot
- for i = 1, #clients+1 do
- if clients[i] == nil then
- local idx = i
- local client = Client(sock, idx)
- DEBUG(string.format("%u: New client", idx))
- clients[idx] = client
- end
- end
- end
-end
-
---[[
- check for client activity
---]]
-local function check_clients()
- for idx,client in ipairs(clients) do
- if not client.closed then
- client.update()
- end
- if client.closed then
- table.remove(clients,idx)
- end
- end
-end
+web.printf(web.MAV_SEVERITY.INFO, "Starting web server")
+web.add_board_status("Arm Status", 'arming:is_armed() and "ARMED" or "DISARMED"')
+web.add_board_status("AHRS Location", 'location_string(ahrs:get_location())')
+web.add_board_status("GPS Location", 'location_string(ahrs:get_location())')
local function update()
- check_new_clients()
- check_clients()
- return update,5
+ web.update()
+ return update, 5
end
-return update,100
+return update, 100
diff --git a/libraries/AP_Scripting/applets/net_webserver.md b/libraries/AP_Scripting/applets/net_webserver.md
index fa45efe780b46..acfab57e94bee 100644
--- a/libraries/AP_Scripting/applets/net_webserver.md
+++ b/libraries/AP_Scripting/applets/net_webserver.md
@@ -1,91 +1 @@
-# Web Server Application
-
-This implements a web server for boards that have networking support.
-
-# Parameters
-
-The web server has a small number of parameters
-
-## WEB_ENABLE
-
-This must be set to 1 to enable the web server
-
-## WEB_BIND_PORT
-
-This sets the network port to use for the server. It defaults to 8080
-
-## WEB_DEBUG
-
-This enables verbose debugging
-
-## WEB_BLOCK_SIZE
-
-This sets the block size for network and file read/write
-operations. Setting a larger value can increase performance at the
-cost of more memory
-
-## WEB_TIMEOUT
-
-This sets the timeout in seconds for inactive client connections.
-
-# Operation
-
-By default the web server serves the root of your microSD card. You
-can include html, javascript (*.js), image files etc on your microSD
-to create a full web server with any structure you want.
-
-## Server Side Scripting
-
-The web server supports embedding lua script elements inside html
-files for files with a filename of *.shtml. Here is an example:
-
-```
-
-
-
-
-
-
Server Side Scripting Test
-
-
Roll
Pitch
Yaw
-
-
-
-
-
-
-
-
-```
-In this example we are using two forms of embedded lua scripts. The
-first form starts with "= math.huge then
+ error("unexpected number value '" .. tostring(val) .. "'")
+ end
+ return string.format("%.14g", val)
+end
+
+
+local type_func_map = {
+ [ "nil" ] = encode_nil,
+ [ "table" ] = encode_table,
+ [ "string" ] = encode_string,
+ [ "number" ] = encode_number,
+ [ "boolean" ] = tostring,
+}
+
+
+encode = function(val, stack)
+ local t = type(val)
+ local f = type_func_map[t]
+ if f then
+ return f(val, stack)
+ end
+ error("unexpected type '" .. t .. "'")
+end
+
+
+function json.encode(val)
+ return ( encode(val) )
+end
+
+
+-------------------------------------------------------------------------------
+-- Decode
+-------------------------------------------------------------------------------
+
+local parse
+
+local function create_set(...)
+ local res = {}
+ for i = 1, select("#", ...) do
+ res[ select(i, ...) ] = true
+ end
+ return res
+end
+
+local space_chars = create_set(" ", "\t", "\r", "\n")
+local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
+local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
+local literals = create_set("true", "false", "null")
+
+local literal_map = {
+ [ "true" ] = true,
+ [ "false" ] = false,
+ [ "null" ] = nil,
+}
+
+
+local function next_char(str, idx, set, negate)
+ for i = idx, #str do
+ if set[str:sub(i, i)] ~= negate then
+ return i
+ end
+ end
+ return #str + 1
+end
+
+
+local function decode_error(str, idx, msg)
+ local line_count = 1
+ local col_count = 1
+ for i = 1, idx - 1 do
+ col_count = col_count + 1
+ if str:sub(i, i) == "\n" then
+ line_count = line_count + 1
+ col_count = 1
+ end
+ end
+ error( string.format("%s at line %d col %d", msg, line_count, col_count) )
+end
+
+
+local function codepoint_to_utf8(n)
+ -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
+ local f = math.floor
+ if n <= 0x7f then
+ return string.char(n)
+ elseif n <= 0x7ff then
+ return string.char(f(n / 64) + 192, n % 64 + 128)
+ elseif n <= 0xffff then
+ return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
+ elseif n <= 0x10ffff then
+ return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
+ f(n % 4096 / 64) + 128, n % 64 + 128)
+ end
+ error( string.format("invalid unicode codepoint '%x'", n) )
+end
+
+
+local function parse_unicode_escape(s)
+ local n1 = tonumber( s:sub(1, 4), 16 )
+ local n2 = tonumber( s:sub(7, 10), 16 )
+ -- Surrogate pair?
+ if n2 then
+ return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
+ else
+ return codepoint_to_utf8(n1)
+ end
+end
+
+
+local function parse_string(str, i)
+ local res = ""
+ local j = i + 1
+ local k = j
+
+ while j <= #str do
+ local x = str:byte(j)
+
+ if x < 32 then
+ decode_error(str, j, "control character in string")
+
+ elseif x == 92 then -- `\`: Escape
+ res = res .. str:sub(k, j - 1)
+ j = j + 1
+ local c = str:sub(j, j)
+ if c == "u" then
+ local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
+ or str:match("^%x%x%x%x", j + 1)
+ or decode_error(str, j - 1, "invalid unicode escape in string")
+ res = res .. parse_unicode_escape(hex)
+ j = j + #hex
+ else
+ if not escape_chars[c] then
+ decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
+ end
+ res = res .. escape_char_map_inv[c]
+ end
+ k = j + 1
+
+ elseif x == 34 then -- `"`: End of string
+ res = res .. str:sub(k, j - 1)
+ return res, j + 1
+ end
+
+ j = j + 1
+ end
+
+ decode_error(str, i, "expected closing quote for string")
+end
+
+
+local function parse_number(str, i)
+ local x = next_char(str, i, delim_chars)
+ local s = str:sub(i, x - 1)
+ local n = tonumber(s)
+ if not n then
+ decode_error(str, i, "invalid number '" .. s .. "'")
+ end
+ return n, x
+end
+
+
+local function parse_literal(str, i)
+ local x = next_char(str, i, delim_chars)
+ local word = str:sub(i, x - 1)
+ if not literals[word] then
+ decode_error(str, i, "invalid literal '" .. word .. "'")
+ end
+ return literal_map[word], x
+end
+
+
+local function parse_array(str, i)
+ local res = {}
+ local n = 1
+ i = i + 1
+ while 1 do
+ local x
+ i = next_char(str, i, space_chars, true)
+ -- Empty / end of array?
+ if str:sub(i, i) == "]" then
+ i = i + 1
+ break
+ end
+ -- Read token
+ x, i = parse(str, i)
+ res[n] = x
+ n = n + 1
+ -- Next token
+ i = next_char(str, i, space_chars, true)
+ local chr = str:sub(i, i)
+ i = i + 1
+ if chr == "]" then break end
+ if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
+ end
+ return res, i
+end
+
+
+local function parse_object(str, i)
+ local res = {}
+ i = i + 1
+ while 1 do
+ local key, val
+ i = next_char(str, i, space_chars, true)
+ -- Empty / end of object?
+ if str:sub(i, i) == "}" then
+ i = i + 1
+ break
+ end
+ -- Read key
+ if str:sub(i, i) ~= '"' then
+ decode_error(str, i, "expected string for key")
+ end
+ key, i = parse(str, i)
+ -- Read ':' delimiter
+ i = next_char(str, i, space_chars, true)
+ if str:sub(i, i) ~= ":" then
+ decode_error(str, i, "expected ':' after key")
+ end
+ i = next_char(str, i + 1, space_chars, true)
+ -- Read value
+ val, i = parse(str, i)
+ -- Set
+ res[key] = val
+ -- Next token
+ i = next_char(str, i, space_chars, true)
+ local chr = str:sub(i, i)
+ i = i + 1
+ if chr == "}" then break end
+ if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
+ end
+ return res, i
+end
+
+
+local char_func_map = {
+ [ '"' ] = parse_string,
+ [ "0" ] = parse_number,
+ [ "1" ] = parse_number,
+ [ "2" ] = parse_number,
+ [ "3" ] = parse_number,
+ [ "4" ] = parse_number,
+ [ "5" ] = parse_number,
+ [ "6" ] = parse_number,
+ [ "7" ] = parse_number,
+ [ "8" ] = parse_number,
+ [ "9" ] = parse_number,
+ [ "-" ] = parse_number,
+ [ "t" ] = parse_literal,
+ [ "f" ] = parse_literal,
+ [ "n" ] = parse_literal,
+ [ "[" ] = parse_array,
+ [ "{" ] = parse_object,
+}
+
+
+parse = function(str, idx)
+ local chr = str:sub(idx, idx)
+ local f = char_func_map[chr]
+ if f then
+ return f(str, idx)
+ end
+ decode_error(str, idx, "unexpected character '" .. chr .. "'")
+end
+
+
+function json.decode(str)
+ if type(str) ~= "string" then
+ error("expected argument of type string, got " .. type(str))
+ end
+ local res, idx = parse(str, next_char(str, 1, space_chars, true))
+ idx = next_char(str, idx, space_chars, true)
+ if idx <= #str then
+ decode_error(str, idx, "trailing garbage")
+ end
+ return res
+end
+
+
+return json
diff --git a/libraries/AP_Scripting/modules/webserver.lua b/libraries/AP_Scripting/modules/webserver.lua
new file mode 100644
index 0000000000000..4e79ed0ed96a5
--- /dev/null
+++ b/libraries/AP_Scripting/modules/webserver.lua
@@ -0,0 +1,1090 @@
+--[[
+ example script to test lua socket API
+--]]
+---@diagnostic disable: param-type-mismatch
+---@diagnostic disable: undefined-field
+---@diagnostic disable: need-check-nil
+---@diagnostic disable: redundant-parameter
+
+local json = require('json')
+local web = {}
+
+web.MAV_SEVERITY = {EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFO=6, DEBUG=7}
+
+PARAM_TABLE_KEY = 47
+PARAM_TABLE_PREFIX = "WEB_"
+
+-- add a parameter and bind it to a variable
+function bind_add_param(name, idx, default_value)
+ assert(param:add_param(PARAM_TABLE_KEY, idx, name, default_value), string.format('could not add param %s', name))
+ return Parameter(PARAM_TABLE_PREFIX .. name)
+end
+
+-- Setup Parameters
+assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 6), 'net_test: could not add param table')
+
+--[[
+ // @Param: WEB_ENABLE
+ // @DisplayName: enable web server
+ // @Description: enable web server
+ // @Values: 0:Disabled,1:Enabled
+ // @User: Standard
+--]]
+local WEB_ENABLE = bind_add_param('ENABLE', 1, 1)
+
+--[[
+ // @Param: WEB_BIND_PORT
+ // @DisplayName: web server TCP port
+ // @Description: web server TCP port
+ // @Range: 1 65535
+ // @User: Standard
+--]]
+local WEB_BIND_PORT = bind_add_param('BIND_PORT', 2, 8080)
+
+--[[
+ // @Param: WEB_DEBUG
+ // @DisplayName: web server debugging
+ // @Description: web server debugging
+ // @Values: 0:Disabled,1:Enabled
+ // @User: Advanced
+--]]
+local WEB_DEBUG = bind_add_param('DEBUG', 3, 0)
+
+--[[
+ // @Param: WEB_BLOCK_SIZE
+ // @DisplayName: web server block size
+ // @Description: web server block size for download
+ // @Range: 1 65535
+ // @User: Advanced
+--]]
+local WEB_BLOCK_SIZE = bind_add_param('BLOCK_SIZE', 4, 10240)
+
+--[[
+ // @Param: WEB_TIMEOUT
+ // @DisplayName: web server timeout
+ // @Description: timeout for inactive connections
+ // @Units: s
+ // @Range: 0.1 60
+ // @User: Advanced
+--]]
+local WEB_TIMEOUT = bind_add_param('TIMEOUT', 5, 2.0)
+
+--[[
+ // @Param: WEB_SENDFILE_MIN
+ // @DisplayName: web server minimum file size for sendfile
+ // @Description: sendfile is an offloading mechanism for faster file download. If this is non-zero and the file is larger than this size then sendfile will be used for file download
+ // @Range: 0 10000000
+ // @User: Advanced
+--]]
+local WEB_SENDFILE_MIN = bind_add_param('SENDFILE_MIN', 6, 100000)
+
+local BRD_RTC_TZ_MIN = Parameter("BRD_RTC_TZ_MIN")
+
+if periph ~= nil then
+ function web.printf(severity, str)
+ return periph:can_printf_severity(severity, str)
+ end
+else
+ function web.printf(severity, str)
+ return gcs:send_text(severity, str)
+ end
+end
+
+if WEB_ENABLE:get() ~= 1 then
+ web.printf(web.MAV_SEVERITY.INFO, "WebServer: disabled")
+ return
+end
+
+web.printf(web.MAV_SEVERITY.INFO, string.format("WebServer: starting on port %u", WEB_BIND_PORT:get()))
+
+local sock_listen = Socket(0)
+local clients = {}
+
+local DOCTYPE = ""
+local SERVER_VERSION = "net_webserver 1.0"
+local CONTENT_TEXT_HTML = "text/html;charset=UTF-8"
+local CONTENT_OCTET_STREAM = "application/octet-stream"
+
+local HIDDEN_FOLDERS = { "@SYS", "@ROMFS", "@MISSION", "@PARAM" }
+
+local MNT_PREFIX = "/mnt"
+local MNT_PREFIX2 = MNT_PREFIX .. "/"
+
+local MIME_TYPES = {
+ ["apj"] = CONTENT_OCTET_STREAM,
+ ["dat"] = CONTENT_OCTET_STREAM,
+ ["o"] = CONTENT_OCTET_STREAM,
+ ["obj"] = CONTENT_OCTET_STREAM,
+ ["lua"] = "text/x-lua",
+ ["py"] = "text/x-python",
+ ["shtml"] = CONTENT_TEXT_HTML,
+ ["js"] = "text/javascript",
+ -- thanks to https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
+ ["aac"] = "audio/aac",
+ ["abw"] = "application/x-abiword",
+ ["arc"] = "application/x-freearc",
+ ["avif"] = "image/avif",
+ ["avi"] = "video/x-msvideo",
+ ["azw"] = "application/vnd.amazon.ebook",
+ ["bin"] = "application/octet-stream",
+ ["bmp"] = "image/bmp",
+ ["bz"] = "application/x-bzip",
+ ["bz2"] = "application/x-bzip2",
+ ["cda"] = "application/x-cdf",
+ ["csh"] = "application/x-csh",
+ ["css"] = "text/css",
+ ["csv"] = "text/csv",
+ ["doc"] = "application/msword",
+ ["docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ ["eot"] = "application/vnd.ms-fontobject",
+ ["epub"] = "application/epub+zip",
+ ["gz"] = "application/gzip",
+ ["gif"] = "image/gif",
+ ["htm"] = CONTENT_TEXT_HTML,
+ ["html"] = CONTENT_TEXT_HTML,
+ ["ico"] = "image/vnd.microsoft.icon",
+ ["ics"] = "text/calendar",
+ ["jar"] = "application/java-archive",
+ ["jpeg"] = "image/jpeg",
+ ["json"] = "application/json",
+ ["jsonld"] = "application/ld+json",
+ ["mid"] = "audio/x-midi",
+ ["mjs"] = "text/javascript",
+ ["mp3"] = "audio/mpeg",
+ ["mp4"] = "video/mp4",
+ ["mpeg"] = "video/mpeg",
+ ["mpkg"] = "application/vnd.apple.installer+xml",
+ ["odp"] = "application/vnd.oasis.opendocument.presentation",
+ ["ods"] = "application/vnd.oasis.opendocument.spreadsheet",
+ ["odt"] = "application/vnd.oasis.opendocument.text",
+ ["oga"] = "audio/ogg",
+ ["ogv"] = "video/ogg",
+ ["ogx"] = "application/ogg",
+ ["opus"] = "audio/opus",
+ ["otf"] = "font/otf",
+ ["png"] = "image/png",
+ ["pdf"] = "application/pdf",
+ ["php"] = "application/x-httpd-php",
+ ["ppt"] = "application/vnd.ms-powerpoint",
+ ["pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ ["rar"] = "application/vnd.rar",
+ ["rtf"] = "application/rtf",
+ ["sh"] = "application/x-sh",
+ ["svg"] = "image/svg+xml",
+ ["tar"] = "application/x-tar",
+ ["tif"] = "image/tiff",
+ ["tiff"] = "image/tiff",
+ ["ts"] = "video/mp2t",
+ ["ttf"] = "font/ttf",
+ ["txt"] = "text/plain",
+ ["vsd"] = "application/vnd.visio",
+ ["wav"] = "audio/wav",
+ ["weba"] = "audio/webm",
+ ["webm"] = "video/webm",
+ ["webp"] = "image/webp",
+ ["woff"] = "font/woff",
+ ["woff2"] = "font/woff2",
+ ["xhtml"] = "application/xhtml+xml",
+ ["xls"] = "application/vnd.ms-excel",
+ ["xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ ["xml"] = "default.",
+ ["xul"] = "application/vnd.mozilla.xul+xml",
+ ["zip"] = "application/zip",
+ ["3gp"] = "video",
+ ["3g2"] = "video",
+ ["7z"] = "application/x-7z-compressed",
+}
+
+--[[
+ builtin dynamic pages
+--]]
+local DYNAMIC_PAGES = {
+
+-- main home page
+["/"] = [[
+
+
+
+
+
+ ArduPilot
+
+
+
+
";
+ output.innerHTML = table;
+ }
+ setTimeout(function() { populate_json_objects(period_ms); }, period_ms);
+ }
+ xhr.send();
+ }
+]]
+}
+
+if not sock_listen:bind("0.0.0.0", WEB_BIND_PORT:get()) then
+ web.printf(web.MAV_SEVERITY.ERROR, string.format("WebServer: failed to bind to TCP %u", WEB_BIND_PORT:get()))
+ return
+end
+
+if not sock_listen:listen(20) then
+ web.printf(web.MAV_SEVERITY.ERROR, "WebServer: failed to listen")
+ return
+end
+
+function hms_uptime()
+ local s = (millis()/1000):toint()
+ local min = math.floor(s / 60) % 60
+ local hr = math.floor(s / 3600)
+ return string.format("%u hours %u minutes %u seconds", hr, min, s%60)
+end
+
+--[[
+ split string by pattern
+--]]
+local function split(str, pattern)
+ local ret = {}
+ for s in string.gmatch(str, pattern) do
+ table.insert(ret, s)
+ end
+ return ret
+end
+
+--[[
+ return true if a string ends in the 2nd string
+--]]
+local function endswith(str, s)
+ local len1 = #str
+ local len2 = #s
+ return string.sub(str,1+len1-len2,len1) == s
+end
+
+--[[
+ return true if a string starts with the 2nd string
+--]]
+local function startswith(str, s)
+ return string.sub(str,1,#s) == s
+end
+
+local debug_count=0
+
+function DEBUG(txt)
+ if WEB_DEBUG:get() ~= 0 then
+ web.printf(web.MAV_SEVERITY.DEBUG, txt .. string.format(" [%u]", debug_count))
+ debug_count = debug_count + 1
+ end
+end
+
+--[[
+ return index of element in a table
+--]]
+function table_index(t,el)
+ for i,v in ipairs(t) do
+ if v == el then
+ return i
+ end
+ end
+ return nil
+end
+
+--[[
+ return true if a table contains a given element
+--]]
+function table_contains(t,el)
+ local i = table_index(t, el)
+ return i ~= nil
+end
+
+function is_hidden_dir(path)
+ return table_contains(HIDDEN_FOLDERS, path)
+end
+
+local DAYS = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }
+local MONTHS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }
+
+function isdirectory(path)
+ local s = fs:stat(path)
+ return s and s:is_directory()
+end
+
+--[[
+ time string for directory listings
+--]]
+function file_timestring(path)
+ local s = fs:stat(path)
+ if not s then
+ return ""
+ end
+ local mtime = s:mtime()
+ mtime = mtime + BRD_RTC_TZ_MIN:get()*60
+ local year, month, day, hour, min, sec, _ = rtc:clock_s_to_date_fields(mtime)
+ if not year then
+ return ""
+ end
+ return string.format("%04u-%02u-%02u %02u:%02u", year, month+1, day, hour, min, sec)
+end
+
+--[[
+ time string for Last-Modified
+--]]
+function file_timestring_http(mtime)
+ local year, month, day, hour, min, sec, wday = rtc:clock_s_to_date_fields(mtime)
+ if not year then
+ return ""
+ end
+ return string.format("%s, %02u %s %u %02u:%02u:%02u GMT",
+ DAYS[wday+1],
+ day,
+ MONTHS[month+1],
+ year,
+ hour,
+ min,
+ sec)
+end
+
+--[[
+ parse a http time string to a uint32_t seconds timestamp
+--]]
+function file_timestring_http_parse(tstring)
+ local dayname, day, monthname, year, hour, min, sec =
+ string.match(tstring,
+ '(%w%w%w), (%d+) (%w%w%w) (%d%d%d%d) (%d%d):(%d%d):(%d%d) GMT')
+ if not dayname then
+ return nil
+ end
+ local mon = table_index(MONTHS, monthname)
+ return rtc:date_fields_to_clock_s(year, mon-1, day, hour, min, sec)
+end
+
+--[[
+ return true if path exists and is not a directory
+--]]
+function file_exists(path)
+ local s = fs:stat(path)
+ if not s then
+ return false
+ end
+ return not s:is_directory()
+end
+
+--[[
+ substitute variables of form {xxx} from a table
+ from http://lua-users.org/wiki/StringInterpolation
+--]]
+function substitute_vars(s, vars)
+ s = (string.gsub(s, "({([^}]+)})",
+ function(whole,i)
+ return vars[i] or whole
+ end))
+ return s
+end
+
+--[[
+ lat or lon as a string, working around limited type in ftoa_engine
+--]]
+function latlon_str(ll)
+ local ipart = tonumber(string.match(tostring(ll*1.0e-7), '(.*[.]).*'))
+ local fpart = math.abs(ll - ipart*10000000)
+ return string.format("%d.%u", ipart, fpart, ipart*10000000, ll)
+end
+
+--[[
+ location string for home page
+--]]
+function location_string(loc)
+ return substitute_vars([[{lat} {lon} {alt}]],
+ { ["lat"] = latlon_str(loc:lat()),
+ ["lon"] = latlon_str(loc:lng()),
+ ["alt"] = string.format("%.1fm", loc:alt()*1.0e-2) })
+end
+
+--[[
+ evaluate some lua code and return as a string
+--]]
+function web.evaluate(code)
+ local eval_code = "function eval_func()\n" .. code .. "\nend\n"
+ local f, errloc, err = load(eval_code, "eval_func", "t", _ENV)
+ if not f then
+ DEBUG(string.format("load failed: err=%s errloc=%s", err, errloc))
+ return nil
+ end
+ local success, err2 = pcall(f)
+ if not success then
+ DEBUG(string.format("pcall failed: err=%s", err2))
+ return nil
+ end
+ local ok, s2 = pcall(eval_func)
+ eval_func = nil
+ if ok then
+ return s2
+ end
+ return nil
+end
+
+
+--[[
+ client class for open connections
+--]]
+local function Client(_sock, _idx)
+ local self = {}
+
+ self.closed = false
+
+ local sock = _sock
+ local idx = _idx
+ local have_header = false
+ local header = ""
+ local header_lines = {}
+ local header_vars = {}
+ local run = nil
+ local protocol = nil
+ local file = nil
+ local start_time = millis()
+ local offset = 0
+
+ function self.read_header()
+ local s = sock:recv(2048)
+ if not s then
+ local now = millis()
+ if not sock:is_connected() or now - start_time > WEB_TIMEOUT:get()*1000 then
+ -- EOF while looking for header
+ DEBUG(string.format("%u: EOF", idx))
+ self.remove()
+ return false
+ end
+ return false
+ end
+ if not s or #s == 0 then
+ return false
+ end
+ header = header .. s
+ local eoh = string.find(s, '\r\n\r\n')
+ if eoh then
+ DEBUG(string.format("%u: got header", idx))
+ have_header = true
+ header_lines = split(header, "[^\r\n]+")
+ -- blocking for reply
+ sock:set_blocking(true)
+ return true
+ end
+ return false
+ end
+
+ function self.sendstring(s)
+ sock:send(s, #s)
+ end
+
+ function self.sendline(s)
+ self.sendstring(s .. "\r\n")
+ end
+
+ --[[
+ send a string with variable substitution using {varname}
+ --]]
+ function self.sendstring_vars(s, vars)
+ self.sendstring(substitute_vars(s, vars))
+ end
+
+ function self.send_header(code, codestr, vars)
+ self.sendline(string.format("%s %u %s", protocol, code, codestr))
+ self.sendline(string.format("Server: %s", SERVER_VERSION))
+ for k,v in pairs(vars) do
+ self.sendline(string.format("%s: %s", k, v))
+ end
+ self.sendline("Connection: close")
+ self.sendline("")
+ end
+
+ -- get size of a file
+ function self.file_size(fname)
+ local s = fs:stat(fname)
+ if not s then
+ return 0
+ end
+ local ret = s:size():toint()
+ DEBUG(string.format("%u: size of '%s' -> %u", idx, fname, ret))
+ return ret
+ end
+
+
+ --[[
+ return full path with .. resolution
+ --]]
+ function self.full_path(path, name)
+ DEBUG(string.format("%u: full_path(%s,%s)", idx, path, name))
+ local ret = path
+ if path == "/" and startswith(name,"@") then
+ return name
+ end
+ if name == ".." then
+ if path == "/" then
+ end
+ if endswith(path,"/") then
+ path = string.sub(path, 1, #path-1)
+ end
+ local dir, _ = string.match(path, '(.*/)(.*)')
+ if not dir then
+ return path
+ end
+ return dir
+ end
+ if not endswith(ret, "/") then
+ ret = ret .. "/"
+ end
+ ret = ret .. name
+ DEBUG(string.format("%u: full_path(%s,%s) -> %s", idx, path, name, ret))
+ return ret
+ end
+
+ function self.directory_list(path)
+ sock:set_blocking(true)
+ if startswith(path, "/@") then
+ path = string.sub(path, 2, #path-1)
+ end
+ DEBUG(string.format("%u: directory_list(%s)", idx, path))
+ local dlist = dirlist(path)
+ if not dlist then
+ dlist = {}
+ end
+ if not table_contains(dlist, "..") then
+ -- on ChibiOS we don't get ..
+ table.insert(dlist, "..")
+ end
+ if path == "/" then
+ for _,v in ipairs(HIDDEN_FOLDERS) do
+ table.insert(dlist, v)
+ end
+ end
+
+ table.sort(dlist)
+ self.send_header(200, "OK", {["Content-Type"]=CONTENT_TEXT_HTML})
+ self.sendline(DOCTYPE)
+ self.sendstring_vars([[
+
+
+ Index of {path}
+
+
+
Index of {path}
+
+
Name
Last modified
Size
+]], {path=path})
+ for _,d in ipairs(dlist) do
+ local skip = d == "."
+ if not skip then
+ local fullpath = self.full_path(path, d)
+ local name = d
+ local sizestr = "0"
+ local stat = fs:stat(fullpath)
+ local size = stat and stat:size() or 0
+ if is_hidden_dir(fullpath) or (stat and stat:is_directory()) then
+ name = name .. "/"
+ elseif size >= 100*1000*1000 then
+ sizestr = string.format("%uM", (size/(1000*1000)):toint())
+ else
+ sizestr = tostring(size)
+ end
+ local modtime = file_timestring(fullpath)
+ self.sendstring_vars([[
+]], { name=name, size=sizestr, modtime=modtime })
+ end
+ end
+ self.sendstring([[
+
+
+
+]])
+ end
+
+ -- send file content
+ function self.send_file()
+ if not sock:pollout(0) then
+ return
+ end
+ local chunk = WEB_BLOCK_SIZE:get()
+ local b = file:read(chunk)
+ sock:set_blocking(true)
+ if b and #b > 0 then
+ local sent = sock:send(b, #b)
+ if sent == -1 then
+ run = nil
+ self.remove()
+ return
+ end
+ if sent < #b then
+ file:seek(offset+sent)
+ end
+ offset = offset + sent
+ end
+ if not b or #b < chunk then
+ -- EOF
+ DEBUG(string.format("%u: sent file", idx))
+ run = nil
+ self.remove()
+ return
+ end
+ end
+
+ --[[
+ load whole file as a string
+ --]]
+ function self.load_file()
+ local chunk = WEB_BLOCK_SIZE:get()
+ local ret = ""
+ while true do
+ local b = file:read(chunk)
+ if not b or #b == 0 then
+ break
+ end
+ ret = ret .. b
+ end
+ return ret
+ end
+
+ --[[
+ process a file as a lua CGI
+ --]]
+ function self.send_cgi()
+ sock:set_blocking(true)
+ local contents = self.load_file()
+ local s = web.evaluate(contents)
+ if s then
+ self.sendstring(s)
+ end
+ self.remove()
+ end
+
+ --[[
+ send file content with server side processsing
+ files ending in .shtml can have embedded lua lika this:
+
+
+
+ Using 'lstr' a return tostring(yourcode) is added to the code
+ automatically
+ --]]
+ function self.send_processed_file(dynamic_page)
+ sock:set_blocking(true)
+ local contents
+ if dynamic_page then
+ contents = file
+ else
+ contents = self.load_file()
+ end
+ while #contents > 0 do
+ local pat1 = "(.-)[<][?]lua[ \n](.-)[?][>](.*)"
+ local pat2 = "(.-)[<][?]lstr[ \n](.-)[?][>](.*)"
+ local p1, p2, p3 = string.match(contents, pat1)
+ if not p1 then
+ p1, p2, p3 = string.match(contents, pat2)
+ if not p1 then
+ break
+ end
+ p2 = "return tostring(" .. p2 .. ")"
+ end
+ self.sendstring(p1)
+ local s2 = web.evaluate(p2)
+ if s2 then
+ self.sendstring(s2)
+ end
+ contents = p3
+ end
+ self.sendstring(contents)
+ self.remove()
+ end
+
+ -- return a content type
+ function self.content_type(path)
+ if path == "/" then
+ return MIME_TYPES["html"]
+ end
+ local _, ext = string.match(path, '(.*[.])(.*)')
+ ext = string.lower(ext)
+ local ret = MIME_TYPES[ext]
+ if not ret then
+ return CONTENT_OCTET_STREAM
+ end
+ return ret
+ end
+
+ -- perform a file download
+ function self.file_download(path)
+ if startswith(path, "/@") then
+ path = string.sub(path, 2, #path)
+ end
+ DEBUG(string.format("%u: file_download(%s)", idx, path))
+ file = DYNAMIC_PAGES[path]
+ dynamic_page = file ~= nil
+ if not dynamic_page then
+ file = io.open(path,"rb")
+ if not file then
+ DEBUG(string.format("%u: Failed to open '%s'", idx, path))
+ return false
+ end
+ end
+ local vars = {["Content-Type"]=self.content_type(path)}
+ local cgi_processing = startswith(path, "/cgi-bin/") and endswith(path, ".lua")
+ local server_side_processing = endswith(path, ".shtml")
+ local stat = fs:stat(path)
+ if not startswith(path, "@") and
+ not server_side_processing and
+ not cgi_processing and stat and
+ not dynamic_page then
+ local fsize = stat:size()
+ local mtime = stat:mtime()
+ vars["Content-Length"]= tostring(fsize)
+ local modtime = file_timestring_http(mtime)
+ if modtime then
+ vars["Last-Modified"] = modtime
+ end
+ local if_modified_since = header_vars['If-Modified-Since']
+ if if_modified_since then
+ local tsec = file_timestring_http_parse(if_modified_since)
+ if tsec and tsec >= mtime then
+ DEBUG(string.format("%u: Not modified: %s %s", idx, modtime, if_modified_since))
+ self.send_header(304, "Not Modified", vars)
+ return true
+ end
+ end
+ end
+ self.send_header(200, "OK", vars)
+ if server_side_processing or dynamic_page then
+ DEBUG(string.format("%u: shtml processing %s", idx, path))
+ run = self.send_processed_file(dynamic_page)
+ elseif cgi_processing then
+ DEBUG(string.format("%u: CGI processing %s", idx, path))
+ run = self.send_cgi
+ elseif stat and
+ WEB_SENDFILE_MIN:get() > 0 and
+ stat:size() >= WEB_SENDFILE_MIN:get() and
+ sock:sendfile(file) then
+ return true
+ else
+ run = self.send_file
+ end
+ return true
+ end
+
+ function self.not_found()
+ self.send_header(404, "Not found", {})
+ end
+
+ function self.moved_permanently(relpath)
+ if not startswith(relpath, "/") then
+ relpath = "/" .. relpath
+ end
+ local location = string.format("http://%s%s", header_vars['Host'], relpath)
+ DEBUG(string.format("%u: Redirect -> %s", idx, location))
+ self.send_header(301, "Moved Permanently", {["Location"]=location})
+ end
+
+ -- process a single request
+ function self.process_request()
+ local h1 = header_lines[1]
+ if not h1 or #h1 == 0 then
+ DEBUG(string.format("%u: empty request", idx))
+ return
+ end
+ local cmd = split(header_lines[1], "%S+")
+ if not cmd or #cmd < 3 then
+ DEBUG(string.format("bad request: %s", header_lines[1]))
+ return
+ end
+ if cmd[1] ~= "GET" then
+ DEBUG(string.format("bad op: %s", cmd[1]))
+ return
+ end
+ protocol = cmd[3]
+ if protocol ~= "HTTP/1.0" and protocol ~= "HTTP/1.1" then
+ DEBUG(string.format("bad protocol: %s", protocol))
+ return
+ end
+ local path = cmd[2]
+ DEBUG(string.format("%u: path='%s'", idx, path))
+
+ -- extract header variables
+ for i = 2,#header_lines do
+ local key, var = string.match(header_lines[i], '(.*): (.*)')
+ if key then
+ header_vars[key] = var
+ end
+ end
+
+ if DYNAMIC_PAGES[path] ~= nil then
+ self.file_download(path)
+ return
+ end
+
+ if path == MNT_PREFIX then
+ path = "/"
+ end
+ if startswith(path, MNT_PREFIX2) then
+ path = string.sub(path,#MNT_PREFIX2,#path)
+ end
+
+ if isdirectory(path) and
+ not endswith(path,"/") and
+ header_vars['Host'] and
+ not is_hidden_dir(path) then
+ self.moved_permanently(path .. "/")
+ return
+ end
+
+ if path ~= "/" and endswith(path,"/") then
+ path = string.sub(path, 1, #path-1)
+ end
+
+ if startswith(path,"/@") then
+ path = string.sub(path, 2, #path)
+ end
+
+ -- see if we have an index file
+ if isdirectory(path) and file_exists(path .. "/index.html") then
+ DEBUG(string.format("%u: found index.html", idx))
+ if self.file_download(path .. "/index.html") then
+ return
+ end
+ end
+
+ -- see if it is a directory
+ if (path == "/" or
+ DYNAMIC_PAGES[path] == nil) and
+ (endswith(path,"/") or
+ isdirectory(path) or
+ is_hidden_dir(path)) then
+ self.directory_list(path)
+ return
+ end
+
+ -- or a file
+ if self.file_download(path) then
+ return
+ end
+ self.not_found(path)
+ end
+
+ -- update the client
+ function self.update()
+ if run then
+ run()
+ return
+ end
+ if not have_header then
+ if not self.read_header() then
+ return
+ end
+ end
+ self.process_request()
+ if not run then
+ -- nothing more to do
+ self.remove()
+ end
+ end
+
+ function self.remove()
+ DEBUG(string.format("%u: removing client OFFSET=%u", idx, offset))
+ if sock then
+ sock:close()
+ sock = nil
+ end
+ self.closed = true
+ end
+
+ -- return the instance
+ return self
+end
+
+--[[
+ see if any new clients want to connect
+--]]
+local function check_new_clients()
+ while sock_listen:pollin(0) do
+ local sock = sock_listen:accept()
+ if not sock then
+ return
+ end
+ -- non-blocking for header read
+ sock:set_blocking(false)
+ -- find free client slot
+ for i = 1, #clients+1 do
+ if clients[i] == nil then
+ local idx = i
+ local client = Client(sock, idx)
+ DEBUG(string.format("%u: New client", idx))
+ clients[idx] = client
+ end
+ end
+ end
+end
+
+--[[
+ check for client activity
+--]]
+local function check_clients()
+ for idx,client in ipairs(clients) do
+ if not client.closed then
+ client.update()
+ end
+ if client.closed then
+ table.remove(clients,idx)
+ end
+ end
+end
+
+function web.update()
+ check_new_clients()
+ check_clients()
+end
+
+local sections = {}
+
+function web.add_board_status(field, func)
+ table.insert(user_board_status, {field=field, func=func})
+end
+
+function custom_board_status()
+ -- run through the user board status functions
+ local ret = ""
+ for _,board_status in pairs(user_board_status) do
+ run = "return tostring(" .. board_status.func .. ")"
+ ret = ret .. string.format("
%s
%s
\n", board_status.field, web.evaluate(run))
+ end
+ return ret
+end
+
+function web.Sections()
+ local self = {}
+ local _section_table = {}
+
+ function self.add_to_table(section_table, section)
+ for _,v in ipairs(section_table) do
+ if v.section == section then
+ return
+ end
+ end
+ table.insert(section_table, {section=section, values={}})
+ end
+
+ function self.add_split_field_to_section_table(section_table, fields, fmt, values)
+ if #fields == 0 then
+ return
+ end
+
+ local section = fields[1]
+ if #fields > 2 then
+ for i,v in ipairs(section_table) do
+ if v.section == section then
+ -- this is an existing section
+ self.add_split_field_to_section_table(v.values, {table.unpack(fields, 2)}, fmt, values)
+ return
+ end
+ end
+ -- this is a new section
+ table.insert(section_table, {section=section, values={}})
+ self.add_split_field_to_section_table(section_table[#section_table].values, {table.unpack(fields, 2)}, fmt, values)
+ return
+ else
+ for i,v in ipairs(section_table) do
+ if v.section == section then
+ -- this is an existing section
+ local field = {field=fields[2], fmt=fmt, values=values}
+ table.insert(v.values, field)
+ return
+ end
+ end
+ -- this is a new section
+ table.insert(section_table, {section=section, values={}})
+ self.add_field_to_table(section_table[#section_table].values, fields[2], fmt, values)
+ return
+ end
+ end
+
+ function self.add(section)
+ self.add_to_table(_section_table, section)
+ return self
+ end
+
+ function self.add_field(field, fmt, values)
+ local fields = split(field, "[^:]+")
+ self.add_section_field_to_table(_section_table, field, fmt, values, 0)
+ return self
+ end
+
+ return self
+end
+
+function populate_sections()
+ local ret = ""
+ for _,section in ipairs(sections) do
+ ret = ret .. string.format("\n
\n%s\n
\n", section.title, section.content)
+ end
+ return ret
+end
+
+return web
diff --git a/libraries/AP_Scripting/modules/webserver.md b/libraries/AP_Scripting/modules/webserver.md
new file mode 100644
index 0000000000000..fa45efe780b46
--- /dev/null
+++ b/libraries/AP_Scripting/modules/webserver.md
@@ -0,0 +1,91 @@
+# Web Server Application
+
+This implements a web server for boards that have networking support.
+
+# Parameters
+
+The web server has a small number of parameters
+
+## WEB_ENABLE
+
+This must be set to 1 to enable the web server
+
+## WEB_BIND_PORT
+
+This sets the network port to use for the server. It defaults to 8080
+
+## WEB_DEBUG
+
+This enables verbose debugging
+
+## WEB_BLOCK_SIZE
+
+This sets the block size for network and file read/write
+operations. Setting a larger value can increase performance at the
+cost of more memory
+
+## WEB_TIMEOUT
+
+This sets the timeout in seconds for inactive client connections.
+
+# Operation
+
+By default the web server serves the root of your microSD card. You
+can include html, javascript (*.js), image files etc on your microSD
+to create a full web server with any structure you want.
+
+## Server Side Scripting
+
+The web server supports embedding lua script elements inside html
+files for files with a filename of *.shtml. Here is an example:
+
+```
+
+
+
+
+
+
Server Side Scripting Test
+
+
Roll
Pitch
Yaw
+
+
+
+
+
+
+
+
+```
+In this example we are using two forms of embedded lua scripts. The
+first form starts with "