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 6c4bbed94..bc2d96dfa 100644 Binary files a/tasmota/berry/gpio_viewer/gpioviewer.bec and b/tasmota/berry/gpio_viewer/gpioviewer.bec differ diff --git a/tasmota/berry/gpio_viewer/webserver_async.be b/tasmota/berry/gpio_viewer/webserver_async.be index 61d997274..640f79d30 100644 --- a/tasmota/berry/gpio_viewer/webserver_async.be +++ b/tasmota/berry/gpio_viewer/webserver_async.be @@ -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"

Hello world {i}

")