diff --git a/PICOWEB.md b/PICOWEB.md
new file mode 100644
index 0000000..03a83cf
--- /dev/null
+++ b/PICOWEB.md
@@ -0,0 +1,94 @@
+# Running Picoweb on hardware devices
+
+This has regularly caused dificulty on the forum.
+
+The target hardware is assumed to be running official MicroPython firmware.
+
+This repo aims to clarify the installation process. Paul Sokolovsky's Picoweb
+code is unchanged except for the name of the logging library. The demos are
+trivially changed to use IP '0.0.0.0' and port 80.
+
+To install on a hardware platform such as ESP32 or Pyboard D it is necessary to
+copy this directory and its contents (including subdirectories) to the target.
+If using `rshell` on an ESP32 change to this directory, at the `rshell` prompt
+issue
+
+```
+/my/tree/PicoWeb> rsync . /pyboard
+```
+This may take some time.
+
+At the REPL connect to the network and determine your IP address
+```
+>>> import network
+>>> w = network.WLAN()
+>>> w.ifconfig()
+```
+
+issue
+```
+>>> from picoweb import example_webapp
+```
+
+or
+```
+>>> from picoweb import example_webapp2
+```
+
+Then point your browser at the IP address determined above.
+
+Note that some platforms will have `uasyncio` installed as frozen bytecode: in
+such cases there is no need to copy the `uasyncio` subdirectory (if you do, it
+will be ignored).
+
+# ESP8266
+
+RAM limitations require the use of frozen bytecode, and getting the examples
+running is a little more involved. Create a directory on your PC and copy the
+contents of this directory to it. Then add the files `inisetup.py`, `_boot.py`
+and `flashbdev.py` which may be found in the MicroPython source tree under
+`ports/esp8266/modules`. You may also want to add a custom connect module to
+simplify connection to your WiFi. Then build the firmware. The script I used
+was
+```bash
+#! /bin/bash
+
+# Test picoweb on ESP8266
+
+DIRECTORY='/home/adminpete/temp/picoweb'
+
+cd /mnt/qnap2/data/Projects/MicroPython/micropython/ports/esp8266
+
+make clean
+esptool.py --port /dev/ttyUSB0 erase_flash
+
+if make -j 8 FROZEN_MPY_DIR=$DIRECTORY
+then
+ sleep 1
+ esptool.py --port /dev/ttyUSB0 --baud 115200 write_flash --flash_size=detect -fm dio 0 build/firmware-combined.bin
+ sleep 4
+ rshell -p /dev/ttyUSB0 --buffer-size=30 --editor nano
+else
+ echo Build failure
+fi
+```
+For the demos you will need to make the `example_webapp.py` source file and
+`squares.tpl` accessible in the filesystem. The following `rshell` commands,
+executed from this directory or the one created above, will make these
+available.
+```
+path/to/repo> mkdir /pyboard/picoweb
+path/to/repo> mkdir /pyboard/picoweb/templates
+path/to/repo> cp picoweb/example_webapp.py /pyboard/picoweb/
+path/to/repo> cp picoweb/templates/squares.tpl /pyboard/picoweb/templates/
+```
+
+
+# Documentation and further examples
+
+See [the PicoWeb docs](https://github.com/pfalcon/picoweb)
+
+Note that to run under official MicroPython, references to `ulogging` in these
+demos must be changed to `logging`. You may also want to change IP and port as
+above.
+
diff --git a/PicoWeb/logging.py b/PicoWeb/logging.py
new file mode 100644
index 0000000..cea2de0
--- /dev/null
+++ b/PicoWeb/logging.py
@@ -0,0 +1,94 @@
+import sys
+
+CRITICAL = 50
+ERROR = 40
+WARNING = 30
+INFO = 20
+DEBUG = 10
+NOTSET = 0
+
+_level_dict = {
+ CRITICAL: "CRIT",
+ ERROR: "ERROR",
+ WARNING: "WARN",
+ INFO: "INFO",
+ DEBUG: "DEBUG",
+}
+
+_stream = sys.stderr
+
+class Logger:
+
+ level = NOTSET
+
+ def __init__(self, name):
+ self.name = name
+
+ def _level_str(self, level):
+ l = _level_dict.get(level)
+ if l is not None:
+ return l
+ return "LVL%s" % level
+
+ def setLevel(self, level):
+ self.level = level
+
+ def isEnabledFor(self, level):
+ return level >= (self.level or _level)
+
+ def log(self, level, msg, *args):
+ if level >= (self.level or _level):
+ _stream.write("%s:%s:" % (self._level_str(level), self.name))
+ if not args:
+ print(msg, file=_stream)
+ else:
+ print(msg % args, file=_stream)
+
+ def debug(self, msg, *args):
+ self.log(DEBUG, msg, *args)
+
+ def info(self, msg, *args):
+ self.log(INFO, msg, *args)
+
+ def warning(self, msg, *args):
+ self.log(WARNING, msg, *args)
+
+ def error(self, msg, *args):
+ self.log(ERROR, msg, *args)
+
+ def critical(self, msg, *args):
+ self.log(CRITICAL, msg, *args)
+
+ def exc(self, e, msg, *args):
+ self.log(ERROR, msg, *args)
+ sys.print_exception(e, _stream)
+
+ def exception(self, msg, *args):
+ self.exc(sys.exc_info()[1], msg, *args)
+
+
+_level = INFO
+_loggers = {}
+
+def getLogger(name):
+ if name in _loggers:
+ return _loggers[name]
+ l = Logger(name)
+ _loggers[name] = l
+ return l
+
+def info(msg, *args):
+ getLogger(None).info(msg, *args)
+
+def debug(msg, *args):
+ getLogger(None).debug(msg, *args)
+
+def basicConfig(level=INFO, filename=None, stream=None, format=None):
+ global _level, _stream
+ _level = level
+ if stream:
+ _stream = stream
+ if filename is not None:
+ print("logging.basicConfig: filename arg is not supported")
+ if format is not None:
+ print("logging.basicConfig: format arg is not supported")
diff --git a/PicoWeb/picoweb/__init__.py b/PicoWeb/picoweb/__init__.py
new file mode 100644
index 0000000..6708069
--- /dev/null
+++ b/PicoWeb/picoweb/__init__.py
@@ -0,0 +1,300 @@
+# Picoweb web pico-framework for MicroPython
+# Copyright (c) 2014-2018 Paul Sokolovsky
+# SPDX-License-Identifier: MIT
+import sys
+import gc
+import micropython
+import utime
+import uio
+import ure as re
+import uerrno
+import uasyncio as asyncio
+import pkg_resources
+
+from .utils import parse_qs
+
+
+def get_mime_type(fname):
+ # Provide minimal detection of important file
+ # types to keep browsers happy
+ if fname.endswith(".html"):
+ return "text/html"
+ if fname.endswith(".css"):
+ return "text/css"
+ if fname.endswith(".png") or fname.endswith(".jpg"):
+ return "image"
+ return "text/plain"
+
+def sendstream(writer, f):
+ buf = bytearray(64)
+ while True:
+ l = f.readinto(buf)
+ if not l:
+ break
+ yield from writer.awrite(buf, 0, l)
+
+
+def jsonify(writer, dict):
+ import ujson
+ yield from start_response(writer, "application/json")
+ yield from writer.awrite(ujson.dumps(dict))
+
+def start_response(writer, content_type="text/html", status="200", headers=None):
+ yield from writer.awrite("HTTP/1.0 %s NA\r\n" % status)
+ yield from writer.awrite("Content-Type: ")
+ yield from writer.awrite(content_type)
+ if not headers:
+ yield from writer.awrite("\r\n\r\n")
+ return
+ yield from writer.awrite("\r\n")
+ if isinstance(headers, bytes) or isinstance(headers, str):
+ yield from writer.awrite(headers)
+ else:
+ for k, v in headers.items():
+ yield from writer.awrite(k)
+ yield from writer.awrite(": ")
+ yield from writer.awrite(v)
+ yield from writer.awrite("\r\n")
+ yield from writer.awrite("\r\n")
+
+def http_error(writer, status):
+ yield from start_response(writer, status=status)
+ yield from writer.awrite(status)
+
+
+class HTTPRequest:
+
+ def __init__(self):
+ pass
+
+ def read_form_data(self):
+ size = int(self.headers[b"Content-Length"])
+ data = yield from self.reader.read(size)
+ form = parse_qs(data.decode())
+ self.form = form
+
+ def parse_qs(self):
+ form = parse_qs(self.qs)
+ self.form = form
+
+
+class WebApp:
+
+ def __init__(self, pkg, routes=None, serve_static=True):
+ if routes:
+ self.url_map = routes
+ else:
+ self.url_map = []
+ if pkg and pkg != "__main__":
+ self.pkg = pkg.split(".", 1)[0]
+ else:
+ self.pkg = None
+ if serve_static:
+ self.url_map.append((re.compile("^/(static/.+)"), self.handle_static))
+ self.mounts = []
+ self.inited = False
+ # Instantiated lazily
+ self.template_loader = None
+ self.headers_mode = "parse"
+
+ def parse_headers(self, reader):
+ headers = {}
+ while True:
+ l = yield from reader.readline()
+ if l == b"\r\n":
+ break
+ k, v = l.split(b":", 1)
+ headers[k] = v.strip()
+ return headers
+
+ def _handle(self, reader, writer):
+ if self.debug > 1:
+ micropython.mem_info()
+
+ close = True
+ req = None
+ try:
+ request_line = yield from reader.readline()
+ if request_line == b"":
+ if self.debug >= 0:
+ self.log.error("%s: EOF on request start" % reader)
+ yield from writer.aclose()
+ return
+ req = HTTPRequest()
+ # TODO: bytes vs str
+ request_line = request_line.decode()
+ method, path, proto = request_line.split()
+ if self.debug >= 0:
+ self.log.info('%.3f %s %s "%s %s"' % (utime.time(), req, writer, method, path))
+ path = path.split("?", 1)
+ qs = ""
+ if len(path) > 1:
+ qs = path[1]
+ path = path[0]
+
+ #print("================")
+ #print(req, writer)
+ #print(req, (method, path, qs, proto), req.headers)
+
+ # Find which mounted subapp (if any) should handle this request
+ app = self
+ while True:
+ found = False
+ for subapp in app.mounts:
+ root = subapp.url
+ #print(path, "vs", root)
+ if path[:len(root)] == root:
+ app = subapp
+ found = True
+ path = path[len(root):]
+ if not path.startswith("/"):
+ path = "/" + path
+ break
+ if not found:
+ break
+
+ # We initialize apps on demand, when they really get requests
+ if not app.inited:
+ app.init()
+
+ # Find handler to serve this request in app's url_map
+ found = False
+ for e in app.url_map:
+ pattern = e[0]
+ handler = e[1]
+ extra = {}
+ if len(e) > 2:
+ extra = e[2]
+
+ if path == pattern:
+ found = True
+ break
+ elif not isinstance(pattern, str):
+ # Anything which is non-string assumed to be a ducktype
+ # pattern matcher, whose .match() method is called. (Note:
+ # Django uses .search() instead, but .match() is more
+ # efficient and we're not exactly compatible with Django
+ # URL matching anyway.)
+ m = pattern.match(path)
+ if m:
+ req.url_match = m
+ found = True
+ break
+
+ if not found:
+ headers_mode = "skip"
+ else:
+ headers_mode = extra.get("headers", self.headers_mode)
+
+ if headers_mode == "skip":
+ while True:
+ l = yield from reader.readline()
+ if l == b"\r\n":
+ break
+ elif headers_mode == "parse":
+ req.headers = yield from self.parse_headers(reader)
+ else:
+ assert headers_mode == "leave"
+
+ if found:
+ req.method = method
+ req.path = path
+ req.qs = qs
+ req.reader = reader
+ close = yield from handler(req, writer)
+ else:
+ yield from start_response(writer, status="404")
+ yield from writer.awrite("404\r\n")
+ #print(req, "After response write")
+ except Exception as e:
+ if self.debug >= 0:
+ self.log.exc(e, "%.3f %s %s %r" % (utime.time(), req, writer, e))
+
+ if close is not False:
+ yield from writer.aclose()
+ if __debug__ and self.debug > 1:
+ self.log.debug("%.3f %s Finished processing request", utime.time(), req)
+
+ def mount(self, url, app):
+ "Mount a sub-app at the url of current app."
+ # Inspired by Bottle. It might seem that dispatching to
+ # subapps would rather be handled by normal routes, but
+ # arguably, that's less efficient. Taking into account
+ # that paradigmatically there's difference between handing
+ # an action and delegating responisibilities to another
+ # app, Bottle's way was followed.
+ app.url = url
+ self.mounts.append(app)
+
+ def route(self, url, **kwargs):
+ def _route(f):
+ self.url_map.append((url, f, kwargs))
+ return f
+ return _route
+
+ def add_url_rule(self, url, func, **kwargs):
+ # Note: this method skips Flask's "endpoint" argument,
+ # because it's alleged bloat.
+ self.url_map.append((url, func, kwargs))
+
+ def _load_template(self, tmpl_name):
+ if self.template_loader is None:
+ import utemplate.source
+ self.template_loader = utemplate.source.Loader(self.pkg, "templates")
+ return self.template_loader.load(tmpl_name)
+
+ def render_template(self, writer, tmpl_name, args=()):
+ tmpl = self._load_template(tmpl_name)
+ for s in tmpl(*args):
+ yield from writer.awrite(s)
+
+ def render_str(self, tmpl_name, args=()):
+ #TODO: bloat
+ tmpl = self._load_template(tmpl_name)
+ return ''.join(tmpl(*args))
+
+ def sendfile(self, writer, fname, content_type=None, headers=None):
+ if not content_type:
+ content_type = get_mime_type(fname)
+ try:
+ with pkg_resources.resource_stream(self.pkg, fname) as f:
+ yield from start_response(writer, content_type, "200", headers)
+ yield from sendstream(writer, f)
+ except OSError as e:
+ if e.args[0] == uerrno.ENOENT:
+ yield from http_error(writer, "404")
+ else:
+ raise
+
+ def handle_static(self, req, resp):
+ path = req.url_match.group(1)
+ print(path)
+ if ".." in path:
+ yield from http_error(resp, "403")
+ return
+ yield from self.sendfile(resp, path)
+
+ def init(self):
+ """Initialize a web application. This is for overriding by subclasses.
+ This is good place to connect to/initialize a database, for example."""
+ self.inited = True
+
+ def run(self, host="127.0.0.1", port=8081, debug=False, lazy_init=False, log=None):
+ if log is None and debug >= 0:
+ import logging
+ log = logging.getLogger("picoweb")
+ if debug > 0:
+ log.setLevel(logging.DEBUG)
+ self.log = log
+ gc.collect()
+ self.debug = int(debug)
+ self.init()
+ if not lazy_init:
+ for app in self.mounts:
+ app.init()
+ loop = asyncio.get_event_loop()
+ if debug > 0:
+ print("* Running on http://%s:%s/" % (host, port))
+ loop.create_task(asyncio.start_server(self._handle, host, port))
+ loop.run_forever()
+ loop.close()
diff --git a/PicoWeb/picoweb/example_webapp.py b/PicoWeb/picoweb/example_webapp.py
new file mode 100644
index 0000000..ff99b5a
--- /dev/null
+++ b/PicoWeb/picoweb/example_webapp.py
@@ -0,0 +1,55 @@
+#
+# This is a picoweb example showing a centralized web page route
+# specification (classical Django style).
+#
+import ure as re
+import picoweb
+
+
+def index(req, resp):
+ # You can construct an HTTP response completely yourself, having
+ # a full control of headers sent...
+ yield from resp.awrite("HTTP/1.0 200 OK\r\n")
+ yield from resp.awrite("Content-Type: text/html\r\n")
+ yield from resp.awrite("\r\n")
+ yield from resp.awrite("I can show you a table of squares.
")
+ yield from resp.awrite("Or my source.
")
+ yield from resp.awrite("Or enter /iam/Mickey Mouse after the URL for regexp match.")
+
+
+def squares(req, resp):
+ # Or can use a convenience function start_response() (see its source for
+ # extra params it takes).
+ yield from picoweb.start_response(resp)
+ yield from app.render_template(resp, "squares.tpl", (req,))
+
+
+def hello(req, resp):
+ yield from picoweb.start_response(resp)
+ # Here's how you extract matched groups from a regex URI match
+ yield from resp.awrite("Hello " + req.url_match.group(1))
+
+
+ROUTES = [
+ # You can specify exact URI string matches...
+ ("/", index),
+ ("/squares", squares),
+ ("/file", lambda req, resp: (yield from app.sendfile(resp, "example_webapp.py"))),
+ # ... or match using a regex, the match result available as req.url_match
+ # for match group extraction in your view.
+ (re.compile("^/iam/(.+)"), hello),
+]
+
+
+import logging
+logging.basicConfig(level=logging.INFO)
+#logging.basicConfig(level=logging.DEBUG)
+
+app = picoweb.WebApp(__name__, ROUTES)
+# debug values:
+# -1 disable all logging
+# 0 (False) normal logging: requests and errors
+# 1 (True) debug logging
+# 2 extra debug logging
+app.run(debug=1, host='0.0.0.0', port=80)
+
diff --git a/PicoWeb/picoweb/example_webapp2.py b/PicoWeb/picoweb/example_webapp2.py
new file mode 100644
index 0000000..9f58fac
--- /dev/null
+++ b/PicoWeb/picoweb/example_webapp2.py
@@ -0,0 +1,25 @@
+#
+# This is a picoweb example showing a web page route
+# specification using view decorators (Flask style).
+#
+import picoweb
+
+
+app = picoweb.WebApp(__name__)
+
+@app.route("/")
+def index(req, resp):
+ yield from picoweb.start_response(resp)
+ yield from resp.awrite("I can show you a table of squares.")
+
+@app.route("/squares")
+def squares(req, resp):
+ yield from picoweb.start_response(resp)
+ yield from app.render_template(resp, "squares.tpl", (req,))
+
+
+import logging
+logging.basicConfig(level=logging.INFO)
+
+app.run(debug=True, host='0.0.0.0', port=80)
+
diff --git a/PicoWeb/picoweb/templates/squares.tpl b/PicoWeb/picoweb/templates/squares.tpl
new file mode 100644
index 0000000..ca26513
--- /dev/null
+++ b/PicoWeb/picoweb/templates/squares.tpl
@@ -0,0 +1,9 @@
+{% args req %}
+
+Request path: '{{req.path}}'
+
+{% for i in range(5) %}
+ {{i}} | {{"%2d" % i ** 2}} |
+{% endfor %}
+
+
diff --git a/PicoWeb/picoweb/utils.py b/PicoWeb/picoweb/utils.py
new file mode 100644
index 0000000..8943ed8
--- /dev/null
+++ b/PicoWeb/picoweb/utils.py
@@ -0,0 +1,28 @@
+def unquote_plus(s):
+ # TODO: optimize
+ s = s.replace("+", " ")
+ arr = s.split("%")
+ arr2 = [chr(int(x[:2], 16)) + x[2:] for x in arr[1:]]
+ return arr[0] + "".join(arr2)
+
+def parse_qs(s):
+ res = {}
+ if s:
+ pairs = s.split("&")
+ for p in pairs:
+ vals = [unquote_plus(x) for x in p.split("=", 1)]
+ if len(vals) == 1:
+ vals.append(True)
+ old = res.get(vals[0])
+ if old is not None:
+ if not isinstance(old, list):
+ old = [old]
+ res[vals[0]] = old
+ old.append(vals[1])
+ else:
+ res[vals[0]] = vals[1]
+ return res
+
+#print(parse_qs("foo"))
+#print(parse_qs("fo%41o+bar=+++1"))
+#print(parse_qs("foo=1&foo=2"))
diff --git a/PicoWeb/pkg_resources.py b/PicoWeb/pkg_resources.py
new file mode 100644
index 0000000..9ab28e9
--- /dev/null
+++ b/PicoWeb/pkg_resources.py
@@ -0,0 +1,27 @@
+import uio
+
+c = {}
+
+def resource_stream(package, resource):
+ if package not in c:
+ try:
+ if package:
+ p = __import__(package + ".R", None, None, True)
+ else:
+ p = __import__("R")
+ c[package] = p.R
+ except ImportError:
+ if package:
+ p = __import__(package)
+ d = p.__path__
+ else:
+ d = "."
+# if d[0] != "/":
+# import uos
+# d = uos.getcwd() + "/" + d
+ c[package] = d + "/"
+
+ p = c[package]
+ if isinstance(p, dict):
+ return uio.BytesIO(p[resource])
+ return open(p + resource, "rb")
diff --git a/PicoWeb/uasyncio/__init__.py b/PicoWeb/uasyncio/__init__.py
new file mode 100644
index 0000000..41fa572
--- /dev/null
+++ b/PicoWeb/uasyncio/__init__.py
@@ -0,0 +1,258 @@
+import uerrno
+import uselect as select
+import usocket as _socket
+from uasyncio.core import *
+
+
+DEBUG = 0
+log = None
+
+def set_debug(val):
+ global DEBUG, log
+ DEBUG = val
+ if val:
+ import logging
+ log = logging.getLogger("uasyncio")
+
+
+class PollEventLoop(EventLoop):
+
+ def __init__(self, runq_len=16, waitq_len=16):
+ EventLoop.__init__(self, runq_len, waitq_len)
+ self.poller = select.poll()
+ self.objmap = {}
+
+ def add_reader(self, sock, cb, *args):
+ if DEBUG and __debug__:
+ log.debug("add_reader%s", (sock, cb, args))
+ if args:
+ self.poller.register(sock, select.POLLIN)
+ self.objmap[id(sock)] = (cb, args)
+ else:
+ self.poller.register(sock, select.POLLIN)
+ self.objmap[id(sock)] = cb
+
+ def remove_reader(self, sock):
+ if DEBUG and __debug__:
+ log.debug("remove_reader(%s)", sock)
+ self.poller.unregister(sock)
+ del self.objmap[id(sock)]
+
+ def add_writer(self, sock, cb, *args):
+ if DEBUG and __debug__:
+ log.debug("add_writer%s", (sock, cb, args))
+ if args:
+ self.poller.register(sock, select.POLLOUT)
+ self.objmap[id(sock)] = (cb, args)
+ else:
+ self.poller.register(sock, select.POLLOUT)
+ self.objmap[id(sock)] = cb
+
+ def remove_writer(self, sock):
+ if DEBUG and __debug__:
+ log.debug("remove_writer(%s)", sock)
+ try:
+ self.poller.unregister(sock)
+ self.objmap.pop(id(sock), None)
+ except OSError as e:
+ # StreamWriter.awrite() first tries to write to a socket,
+ # and if that succeeds, yield IOWrite may never be called
+ # for that socket, and it will never be added to poller. So,
+ # ignore such error.
+ if e.args[0] != uerrno.ENOENT:
+ raise
+
+ def wait(self, delay):
+ if DEBUG and __debug__:
+ log.debug("poll.wait(%d)", delay)
+ # We need one-shot behavior (second arg of 1 to .poll())
+ res = self.poller.ipoll(delay, 1)
+ #log.debug("poll result: %s", res)
+ # Remove "if res" workaround after
+ # https://github.com/micropython/micropython/issues/2716 fixed.
+ if res:
+ for sock, ev in res:
+ cb = self.objmap[id(sock)]
+ if ev & (select.POLLHUP | select.POLLERR):
+ # These events are returned even if not requested, and
+ # are sticky, i.e. will be returned again and again.
+ # If the caller doesn't do proper error handling and
+ # unregister this sock, we'll busy-loop on it, so we
+ # as well can unregister it now "just in case".
+ self.remove_reader(sock)
+ if DEBUG and __debug__:
+ log.debug("Calling IO callback: %r", cb)
+ if isinstance(cb, tuple):
+ cb[0](*cb[1])
+ else:
+ cb.pend_throw(None)
+ self.call_soon(cb)
+
+
+class StreamReader:
+
+ def __init__(self, polls, ios=None):
+ if ios is None:
+ ios = polls
+ self.polls = polls
+ self.ios = ios
+
+ def read(self, n=-1):
+ while True:
+ yield IORead(self.polls)
+ res = self.ios.read(n)
+ if res is not None:
+ break
+ # This should not happen for real sockets, but can easily
+ # happen for stream wrappers (ssl, websockets, etc.)
+ #log.warn("Empty read")
+ if not res:
+ yield IOReadDone(self.polls)
+ return res
+
+ def readexactly(self, n):
+ buf = b""
+ while n:
+ yield IORead(self.polls)
+ res = self.ios.read(n)
+ assert res is not None
+ if not res:
+ yield IOReadDone(self.polls)
+ break
+ buf += res
+ n -= len(res)
+ return buf
+
+ def readline(self):
+ if DEBUG and __debug__:
+ log.debug("StreamReader.readline()")
+ buf = b""
+ while True:
+ yield IORead(self.polls)
+ res = self.ios.readline()
+ assert res is not None
+ if not res:
+ yield IOReadDone(self.polls)
+ break
+ buf += res
+ if buf[-1] == 0x0a:
+ break
+ if DEBUG and __debug__:
+ log.debug("StreamReader.readline(): %s", buf)
+ return buf
+
+ def aclose(self):
+ yield IOReadDone(self.polls)
+ self.ios.close()
+
+ def __repr__(self):
+ return "" % (self.polls, self.ios)
+
+
+class StreamWriter:
+
+ def __init__(self, s, extra):
+ self.s = s
+ self.extra = extra
+
+ def awrite(self, buf, off=0, sz=-1):
+ # This method is called awrite (async write) to not proliferate
+ # incompatibility with original asyncio. Unlike original asyncio
+ # whose .write() method is both not a coroutine and guaranteed
+ # to return immediately (which means it has to buffer all the
+ # data), this method is a coroutine.
+ if sz == -1:
+ sz = len(buf) - off
+ if DEBUG and __debug__:
+ log.debug("StreamWriter.awrite(): spooling %d bytes", sz)
+ while True:
+ res = self.s.write(buf, off, sz)
+ # If we spooled everything, return immediately
+ if res == sz:
+ if DEBUG and __debug__:
+ log.debug("StreamWriter.awrite(): completed spooling %d bytes", res)
+ return
+ if res is None:
+ res = 0
+ if DEBUG and __debug__:
+ log.debug("StreamWriter.awrite(): spooled partial %d bytes", res)
+ assert res < sz
+ off += res
+ sz -= res
+ yield IOWrite(self.s)
+ #assert s2.fileno() == self.s.fileno()
+ if DEBUG and __debug__:
+ log.debug("StreamWriter.awrite(): can write more")
+
+ # Write piecewise content from iterable (usually, a generator)
+ def awriteiter(self, iterable):
+ for buf in iterable:
+ yield from self.awrite(buf)
+
+ def aclose(self):
+ yield IOWriteDone(self.s)
+ self.s.close()
+
+ def get_extra_info(self, name, default=None):
+ return self.extra.get(name, default)
+
+ def __repr__(self):
+ return "" % self.s
+
+
+def open_connection(host, port, ssl=False):
+ if DEBUG and __debug__:
+ log.debug("open_connection(%s, %s)", host, port)
+ ai = _socket.getaddrinfo(host, port, 0, _socket.SOCK_STREAM)
+ ai = ai[0]
+ s = _socket.socket(ai[0], ai[1], ai[2])
+ s.setblocking(False)
+ try:
+ s.connect(ai[-1])
+ except OSError as e:
+ if e.args[0] != uerrno.EINPROGRESS:
+ raise
+ if DEBUG and __debug__:
+ log.debug("open_connection: After connect")
+ yield IOWrite(s)
+# if __debug__:
+# assert s2.fileno() == s.fileno()
+ if DEBUG and __debug__:
+ log.debug("open_connection: After iowait: %s", s)
+ if ssl:
+ print("Warning: uasyncio SSL support is alpha")
+ import ussl
+ s.setblocking(True)
+ s2 = ussl.wrap_socket(s)
+ s.setblocking(False)
+ return StreamReader(s, s2), StreamWriter(s2, {})
+ return StreamReader(s), StreamWriter(s, {})
+
+
+def start_server(client_coro, host, port, backlog=10):
+ if DEBUG and __debug__:
+ log.debug("start_server(%s, %s)", host, port)
+ ai = _socket.getaddrinfo(host, port, 0, _socket.SOCK_STREAM)
+ ai = ai[0]
+ s = _socket.socket(ai[0], ai[1], ai[2])
+ s.setblocking(False)
+
+ s.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
+ s.bind(ai[-1])
+ s.listen(backlog)
+ while True:
+ if DEBUG and __debug__:
+ log.debug("start_server: Before accept")
+ yield IORead(s)
+ if DEBUG and __debug__:
+ log.debug("start_server: After iowait")
+ s2, client_addr = s.accept()
+ s2.setblocking(False)
+ if DEBUG and __debug__:
+ log.debug("start_server: After accept: %s", s2)
+ extra = {"peername": client_addr}
+ yield client_coro(StreamReader(s2), StreamWriter(s2, extra))
+
+
+import uasyncio.core
+uasyncio.core._event_loop_class = PollEventLoop
diff --git a/PicoWeb/uasyncio/core.py b/PicoWeb/uasyncio/core.py
new file mode 100644
index 0000000..77fdb7a
--- /dev/null
+++ b/PicoWeb/uasyncio/core.py
@@ -0,0 +1,315 @@
+import utime as time
+import utimeq
+import ucollections
+
+
+type_gen = type((lambda: (yield))())
+
+DEBUG = 0
+log = None
+
+def set_debug(val):
+ global DEBUG, log
+ DEBUG = val
+ if val:
+ import logging
+ log = logging.getLogger("uasyncio.core")
+
+
+class CancelledError(Exception):
+ pass
+
+
+class TimeoutError(CancelledError):
+ pass
+
+
+class EventLoop:
+
+ def __init__(self, runq_len=16, waitq_len=16):
+ self.runq = ucollections.deque((), runq_len, True)
+ self.waitq = utimeq.utimeq(waitq_len)
+ # Current task being run. Task is a top-level coroutine scheduled
+ # in the event loop (sub-coroutines executed transparently by
+ # yield from/await, event loop "doesn't see" them).
+ self.cur_task = None
+
+ def time(self):
+ return time.ticks_ms()
+
+ def create_task(self, coro):
+ # CPython 3.4.2
+ self.call_later_ms(0, coro)
+ # CPython asyncio incompatibility: we don't return Task object
+
+ def call_soon(self, callback, *args):
+ if __debug__ and DEBUG:
+ log.debug("Scheduling in runq: %s", (callback, args))
+ self.runq.append(callback)
+ if not isinstance(callback, type_gen):
+ self.runq.append(args)
+
+ def call_later(self, delay, callback, *args):
+ self.call_at_(time.ticks_add(self.time(), int(delay * 1000)), callback, args)
+
+ def call_later_ms(self, delay, callback, *args):
+ if not delay:
+ return self.call_soon(callback, *args)
+ self.call_at_(time.ticks_add(self.time(), delay), callback, args)
+
+ def call_at_(self, time, callback, args=()):
+ if __debug__ and DEBUG:
+ log.debug("Scheduling in waitq: %s", (time, callback, args))
+ self.waitq.push(time, callback, args)
+
+ def wait(self, delay):
+ # Default wait implementation, to be overriden in subclasses
+ # with IO scheduling
+ if __debug__ and DEBUG:
+ log.debug("Sleeping for: %s", delay)
+ time.sleep_ms(delay)
+
+ def run_forever(self):
+ cur_task = [0, 0, 0]
+ while True:
+ # Expire entries in waitq and move them to runq
+ tnow = self.time()
+ while self.waitq:
+ t = self.waitq.peektime()
+ delay = time.ticks_diff(t, tnow)
+ if delay > 0:
+ break
+ self.waitq.pop(cur_task)
+ if __debug__ and DEBUG:
+ log.debug("Moving from waitq to runq: %s", cur_task[1])
+ self.call_soon(cur_task[1], *cur_task[2])
+
+ # Process runq
+ l = len(self.runq)
+ if __debug__ and DEBUG:
+ log.debug("Entries in runq: %d", l)
+ while l:
+ cb = self.runq.popleft()
+ l -= 1
+ args = ()
+ if not isinstance(cb, type_gen):
+ args = self.runq.popleft()
+ l -= 1
+ if __debug__ and DEBUG:
+ log.info("Next callback to run: %s", (cb, args))
+ cb(*args)
+ continue
+
+ if __debug__ and DEBUG:
+ log.info("Next coroutine to run: %s", (cb, args))
+ self.cur_task = cb
+ delay = 0
+ try:
+ if args is ():
+ ret = next(cb)
+ else:
+ ret = cb.send(*args)
+ if __debug__ and DEBUG:
+ log.info("Coroutine %s yield result: %s", cb, ret)
+ if isinstance(ret, SysCall1):
+ arg = ret.arg
+ if isinstance(ret, SleepMs):
+ delay = arg
+ elif isinstance(ret, IORead):
+ cb.pend_throw(False)
+ self.add_reader(arg, cb)
+ continue
+ elif isinstance(ret, IOWrite):
+ cb.pend_throw(False)
+ self.add_writer(arg, cb)
+ continue
+ elif isinstance(ret, IOReadDone):
+ self.remove_reader(arg)
+ elif isinstance(ret, IOWriteDone):
+ self.remove_writer(arg)
+ elif isinstance(ret, StopLoop):
+ return arg
+ else:
+ assert False, "Unknown syscall yielded: %r (of type %r)" % (ret, type(ret))
+ elif isinstance(ret, type_gen):
+ self.call_soon(ret)
+ elif isinstance(ret, int):
+ # Delay
+ delay = ret
+ elif ret is None:
+ # Just reschedule
+ pass
+ elif ret is False:
+ # Don't reschedule
+ continue
+ else:
+ assert False, "Unsupported coroutine yield value: %r (of type %r)" % (ret, type(ret))
+ except StopIteration as e:
+ if __debug__ and DEBUG:
+ log.debug("Coroutine finished: %s", cb)
+ continue
+ except CancelledError as e:
+ if __debug__ and DEBUG:
+ log.debug("Coroutine cancelled: %s", cb)
+ continue
+ # Currently all syscalls don't return anything, so we don't
+ # need to feed anything to the next invocation of coroutine.
+ # If that changes, need to pass that value below.
+ if delay:
+ self.call_later_ms(delay, cb)
+ else:
+ self.call_soon(cb)
+
+ # Wait until next waitq task or I/O availability
+ delay = 0
+ if not self.runq:
+ delay = -1
+ if self.waitq:
+ tnow = self.time()
+ t = self.waitq.peektime()
+ delay = time.ticks_diff(t, tnow)
+ if delay < 0:
+ delay = 0
+ self.wait(delay)
+
+ def run_until_complete(self, coro):
+ def _run_and_stop():
+ yield from coro
+ yield StopLoop(0)
+ self.call_soon(_run_and_stop())
+ self.run_forever()
+
+ def stop(self):
+ self.call_soon((lambda: (yield StopLoop(0)))())
+
+ def close(self):
+ pass
+
+
+class SysCall:
+
+ def __init__(self, *args):
+ self.args = args
+
+ def handle(self):
+ raise NotImplementedError
+
+# Optimized syscall with 1 arg
+class SysCall1(SysCall):
+
+ def __init__(self, arg):
+ self.arg = arg
+
+class StopLoop(SysCall1):
+ pass
+
+class IORead(SysCall1):
+ pass
+
+class IOWrite(SysCall1):
+ pass
+
+class IOReadDone(SysCall1):
+ pass
+
+class IOWriteDone(SysCall1):
+ pass
+
+
+_event_loop = None
+_event_loop_class = EventLoop
+def get_event_loop(runq_len=16, waitq_len=16):
+ global _event_loop
+ if _event_loop is None:
+ _event_loop = _event_loop_class(runq_len, waitq_len)
+ return _event_loop
+
+def sleep(secs):
+ yield int(secs * 1000)
+
+# Implementation of sleep_ms awaitable with zero heap memory usage
+class SleepMs(SysCall1):
+
+ def __init__(self):
+ self.v = None
+ self.arg = None
+
+ def __call__(self, arg):
+ self.v = arg
+ #print("__call__")
+ return self
+
+ def __iter__(self):
+ #print("__iter__")
+ return self
+
+ def __next__(self):
+ if self.v is not None:
+ #print("__next__ syscall enter")
+ self.arg = self.v
+ self.v = None
+ return self
+ #print("__next__ syscall exit")
+ _stop_iter.__traceback__ = None
+ raise _stop_iter
+
+_stop_iter = StopIteration()
+sleep_ms = SleepMs()
+
+
+def cancel(coro):
+ prev = coro.pend_throw(CancelledError())
+ if prev is False:
+ _event_loop.call_soon(coro)
+
+
+class TimeoutObj:
+ def __init__(self, coro):
+ self.coro = coro
+
+
+def wait_for_ms(coro, timeout):
+
+ def waiter(coro, timeout_obj):
+ res = yield from coro
+ if __debug__ and DEBUG:
+ log.debug("waiter: cancelling %s", timeout_obj)
+ timeout_obj.coro = None
+ return res
+
+ def timeout_func(timeout_obj):
+ if timeout_obj.coro:
+ if __debug__ and DEBUG:
+ log.debug("timeout_func: cancelling %s", timeout_obj.coro)
+ prev = timeout_obj.coro.pend_throw(TimeoutError())
+ #print("prev pend", prev)
+ if prev is False:
+ _event_loop.call_soon(timeout_obj.coro)
+
+ timeout_obj = TimeoutObj(_event_loop.cur_task)
+ _event_loop.call_later_ms(timeout, timeout_func, timeout_obj)
+ return (yield from waiter(coro, timeout_obj))
+
+
+def wait_for(coro, timeout):
+ return wait_for_ms(coro, int(timeout * 1000))
+
+
+def coroutine(f):
+ return f
+
+#
+# The functions below are deprecated in uasyncio, and provided only
+# for compatibility with CPython asyncio
+#
+
+def ensure_future(coro, loop=_event_loop):
+ _event_loop.call_soon(coro)
+ # CPython asyncio incompatibility: we don't return Task object
+ return coro
+
+
+# CPython asyncio incompatibility: Task is a function, not a class (for efficiency)
+def Task(coro, loop=_event_loop):
+ # Same as async()
+ _event_loop.call_soon(coro)
diff --git a/PicoWeb/utemplate/compiled.py b/PicoWeb/utemplate/compiled.py
new file mode 100644
index 0000000..82237a4
--- /dev/null
+++ b/PicoWeb/utemplate/compiled.py
@@ -0,0 +1,14 @@
+class Loader:
+
+ def __init__(self, pkg, dir):
+ if dir == ".":
+ dir = ""
+ else:
+ dir = dir.replace("/", ".") + "."
+ if pkg and pkg != "__main__":
+ dir = pkg + "." + dir
+ self.p = dir
+
+ def load(self, name):
+ name = name.replace(".", "_")
+ return __import__(self.p + name, None, None, (name,)).render
diff --git a/PicoWeb/utemplate/source.py b/PicoWeb/utemplate/source.py
new file mode 100644
index 0000000..a9948a1
--- /dev/null
+++ b/PicoWeb/utemplate/source.py
@@ -0,0 +1,190 @@
+# os module is loaded on demand
+#import os
+
+from . import compiled
+
+
+class Compiler:
+
+ START_CHAR = "{"
+ STMNT = "%"
+ STMNT_END = "%}"
+ EXPR = "{"
+ EXPR_END = "}}"
+
+ def __init__(self, file_in, file_out, indent=0, seq=0, loader=None):
+ self.file_in = file_in
+ self.file_out = file_out
+ self.loader = loader
+ self.seq = seq
+ self._indent = indent
+ self.stack = []
+ self.in_literal = False
+ self.flushed_header = False
+ self.args = "*a, **d"
+
+ def indent(self, adjust=0):
+ if not self.flushed_header:
+ self.flushed_header = True
+ self.indent()
+ self.file_out.write("def render%s(%s):\n" % (str(self.seq) if self.seq else "", self.args))
+ self.stack.append("def")
+ self.file_out.write(" " * (len(self.stack) + self._indent + adjust))
+
+ def literal(self, s):
+ if not s:
+ return
+ if not self.in_literal:
+ self.indent()
+ self.file_out.write('yield """')
+ self.in_literal = True
+ self.file_out.write(s.replace('"', '\\"'))
+
+ def close_literal(self):
+ if self.in_literal:
+ self.file_out.write('"""\n')
+ self.in_literal = False
+
+ def render_expr(self, e):
+ self.indent()
+ self.file_out.write('yield str(' + e + ')\n')
+
+ def parse_statement(self, stmt):
+ tokens = stmt.split(None, 1)
+ if tokens[0] == "args":
+ if len(tokens) > 1:
+ self.args = tokens[1]
+ else:
+ self.args = ""
+ elif tokens[0] == "set":
+ self.indent()
+ self.file_out.write(stmt[3:].strip() + "\n")
+ elif tokens[0] == "include":
+ if not self.flushed_header:
+ # If there was no other output, we still need a header now
+ self.indent()
+ tokens = tokens[1].split(None, 1)
+ args = ""
+ if len(tokens) > 1:
+ args = tokens[1]
+ if tokens[0][0] == "{":
+ self.indent()
+ # "1" as fromlist param is uPy hack
+ self.file_out.write('_ = __import__(%s.replace(".", "_"), None, None, 1)\n' % tokens[0][2:-2])
+ self.indent()
+ self.file_out.write("yield from _.render(%s)\n" % args)
+ return
+
+ with self.loader.input_open(tokens[0][1:-1]) as inc:
+ self.seq += 1
+ c = Compiler(inc, self.file_out, len(self.stack) + self._indent, self.seq)
+ inc_id = self.seq
+ self.seq = c.compile()
+ self.indent()
+ self.file_out.write("yield from render%d(%s)\n" % (inc_id, args))
+ elif len(tokens) > 1:
+ if tokens[0] == "elif":
+ assert self.stack[-1] == "if"
+ self.indent(-1)
+ self.file_out.write(stmt + ":\n")
+ else:
+ self.indent()
+ self.file_out.write(stmt + ":\n")
+ self.stack.append(tokens[0])
+ else:
+ if stmt.startswith("end"):
+ assert self.stack[-1] == stmt[3:]
+ self.stack.pop(-1)
+ elif stmt == "else":
+ assert self.stack[-1] == "if"
+ self.indent(-1)
+ self.file_out.write("else:\n")
+ else:
+ assert False
+
+ def parse_line(self, l):
+ while l:
+ start = l.find(self.START_CHAR)
+ if start == -1:
+ self.literal(l)
+ return
+ self.literal(l[:start])
+ self.close_literal()
+ sel = l[start + 1]
+ #print("*%s=%s=" % (sel, EXPR))
+ if sel == self.STMNT:
+ end = l.find(self.STMNT_END)
+ assert end > 0
+ stmt = l[start + len(self.START_CHAR + self.STMNT):end].strip()
+ self.parse_statement(stmt)
+ end += len(self.STMNT_END)
+ l = l[end:]
+ if not self.in_literal and l == "\n":
+ break
+ elif sel == self.EXPR:
+ # print("EXPR")
+ end = l.find(self.EXPR_END)
+ assert end > 0
+ expr = l[start + len(self.START_CHAR + self.EXPR):end].strip()
+ self.render_expr(expr)
+ end += len(self.EXPR_END)
+ l = l[end:]
+ else:
+ self.literal(l[start])
+ l = l[start + 1:]
+
+ def header(self):
+ self.file_out.write("# Autogenerated file\n")
+
+ def compile(self):
+ self.header()
+ for l in self.file_in:
+ self.parse_line(l)
+ self.close_literal()
+ return self.seq
+
+
+class Loader(compiled.Loader):
+
+ def __init__(self, pkg, dir):
+ super().__init__(pkg, dir)
+ self.dir = dir
+ if pkg == "__main__":
+ # if pkg isn't really a package, don't bother to use it
+ # it means we're running from "filesystem directory", not
+ # from a package.
+ pkg = None
+
+ self.pkg_path = ""
+ if pkg:
+ p = __import__(pkg)
+ if isinstance(p.__path__, str):
+ # uPy
+ self.pkg_path = p.__path__
+ else:
+ # CPy
+ self.pkg_path = p.__path__[0]
+ self.pkg_path += "/"
+
+ def input_open(self, template):
+ path = self.pkg_path + self.dir + "/" + template
+ return open(path)
+
+ def compiled_path(self, template):
+ return self.dir + "/" + template.replace(".", "_") + ".py"
+
+ def load(self, name):
+ try:
+ return super().load(name)
+ except (OSError, ImportError):
+ pass
+
+ compiled_path = self.pkg_path + self.compiled_path(name)
+
+ f_in = self.input_open(name)
+ f_out = open(compiled_path, "w")
+ c = Compiler(f_in, f_out, loader=self)
+ c.compile()
+ f_in.close()
+ f_out.close()
+ return super().load(name)
diff --git a/README.md b/README.md
index 37264ef..a7c7522 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,11 @@ utilities for users of official MicroPython firmware to simplify installation.
Scripts for building MicroPython for various target hardware types and for
updating your local source. See [docs](./fastbuild/README.md)
+# PicoWeb
+
+[Easy installation](./PICOWEB.md) guide. Simplify installing this on
+MicroPython hardware platforms under official MicroPython firmware.
+
# SSD1306
A means of rendering multiple larger fonts to the SSD1306 OLED display. The
diff --git a/resilient/client_id.py b/resilient/client_id.py
index a068e77..21f2dad 100644
--- a/resilient/client_id.py
+++ b/resilient/client_id.py
@@ -1,4 +1,4 @@
MY_ID = '2\n'
#_SERVER = '192.168.0.35' # Laptop
-SERVER = '192.168.0.33' # Pi
+SERVER = '192.168.0.10' # Pi
PORT = 8123