mirror of
https://github.com/arendst/Tasmota.git
synced 2025-07-25 19:56:30 +00:00
GPIO viewer in Berry initial version using async webserver (#20416)
This commit is contained in:
parent
bd9a99caff
commit
08a769a720
@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Berry `gpio.read_pwm` and `gpio.read_pwm_resolution`
|
- Berry `gpio.read_pwm` and `gpio.read_pwm_resolution`
|
||||||
- Berry `gpio.get_pin_type` and `gpio.ger_pin_type_index`
|
- Berry `gpio.get_pin_type` and `gpio.ger_pin_type_index`
|
||||||
- Berry `gpio.read_pwm` and `gpio.read_pwm_resolution` (#20414)
|
- Berry `gpio.read_pwm` and `gpio.read_pwm_resolution` (#20414)
|
||||||
|
- GPIO viewer in Berry initial version using async webserver
|
||||||
|
|
||||||
### Breaking Changed
|
### Breaking Changed
|
||||||
|
|
||||||
|
BIN
tasmota/berry/gpio_viewer/gpioviewer.bec
Normal file
BIN
tasmota/berry/gpio_viewer/gpioviewer.bec
Normal file
Binary file not shown.
477
tasmota/berry/gpio_viewer/webserver_async.be
Normal file
477
tasmota/berry/gpio_viewer/webserver_async.be
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
#
|
||||||
|
# webserber_async.be - implements a generic async non-blocking HTTP server
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 Stephan Hadinger & Theo Arends
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# restrictions for now:
|
||||||
|
#
|
||||||
|
# Listen to all interfaces
|
||||||
|
# - GET only
|
||||||
|
# - no HTTPS
|
||||||
|
# - support for limited headers
|
||||||
|
# - HTTP 1.0 only
|
||||||
|
|
||||||
|
#@ solidify:Webserver_async
|
||||||
|
#@ solidify:Webserver_async_cnx
|
||||||
|
|
||||||
|
class Webserver_async_cnx
|
||||||
|
var server # link to server object
|
||||||
|
var cnx # holds the tcpclientasync instance
|
||||||
|
var fastloop_cb # cb for fastloop
|
||||||
|
var buf_in # incoming buffer
|
||||||
|
var buf_in_offset
|
||||||
|
var buf_out
|
||||||
|
var phase # parsing phase: 0/ status line, 1/ headers, 2/ payload
|
||||||
|
# request
|
||||||
|
var req_verb
|
||||||
|
var req_uri
|
||||||
|
var req_version
|
||||||
|
var header_host
|
||||||
|
# response
|
||||||
|
var resp_headers
|
||||||
|
var resp_version
|
||||||
|
var mode_chunked
|
||||||
|
# conversion
|
||||||
|
static var CODE_TO_STRING = {
|
||||||
|
100: "Continue",
|
||||||
|
200: "OK",
|
||||||
|
204: "No Content",
|
||||||
|
301: "Moved Permanently",
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Payment Required",
|
||||||
|
404: "Not Found",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
501: "Not Implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# init
|
||||||
|
def init(server, cnx)
|
||||||
|
self.server = server
|
||||||
|
self.cnx = cnx
|
||||||
|
self.buf_in = ''
|
||||||
|
self.buf_in_offset = 0
|
||||||
|
self.buf_out = bytes()
|
||||||
|
self.phase = 0
|
||||||
|
# response
|
||||||
|
self.resp_headers = ''
|
||||||
|
self.resp_version = 1 # HTTP 1.1 # TODO
|
||||||
|
self.mode_chunked = 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)
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# test if connected
|
||||||
|
def connected()
|
||||||
|
return self.cnx ? self.cnx.connected() : false
|
||||||
|
end
|
||||||
|
#############################################################
|
||||||
|
# closing web server
|
||||||
|
def close()
|
||||||
|
tasmota.log(f"WEB: closing cnx", 3)
|
||||||
|
if (self.cnx != nil) self.cnx.close() end
|
||||||
|
self.cnx = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# called by fastloop
|
||||||
|
def loop()
|
||||||
|
if self.cnx == nil # marked for deletion
|
||||||
|
# mark as closed with self.cnx == nil
|
||||||
|
tasmota.remove_fast_loop(self.fastloop_cb)
|
||||||
|
self.fastloop_cb = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# any incoming data?
|
||||||
|
var cnx = self.cnx
|
||||||
|
|
||||||
|
if cnx.available() > 0
|
||||||
|
var buf_in_new = cnx.read()
|
||||||
|
if (!self.buf_in)
|
||||||
|
self.buf_in = buf_in_new
|
||||||
|
else
|
||||||
|
self.buf_in += buf_in_new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# parse incoming if any
|
||||||
|
if (self.buf_in)
|
||||||
|
self.parse()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# parse incoming
|
||||||
|
#
|
||||||
|
# pre: self.buf_in is not empty
|
||||||
|
# post: self.buf_in has made progress (smaller or '')
|
||||||
|
def parse()
|
||||||
|
tasmota.log(f"WEB: incoming {bytes().fromstring(self.buf_in).tohex()}", 3)
|
||||||
|
if self.phase == 0
|
||||||
|
self.parse_http_req_line()
|
||||||
|
elif self.phase == 1
|
||||||
|
self.parse_http_headers()
|
||||||
|
elif self.phase == 2
|
||||||
|
self.parse_http_payload()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# parse incoming request
|
||||||
|
#
|
||||||
|
# pre: self.buf_in is not empty
|
||||||
|
# post: self.buf_in has made progress (smaller or '')
|
||||||
|
def parse_http_req_line()
|
||||||
|
var m = global._re_http_srv.match2(self.buf_in, self.buf_in_offset)
|
||||||
|
# Ex: "GET / HTTP/1.1\r\n"
|
||||||
|
if m
|
||||||
|
var offset = m[0]
|
||||||
|
self.req_verb = m[1] # GET/POST...
|
||||||
|
self.req_uri = m[2] # /
|
||||||
|
self.req_version = m[3] # "1.0" or "1.1"
|
||||||
|
self.phase = 1 # proceed to parsing headers
|
||||||
|
self.buf_in = self.buf_in[offset .. ] # remove what we parsed
|
||||||
|
tasmota.log(f"WEB: HTTP verb: {self.req_verb} URI: '{self.req_uri}' Version:{self.req_version}", 3)
|
||||||
|
self.parse_http_headers()
|
||||||
|
elif size(self.buf_in) > 100 # if no match and we still have 100 bytes, then it fails
|
||||||
|
tasmota.log("WEB: error invalid request", 3)
|
||||||
|
self.close()
|
||||||
|
self.buf_in = ''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# parse incoming headers
|
||||||
|
def parse_http_headers()
|
||||||
|
while true
|
||||||
|
# print("parse_http_headers", "self.buf_in_offset=", self.buf_in_offset)
|
||||||
|
var m = global._re_http_srv_header.match2(self.buf_in, self.buf_in_offset)
|
||||||
|
# print("m=", m)
|
||||||
|
# Ex: [32, 'Content-Type', 'application/json']
|
||||||
|
if m
|
||||||
|
self.event_http_header(m[1], m[2])
|
||||||
|
self.buf_in_offset += m[0]
|
||||||
|
else # no more headers
|
||||||
|
var m2 = global._re_http_srv_body.match2(self.buf_in, self.buf_in_offset)
|
||||||
|
if m2
|
||||||
|
# end of headers
|
||||||
|
# we keep \r\n which is used by pattern
|
||||||
|
self.buf_in = self.buf_in[self.buf_in_offset + m2[0] .. ] # truncate
|
||||||
|
self.buf_in_offset = 0
|
||||||
|
|
||||||
|
self.event_http_headers_end() # no more headers
|
||||||
|
self.phase = 2
|
||||||
|
self.parse_http_payload() # continue to parsing payload
|
||||||
|
end
|
||||||
|
if size(self.buf_in) > 1024 # we don't accept a single header larger than 1KB
|
||||||
|
tasmota.log("WEB: error header is bigger than 1KB", 3)
|
||||||
|
self.close()
|
||||||
|
self.buf_in = ''
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
self.close()
|
||||||
|
self.buf_in = ''
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# event_http_header
|
||||||
|
#
|
||||||
|
# Received header
|
||||||
|
def event_http_header(header_key, header_value)
|
||||||
|
tasmota.log(f"WEB: header key '{header_key}' = '{header_value}'")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# event_http_headers_end
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# parse incoming payload (if any)
|
||||||
|
def parse_http_payload()
|
||||||
|
tasmota.log(f"WEB: parsing payload '{bytes().fromstring(self.buf_in).tohex()}'")
|
||||||
|
# dispatch request before parsing payload
|
||||||
|
self.server.dispatch(self, self.req_uri, self.req_verb)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# Responses
|
||||||
|
#############################################################
|
||||||
|
#############################################################
|
||||||
|
# parse incoming payload (if any)
|
||||||
|
def send_header(name, value, first)
|
||||||
|
if first
|
||||||
|
self.resp_headers = f"{name}: {value}\r\n{self.resp_headers}"
|
||||||
|
else
|
||||||
|
self.resp_headers = f"{self.resp_headers}{name}: {value}\r\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send(code, content_type, content)
|
||||||
|
var response = f"HTTP/1.{self.resp_version} {code} {self.code_to_string(code)}\r\n"
|
||||||
|
if (content_type == nil) content_type = "text/html" end
|
||||||
|
self.send_header("Content-Type", content_type, true)
|
||||||
|
|
||||||
|
# force chunked TODO
|
||||||
|
self.send_header("Accept-Ranges", "none")
|
||||||
|
if self.mode_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", "*")
|
||||||
|
# others
|
||||||
|
self.send_header("Connection", "close")
|
||||||
|
|
||||||
|
response += self.resp_headers
|
||||||
|
response += "\r\n"
|
||||||
|
self.resp_headers = nil
|
||||||
|
|
||||||
|
# send
|
||||||
|
self._write(response)
|
||||||
|
|
||||||
|
if (content) self.write(content) end
|
||||||
|
end
|
||||||
|
|
||||||
|
static def code_to_string(code)
|
||||||
|
return _class.CODE_TO_STRING.find(code, "UNKNOWN")
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# async write
|
||||||
|
def write(s)
|
||||||
|
# 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)
|
||||||
|
else
|
||||||
|
self._write(s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# async write
|
||||||
|
def _write(s)
|
||||||
|
self.cnx.write(s) # TODO move to async later
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_stop()
|
||||||
|
self.write('')
|
||||||
|
self.close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Webserver_dispatcher
|
||||||
|
var uri_prefix # prefix string, must start with '/'
|
||||||
|
var verb # verb to match, or nil for ANY
|
||||||
|
var cb_obj
|
||||||
|
var cb_mth
|
||||||
|
|
||||||
|
def init(uri, cb_obj, cb_mth, verb)
|
||||||
|
self.uri_prefix = uri
|
||||||
|
self.cb_obj = cb_obj
|
||||||
|
self.cb_mth = cb_mth
|
||||||
|
self.verb = verb
|
||||||
|
end
|
||||||
|
|
||||||
|
# return true if matched
|
||||||
|
def dispatch(cnx, uri, verb)
|
||||||
|
import string
|
||||||
|
if string.find(uri, self.uri_prefix) == 0
|
||||||
|
var match = false
|
||||||
|
if (self.verb == nil) || (self.verb == verb)
|
||||||
|
# method is valid
|
||||||
|
self.cb_mth(self.cb_obj, cnx, uri, verb)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
var timeout # default timeout for tcp connection
|
||||||
|
var connections # list of active connections
|
||||||
|
# var timeout # timeout in ms
|
||||||
|
# var auth # web authentication string (Basic Auth) or `nil`, in format `user:password` as bade64
|
||||||
|
# var cmd # GET url command
|
||||||
|
var dispatchers
|
||||||
|
|
||||||
|
static var TIMEOUT = 1000 # default timeout: 1000ms
|
||||||
|
static var HTTP_REQ = "^(\\w+) (\\S+) HTTP\\/(\\d\\.\\d)\r\n"
|
||||||
|
static var HTTP_HEADER_REGEX = "([A-Za-z0-9-]+): (.*?)\r\n" # extract a header with its 2 parts
|
||||||
|
static var HTTP_BODY_REGEX = "\r\n" # end of headers
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# init
|
||||||
|
def init(port, timeout)
|
||||||
|
if (timeout == nil) timeout = self.TIMEOUT end
|
||||||
|
self.connections = []
|
||||||
|
self.dispatchers = []
|
||||||
|
self.server = tcpserver(port) # throws an exception if port is not available
|
||||||
|
# TODO what about max_clients ?
|
||||||
|
self.compile_re()
|
||||||
|
# register cb
|
||||||
|
tasmota.add_driver(self)
|
||||||
|
self.fastloop_cb = def () self.loop() end
|
||||||
|
tasmota.add_fast_loop(self.fastloop_cb)
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# compile once for all the regex
|
||||||
|
def compile_re()
|
||||||
|
import re
|
||||||
|
if !global.contains("_re_http_srv")
|
||||||
|
global._re_http_srv = re.compile(self.HTTP_REQ)
|
||||||
|
global._re_http_srv_header = re.compile(self.HTTP_HEADER_REGEX)
|
||||||
|
global._re_http_srv_body = re.compile(self.HTTP_BODY_REGEX)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# closing web server
|
||||||
|
def close()
|
||||||
|
tasmota.remove_driver(self)
|
||||||
|
tasmota.remove_fast_loop(self.fastloop_cb)
|
||||||
|
self.fastloop_cb = nil
|
||||||
|
self.server.close()
|
||||||
|
|
||||||
|
# close all active connections
|
||||||
|
for cnx: self.connections
|
||||||
|
cnx.close()
|
||||||
|
end
|
||||||
|
self.connections = nil # and free memory
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# clean connections
|
||||||
|
#
|
||||||
|
# Remove any connections that is closed or in error
|
||||||
|
def clean_connections()
|
||||||
|
var idx = 0
|
||||||
|
while idx < size(self.connections)
|
||||||
|
var cnx = self.connections[idx]
|
||||||
|
# remove if not connected
|
||||||
|
if !cnx.connected()
|
||||||
|
# tasmota.log("WEB: does not appear to be connected")
|
||||||
|
cnx.close()
|
||||||
|
self.connections.remove(idx)
|
||||||
|
else
|
||||||
|
idx += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# called by fastloop
|
||||||
|
def loop()
|
||||||
|
self.clean_connections()
|
||||||
|
# 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
|
||||||
|
self.connections.push(cnx)
|
||||||
|
tasmota.log(f"WEB: received connection from XXX")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# add to dispatcher
|
||||||
|
def on(prefix, obj, mth, verb)
|
||||||
|
var dispatcher = Webserver_dispatcher(prefix, obj, mth, verb)
|
||||||
|
self.dispatchers.push(dispatcher)
|
||||||
|
end
|
||||||
|
|
||||||
|
#############################################################
|
||||||
|
# add to dispatcher
|
||||||
|
def dispatch(cnx, uri, verb)
|
||||||
|
var idx = 0
|
||||||
|
while idx < size(self.dispatchers)
|
||||||
|
if (self.dispatchers[idx].dispatch(cnx, uri, verb))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
idx += 1
|
||||||
|
end
|
||||||
|
# fallback unsupported request
|
||||||
|
cnx.send(500, "text/plain")
|
||||||
|
cnx.write("Unsupported")
|
||||||
|
cnx.content_stop()
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
#- Test
|
||||||
|
|
||||||
|
var web = Webserver_async(888)
|
||||||
|
|
||||||
|
def send_more(cnx, i)
|
||||||
|
cnx.write(f"<p>Hello world {i}</p>")
|
||||||
|
if i < 10
|
||||||
|
tasmota.set_timer(1000, def () send_more(cnx, i+1) end)
|
||||||
|
else
|
||||||
|
cnx.content_stop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def f(obj, cnx, uri, verb)
|
||||||
|
cnx.send(200, "text/html")
|
||||||
|
cnx.write("<html><body>")
|
||||||
|
send_more(cnx, 0)
|
||||||
|
# cnx.write("Hello world")
|
||||||
|
# cnx.content_stop()
|
||||||
|
end
|
||||||
|
|
||||||
|
web.on("/hello", nil, f)
|
||||||
|
|
||||||
|
-#
|
212
tasmota/berry/gpio_viewer/webserver_gpioviewer.be
Normal file
212
tasmota/berry/gpio_viewer/webserver_gpioviewer.be
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
#
|
||||||
|
# webserber_gpioviewer.be - implements a generic async non-blocking HTTP server
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 Stephan Hadinger & Theo Arends
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
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
|
||||||
|
var free_space
|
||||||
|
var pin_actual # actual value
|
||||||
|
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
|
||||||
|
|
||||||
|
static var TYPE_DIGITAL = 0
|
||||||
|
static var TYPE_PWM = 1
|
||||||
|
static var TYPE_ANALOG = 2
|
||||||
|
|
||||||
|
static var SAMPLING = 100
|
||||||
|
static var GPIO_RELEASE = "1.0.7"
|
||||||
|
static var HTML_HEAD =
|
||||||
|
"<!DOCTYPE HTML><html><head><title>ESP32 GPIO State</title>"
|
||||||
|
"<base href ='https://thelastoutpostworkshop.github.io/microcontroller_devkit/gpio_viewer/assets/'>"
|
||||||
|
"<link id='defaultStyleSheet' rel='stylesheet' href=''>"
|
||||||
|
"<link id='boardStyleSheet' rel='stylesheet' href=''>"
|
||||||
|
"<link rel='icon' href='favicon.ico' type='image/x-icon'>"
|
||||||
|
|
||||||
|
"<script src='script/webSocket.js'></script>"
|
||||||
|
"<script src='script/boardSwitcher.js'></script>"
|
||||||
|
"</head>"
|
||||||
|
static var HTML_BODY =
|
||||||
|
"<body><div class='grid-container'>\n"
|
||||||
|
"<header class='header'>"
|
||||||
|
"</header>"
|
||||||
|
# Image
|
||||||
|
"<div class='image-container'>\n"
|
||||||
|
"<div id='imageWrapper' class='image-wrapper'>"
|
||||||
|
"<img id='boardImage' src='' alt='Board Image'>\n"
|
||||||
|
"<div id='indicators'></div>"
|
||||||
|
"</div></div></div>"
|
||||||
|
static var HTML_SCRIPT =
|
||||||
|
# Append the script variables
|
||||||
|
"<script>var serverPort = %i;</script>"
|
||||||
|
"<script>var source = new EventSource('http://%s:%i/events');</script>"
|
||||||
|
"<script>var ip = '%s';</script>"
|
||||||
|
"<script>var sampling_interval = '%i';</script>"
|
||||||
|
"<script>var freeSketchSpace = '%i';</script>"
|
||||||
|
"</body></html>"
|
||||||
|
|
||||||
|
def init(port)
|
||||||
|
self.web = Webserver_async(5555)
|
||||||
|
self.sampling_interval = self.SAMPLING
|
||||||
|
self.free_space = 500
|
||||||
|
|
||||||
|
# pins
|
||||||
|
import gpio
|
||||||
|
self.pin_actual = []
|
||||||
|
self.pin_actual.resize(gpio.MAX_GPIO) # full of nil
|
||||||
|
self.last_pin_states = []
|
||||||
|
self.last_pin_states.resize(gpio.MAX_GPIO) # full of nil
|
||||||
|
self.new_pin_states = []
|
||||||
|
self.new_pin_states.resize(gpio.MAX_GPIO) # full of nil
|
||||||
|
self.pin_types = []
|
||||||
|
self.pin_types.resize(gpio.MAX_GPIO) # full of nil
|
||||||
|
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
|
||||||
|
def close()
|
||||||
|
self.web.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_index_page(cnx, uri, verb)
|
||||||
|
import string
|
||||||
|
|
||||||
|
cnx.send(200, "text/html")
|
||||||
|
cnx.write(self.HTML_HEAD)
|
||||||
|
cnx.write(self.HTML_BODY)
|
||||||
|
|
||||||
|
var host = cnx.header_host
|
||||||
|
var host_split = string.split(host, ':') # need to make it stronger
|
||||||
|
var ip = host_split[0]
|
||||||
|
var port = 80
|
||||||
|
if size(host_split) > 1
|
||||||
|
port = int(host_split[1])
|
||||||
|
end
|
||||||
|
|
||||||
|
var html = format(self.HTML_SCRIPT, port, ip, port, ip, self.sampling_interval, self.free_space)
|
||||||
|
cnx.write(html)
|
||||||
|
cnx.content_stop()
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_release_page(cnx, uri, verb)
|
||||||
|
var release = f'{{"release":"{self.GPIO_RELEASE}"}}'
|
||||||
|
cnx.send(200, "application/json", release)
|
||||||
|
cnx.content_stop()
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_events_page(cnx, uri, verb)
|
||||||
|
cnx.set_mode_chunked(false) # no chunking since we use EventSource
|
||||||
|
cnx.send(200, "text/event-stream")
|
||||||
|
|
||||||
|
self.send_events_tick(cnx)
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_events_tick(cnx)
|
||||||
|
import gpio
|
||||||
|
var max_gpio = gpio.MAX_GPIO
|
||||||
|
var msg = "{"
|
||||||
|
var dirty = false
|
||||||
|
var pin = 0
|
||||||
|
self.read_states()
|
||||||
|
|
||||||
|
while pin < max_gpio
|
||||||
|
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]}}}'
|
||||||
|
dirty = true
|
||||||
|
|
||||||
|
self.last_pin_states[pin] = val
|
||||||
|
end
|
||||||
|
pin += 1
|
||||||
|
end
|
||||||
|
msg += "}"
|
||||||
|
|
||||||
|
if dirty
|
||||||
|
# prepare payload
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
|
||||||
|
# send free heap
|
||||||
|
var payload = f"id:{tasmota.millis()}\r\n"
|
||||||
|
"event:free_heap\r\n"
|
||||||
|
"data:{tasmota.memory().find('heap_free', 0)}\r\n\r\n"
|
||||||
|
cnx.write(payload)
|
||||||
|
|
||||||
|
tasmota.set_timer(self.sampling_interval, def () self.send_events_tick(cnx) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
# read all GPIO values, store in `pin_actual` and `new_pin_states`
|
||||||
|
def read_states()
|
||||||
|
import gpio
|
||||||
|
var max_gpio = gpio.MAX_GPIO
|
||||||
|
var pin = 0
|
||||||
|
while pin < max_gpio
|
||||||
|
# check if PWM
|
||||||
|
var pwm_resolution = gpio.read_pwm_resolution(pin)
|
||||||
|
if (pwm_resolution > 0)
|
||||||
|
var pwm_val = gpio.read_pwm(pin)
|
||||||
|
var pwm_state = tasmota.scale_uint(pwm_val, 0, pwm_resolution, 0, 255) # bring back to 0..255
|
||||||
|
self.pin_actual[pin] = pwm_val
|
||||||
|
self.new_pin_states[pin] = pwm_state
|
||||||
|
self.pin_types[pin] = self.TYPE_PWM
|
||||||
|
elif gpio.get_pin_type(pin) > 0
|
||||||
|
# digital read
|
||||||
|
var digital_val = gpio.digital_read(pin) # returns 0 or 1
|
||||||
|
self.pin_actual[pin] = digital_val
|
||||||
|
self.new_pin_states[pin] = digital_val ? 256 : 0
|
||||||
|
self.pin_types[pin] = self.TYPE_DIGITAL
|
||||||
|
else
|
||||||
|
self.pin_actual[pin] = nil
|
||||||
|
self.new_pin_states[pin] = nil
|
||||||
|
self.pin_types[pin] = self.TYPE_DIGITAL
|
||||||
|
end
|
||||||
|
pin += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
gpio_viewer.GPIO_viewer = GPIO_viewer
|
||||||
|
|
||||||
|
if tasmota
|
||||||
|
var gpio_viewer = GPIO_viewer(5555)
|
||||||
|
end
|
||||||
|
|
||||||
|
return gpio_viewer
|
||||||
|
|
||||||
|
#- Test
|
||||||
|
|
||||||
|
var gpio_viewer = GPIO_viewer(5555)
|
||||||
|
|
||||||
|
-#
|
Loading…
x
Reference in New Issue
Block a user