From f0d0cccee612e08b5fb72c47dc8dc25ef35d61c1 Mon Sep 17 00:00:00 2001 From: s-hadinger <49731213+s-hadinger@users.noreply.github.com> Date: Sun, 7 Jan 2024 19:03:56 +0100 Subject: [PATCH] Berry GPIO Viewer fixes (#20423) --- tasmota/berry/gpio_viewer/gpioviewer.be | 51 ++++-- tasmota/berry/gpio_viewer/gpioviewer.bec | Bin 10867 -> 12122 bytes tasmota/berry/gpio_viewer/webserver_async.be | 169 ++++++++++++++----- 3 files changed, 166 insertions(+), 54 deletions(-) diff --git a/tasmota/berry/gpio_viewer/gpioviewer.be b/tasmota/berry/gpio_viewer/gpioviewer.be index ae379f52d..c0f99af67 100644 --- a/tasmota/berry/gpio_viewer/gpioviewer.be +++ b/tasmota/berry/gpio_viewer/gpioviewer.be @@ -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 "" 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 diff --git a/tasmota/berry/gpio_viewer/gpioviewer.bec b/tasmota/berry/gpio_viewer/gpioviewer.bec index 6c4bbed9480ae7b0d76ec2c97370c472691df6e8..bc2d96dfa4ea7b6465d613540849e0feb2b39ff0 100644 GIT binary patch delta 3050 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 PA<2F`JlB{y65#& 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"

Hello world {i}

")