Berry GPIO Viewer fixes (#20423)

This commit is contained in:
s-hadinger 2024-01-07 19:03:56 +01:00 committed by GitHub
parent 5c521d300a
commit f0d0cccee6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 166 additions and 54 deletions

View File

@ -19,10 +19,6 @@
var gpio_viewer = module('gpio_viewer')
gpio_viewer.Webserver_async_cnx = Webserver_async_cnx
gpio_viewer.Webserver_dispatcher = Webserver_dispatcher
gpio_viewer.Webserver_async = Webserver_async
class GPIO_viewer
var web
var sampling_interval
@ -30,6 +26,7 @@ class GPIO_viewer
var last_pin_states # state converted to 0..255
var new_pin_states # get a snapshot of newest values
var pin_types # array of types
var payload1, payload2 # temporary object bytes() to avoid reallocation
static var TYPE_DIGITAL = 0
static var TYPE_PWM = 1
@ -67,8 +64,10 @@ class GPIO_viewer
"</body></html>"
def init(port)
self.web = Webserver_async(5555)
self.web = webserver_async(5555)
self.sampling_interval = self.SAMPLING
self.payload1 = bytes(100) # reserve 100 bytes by default
self.payload2 = bytes(100) # reserve 100 bytes by default
# pins
import gpio
@ -81,6 +80,8 @@ class GPIO_viewer
self.pin_types = []
self.pin_types.resize(gpio.MAX_GPIO) # full of nil
self.web.set_chunked(true)
self.web.set_cors(true)
self.web.on("/release", self, self.send_release_page)
self.web.on("/events", self, self.send_events_page)
self.web.on("/", self, self.send_index_page)
@ -118,7 +119,7 @@ class GPIO_viewer
end
def send_events_page(cnx, uri, verb)
cnx.set_mode_chunked(false) # no chunking since we use EventSource
cnx.set_chunked(false) # no chunking since we use EventSource
cnx.send(200, "text/event-stream")
self.send_events_tick(cnx)
@ -127,7 +128,9 @@ class GPIO_viewer
def send_events_tick(cnx)
import gpio
var max_gpio = gpio.MAX_GPIO
var msg = "{"
var payload1 = self.payload1
payload1.clear()
payload1 .. '{'
var dirty = false
var pin = 0
self.read_states()
@ -136,24 +139,44 @@ class GPIO_viewer
var prev = self.last_pin_states[pin]
var val = self.new_pin_states[pin]
if (prev != val) || (val != nil) # TODO for now send everything every time
if dirty msg += "," end
msg += f'"{pin}":{{"s":{val},"v":{prev},"t":{self.pin_types[pin]}}}'
if dirty
# msg += ","
payload1 .. ","
end
payload1 .. '"'
payload1 .. str(pin)
payload1 .. '":{"s":'
payload1 .. str(val)
payload1 .. ',"v":'
payload1 .. str(self.pin_actual[pin])
payload1 .. ',"t":'
payload1 .. str(self.pin_types[pin])
payload1 .. '}'
# msg += f'"{pin}":{{"s":{val},"v":{prev},"t":{self.pin_types[pin]}}}}'
dirty = true
self.last_pin_states[pin] = val
end
pin += 1
end
msg += "}"
payload1 .. '}'
# msg += "}"
if dirty
# prepare payload
var payload = f"id:{tasmota.millis()}\r\n"
"event:gpio-state\r\n"
"data:{msg}\r\n\r\n"
var payload2 = self.payload2
payload2.clear()
payload2 .. 'id:'
payload2 .. str(tasmota.millis())
payload2 .. "\r\nevent:gpio-state\r\ndata:"
payload2 .. payload1
payload2 .. "\r\n\r\n"
# var payload = f"id:{tasmota.millis()}\r\n"
# "event:gpio-state\r\n"
# "data:{msg}\r\n\r\n"
# tasmota.log(f"GPV: sending '{msg}'", 3)
cnx.write(payload)
cnx.write(payload2)
end
# send free heap

View File

@ -25,12 +25,13 @@
# - support for limited headers
# - HTTP 1.0 only
#@ solidify:Webserver_async
#@ solidify:webserver_async
#@ solidify:Webserver_async_cnx
class Webserver_async_cnx
var server # link to server object
var cnx # holds the tcpclientasync instance
var close_after_send # if true, close after sending
var fastloop_cb # cb for fastloop
var buf_in # incoming buffer
var buf_in_offset
@ -44,7 +45,10 @@ class Webserver_async_cnx
# response
var resp_headers
var resp_version
var mode_chunked
var chunked # if true enable chunked encoding (default true)
var cors # if true send CORS headers (default true)
# bytes objects to be reused
var payload1
# conversion
static var CODE_TO_STRING = {
100: "Continue",
@ -68,17 +72,25 @@ class Webserver_async_cnx
self.buf_in_offset = 0
self.buf_out = bytes()
self.phase = 0
# util
self.payload1 = bytes()
self.close_after_send = false
# response
self.resp_headers = ''
self.resp_version = 1 # HTTP 1.1 # TODO
self.mode_chunked = true
self.chunked = true
self.cors = true
# register cb
self.fastloop_cb = def () self.loop() end
tasmota.add_fast_loop(self.fastloop_cb)
end
def set_mode_chunked(mode_chunked)
self.mode_chunked = bool(mode_chunked)
def set_chunked(chunked)
self.chunked = bool(chunked)
end
def set_cors(cors)
self.cors = bool(cors)
end
#############################################################
@ -86,6 +98,28 @@ class Webserver_async_cnx
def connected()
return self.cnx ? self.cnx.connected() : false
end
#############################################################
# test if out buffer is empty, meaning all was sent
def buf_out_empty()
return size(self.buf_out) == 0
end
#############################################################
# write bytes or string
#
# v must be bytes()
def _write(v)
if (size(v) == 0) return end
var buf_out = self.buf_out
var buf_out_sz = size(buf_out)
buf_out.resize(buf_out_sz + size(v))
buf_out.setbytes(buf_out_sz, v)
self._send() # try sending this now
end
#############################################################
# closing web server
def close()
@ -104,9 +138,12 @@ class Webserver_async_cnx
return
end
# any incoming data?
var cnx = self.cnx
self._send() # try sending outgoing
var cnx = self.cnx
if (cnx == nil) return end # it's possible that it was closed after _send()
# any incoming data?
if cnx.available() > 0
var buf_in_new = cnx.read()
if (!self.buf_in)
@ -122,6 +159,39 @@ class Webserver_async_cnx
end
end
#############################################################
# try sending what we can immediately
def _send()
# any data waiting to go out?
var cnx = self.cnx
if (cnx == nil) return end
var buf_out = self.buf_out
if size(buf_out) > 0
if cnx.listening()
var sent = cnx.write(buf_out)
if sent > 0
# we did sent something
if sent >= size(buf_out)
# all sent
self.buf_out.clear()
else
# remove the first bytes already sent
self.buf_out.setbytes(0, buf_out, sent)
self.byf_out.resize(size(buf_out) - sent)
end
end
end
else
# empty buffer, do the cleaning
self.buf_out.clear()
self.buf_in_offset = 0
if self.close_after_send
self.close()
end
end
end
#############################################################
# parse incoming
#
@ -209,13 +279,6 @@ class Webserver_async_cnx
if (header_key == "Host")
self.header_host = header_value
end
# import string
# header_key = string.tolower(header_key)
# header_value = string.tolower(header_value)
# print("header=", header_key, header_value)
# if header_key == 'transfer-encoding' && string.tolower(header_value) == 'chunked'
# self.is_chunked = true
# end
end
#############################################################
@ -223,12 +286,6 @@ class Webserver_async_cnx
#
# All headers are received
def event_http_headers_end()
# print("event_http_headers_end")
# truncate to save space
# if self.buf_in_offset > 0
# self.buf_in = self.buf_in[self.buf_in_offset .. ]
# self.buf_in_offset = 0
# end
end
#############################################################
@ -243,7 +300,6 @@ class Webserver_async_cnx
#############################################################
# Responses
#############################################################
#############################################################
# parse incoming payload (if any)
def send_header(name, value, first)
if first
@ -260,13 +316,15 @@ class Webserver_async_cnx
# force chunked TODO
self.send_header("Accept-Ranges", "none")
if self.mode_chunked
if self.chunked
self.send_header("Transfer-Encoding", "chunked")
end
# cors
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "*")
self.send_header("Access-Control-Allow-Headers", "*")
if self.cors
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "*")
self.send_header("Access-Control-Allow-Headers", "*")
end
# others
self.send_header("Connection", "close")
@ -275,7 +333,7 @@ class Webserver_async_cnx
self.resp_headers = nil
# send
self._write(response)
self.write_raw(response)
if (content) self.write(content) end
end
@ -286,26 +344,43 @@ class Webserver_async_cnx
#############################################################
# async write
def write(s)
def write(v)
if type(v) == 'string' # if string, convert to bytes
v = bytes().fromstring(v)
end
# use chunk encoding
if self.mode_chunked
var chunk = f"{size(s):X}\r\n{s}\r\n"
tasmota.log(f"WEB: sending chunk '{bytes().fromstring(chunk).tohex()}'")
self._write(chunk)
if self.chunked
var payload1 = self.payload1
payload1.clear()
payload1 .. f"{size(v):X}\r\n"
payload1 .. v
payload1 .. "\r\n"
# var chunk = f"{size(v):X}\r\n{v}\r\n"
# tasmota.log(f"WEB: sending chunk '{payload1.tohex()}'")
self._write(payload1)
else
self._write(s)
self._write(v)
end
end
#############################################################
# async write
def _write(s)
self.cnx.write(s) # TODO move to async later
def write_raw(v)
if (size(v) == 0) return end
if type(v) == 'string' # if string, convert to bytes
v = bytes().fromstring(v)
end
self._write(v)
end
def content_stop()
self.write('')
self.close()
self.close_after_send = true
end
end
@ -337,7 +412,7 @@ class Webserver_dispatcher
end
end
class Webserver_async
class webserver_async
var local_port # listening port, 80 is already used by Tasmota
var server # instance of `tcpserver`
var fastloop_cb # closure used by fastloop
@ -347,6 +422,9 @@ class Webserver_async
# var auth # web authentication string (Basic Auth) or `nil`, in format `user:password` as bade64
# var cmd # GET url command
var dispatchers
# copied in each connection
var chunked # if true enable chunked encoding (default true)
var cors # if true send CORS headers (default true)
static var TIMEOUT = 1000 # default timeout: 1000ms
static var HTTP_REQ = "^(\\w+) (\\S+) HTTP\\/(\\d\\.\\d)\r\n"
@ -379,6 +457,14 @@ class Webserver_async
end
end
def set_chunked(chunked)
self.chunked = bool(chunked)
end
def set_cors(cors)
self.cors = bool(cors)
end
#############################################################
# closing web server
def close()
@ -420,9 +506,10 @@ class Webserver_async
# check if any incoming connection
while self.server.hasclient()
# retrieve new client
var cnx = Webserver_async_cnx(self, self.server.accept()) # TODO move to self.server.acceptasync
var cnx = Webserver_async_cnx(self, self.server.acceptasync())
cnx.set_chunked(self.chunked)
cnx.set_cors(self.cors)
self.connections.push(cnx)
tasmota.log(f"WEB: received connection from XXX")
end
end
@ -451,9 +538,11 @@ class Webserver_async
end
#return webserver_async
#- Test
var web = Webserver_async(888)
var web = webserver_async(888)
def send_more(cnx, i)
cnx.write(f"<p>Hello world {i}</p>")