commit 30c9169a0cd62161b272cd06d60a0e101f4fcd52 Author: Konstantin Koslowski Date: Thu Feb 27 13:02:23 2020 +0100 initial commit, just echoing diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c999f09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +wifi.py diff --git a/boot.py b/boot.py new file mode 100644 index 0000000..217acfb --- /dev/null +++ b/boot.py @@ -0,0 +1,23 @@ +# This file is executed on every boot (including wake-boot from deepsleep) +#import esp +#esp.osdebug(None) +import uos, machine +#uos.dupterm(None, 1) # disable REPL on UART(0) +import gc +#import webrepl +#webrepl.start() +gc.collect() + +# SSID=xxx, PASSPHRASE=yyy +from wifi import * +import network +import utime as time + +sta_if = network.WLAN(network.STA_IF) +sta_if.active(True) +sta_if.connect(SSID, PASSPHRASE) +print('\nconnecting...') +while not sta_if.isconnected(): + print('.') + time.sleep(1) +print("ifconfig: {}".format(sta_if.ifconfig())) \ No newline at end of file diff --git a/lib/logging.py b/lib/logging.py new file mode 100644 index 0000000..58e38ba --- /dev/null +++ b/lib/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") \ No newline at end of file diff --git a/lib/uwebsockets/client.py b/lib/uwebsockets/client.py new file mode 100644 index 0000000..f93db26 --- /dev/null +++ b/lib/uwebsockets/client.py @@ -0,0 +1,68 @@ +""" +Websockets client for micropython + +Based very heavily off +https://github.com/aaugustin/websockets/blob/master/websockets/client.py +""" + +import logging +import usocket as socket +import ubinascii as binascii +import urandom as random +import ussl + +from .protocol import Websocket, urlparse + +LOGGER = logging.getLogger(__name__) + + +class WebsocketClient(Websocket): + is_client = True + +def connect(uri): + """ + Connect a websocket. + """ + + uri = urlparse(uri) + assert uri + + if __debug__: LOGGER.debug("open connection %s:%s", + uri.hostname, uri.port) + + sock = socket.socket() + addr = socket.getaddrinfo(uri.hostname, uri.port) + sock.connect(addr[0][4]) + if uri.protocol == 'wss': + sock = ussl.wrap_socket(sock) + + def send_header(header, *args): + if __debug__: LOGGER.debug(str(header), *args) + sock.write(header % args + '\r\n') + + # Sec-WebSocket-Key is 16 bytes of random base64 encoded + key = binascii.b2a_base64(bytes(random.getrandbits(8) + for _ in range(16)))[:-1] + + send_header(b'GET %s HTTP/1.1', uri.path or '/') + send_header(b'Host: %s:%s', uri.hostname, uri.port) + send_header(b'Connection: Upgrade') + send_header(b'Upgrade: websocket') + send_header(b'Sec-WebSocket-Key: %s', key) + send_header(b'Sec-WebSocket-Version: 13') + send_header(b'Origin: http://{hostname}:{port}'.format( + hostname=uri.hostname, + port=uri.port) + ) + send_header(b'') + + header = sock.readline()[:-2] + assert header.startswith(b'HTTP/1.1 101 '), header + + # We don't (currently) need these headers + # FIXME: should we check the return key? + while header: + if __debug__: LOGGER.debug(str(header)) + header = sock.readline()[:-2] + + return WebsocketClient(sock) \ No newline at end of file diff --git a/lib/uwebsockets/protocol.py b/lib/uwebsockets/protocol.py new file mode 100644 index 0000000..31eb3f7 --- /dev/null +++ b/lib/uwebsockets/protocol.py @@ -0,0 +1,244 @@ +""" +Websockets protocol +""" + +import logging +import ure as re +import ustruct as struct +import urandom as random +import usocket as socket +from ucollections import namedtuple + +LOGGER = logging.getLogger(__name__) + +# Opcodes +OP_CONT = const(0x0) +OP_TEXT = const(0x1) +OP_BYTES = const(0x2) +OP_CLOSE = const(0x8) +OP_PING = const(0x9) +OP_PONG = const(0xa) + +# Close codes +CLOSE_OK = const(1000) +CLOSE_GOING_AWAY = const(1001) +CLOSE_PROTOCOL_ERROR = const(1002) +CLOSE_DATA_NOT_SUPPORTED = const(1003) +CLOSE_BAD_DATA = const(1007) +CLOSE_POLICY_VIOLATION = const(1008) +CLOSE_TOO_BIG = const(1009) +CLOSE_MISSING_EXTN = const(1010) +CLOSE_BAD_CONDITION = const(1011) + +URL_RE = re.compile(r'(wss|ws)://([A-Za-z0-9-\.]+)(?:\:([0-9]+))?(/.+)?') +URI = namedtuple('URI', ('protocol', 'hostname', 'port', 'path')) + +class NoDataException(Exception): + pass + +class ConnectionClosed(Exception): + pass + +def urlparse(uri): + """Parse ws:// URLs""" + match = URL_RE.match(uri) + if match: + protocol = match.group(1) + host = match.group(2) + port = match.group(3) + path = match.group(4) + + if protocol == 'wss': + if port is None: + port = 443 + elif protocol == 'ws': + if port is None: + port = 80 + else: + raise ValueError('Scheme {} is invalid'.format(protocol)) + + return URI(protocol, host, int(port), path) + + +class Websocket: + """ + Basis of the Websocket protocol. + This can probably be replaced with the C-based websocket module, but + this one currently supports more options. + """ + is_client = False + + def __init__(self, sock): + self.sock = sock + self.open = True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + self.close() + + def settimeout(self, timeout): + self.sock.settimeout(timeout) + + def read_frame(self, max_size=None): + """ + Read a frame from the socket. + See https://tools.ietf.org/html/rfc6455#section-5.2 for the details. + """ + + # Frame header + two_bytes = self.sock.read(2) + + if not two_bytes: + raise NoDataException + + byte1, byte2 = struct.unpack('!BB', two_bytes) + + # Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4) + fin = bool(byte1 & 0x80) + opcode = byte1 & 0x0f + + # Byte 2: MASK(1) LENGTH(7) + mask = bool(byte2 & (1 << 7)) + length = byte2 & 0x7f + + if length == 126: # Magic number, length header is 2 bytes + length, = struct.unpack('!H', self.sock.read(2)) + elif length == 127: # Magic number, length header is 8 bytes + length, = struct.unpack('!Q', self.sock.read(8)) + + if mask: # Mask is 4 bytes + mask_bits = self.sock.read(4) + + try: + data = self.sock.read(length) + except MemoryError: + # We can't receive this many bytes, close the socket + if __debug__: LOGGER.debug("Frame of length %s too big. Closing", + length) + self.close(code=CLOSE_TOO_BIG) + return True, OP_CLOSE, None + + if mask: + data = bytes(b ^ mask_bits[i % 4] + for i, b in enumerate(data)) + + return fin, opcode, data + + def write_frame(self, opcode, data=b''): + """ + Write a frame to the socket. + See https://tools.ietf.org/html/rfc6455#section-5.2 for the details. + """ + fin = True + mask = self.is_client # messages sent by client are masked + + length = len(data) + + # Frame header + # Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4) + byte1 = 0x80 if fin else 0 + byte1 |= opcode + + # Byte 2: MASK(1) LENGTH(7) + byte2 = 0x80 if mask else 0 + + if length < 126: # 126 is magic value to use 2-byte length header + byte2 |= length + self.sock.write(struct.pack('!BB', byte1, byte2)) + + elif length < (1 << 16): # Length fits in 2-bytes + byte2 |= 126 # Magic code + self.sock.write(struct.pack('!BBH', byte1, byte2, length)) + + elif length < (1 << 64): + byte2 |= 127 # Magic code + self.sock.write(struct.pack('!BBQ', byte1, byte2, length)) + + else: + raise ValueError() + + if mask: # Mask is 4 bytes + mask_bits = struct.pack('!I', random.getrandbits(32)) + self.sock.write(mask_bits) + + data = bytes(b ^ mask_bits[i % 4] + for i, b in enumerate(data)) + + self.sock.write(data) + + def recv(self): + """ + Receive data from the websocket. + This is slightly different from 'websockets' in that it doesn't + fire off a routine to process frames and put the data in a queue. + If you don't call recv() sufficiently often you won't process control + frames. + """ + assert self.open + + while self.open: + try: + fin, opcode, data = self.read_frame() + except NoDataException: + return '' + except ValueError: + LOGGER.debug("Failed to read frame. Socket dead.") + self._close() + raise ConnectionClosed() + + if not fin: + raise NotImplementedError() + + if opcode == OP_TEXT: + return data.decode('utf-8') + elif opcode == OP_BYTES: + return data + elif opcode == OP_CLOSE: + self._close() + return + elif opcode == OP_PONG: + # Ignore this frame, keep waiting for a data frame + continue + elif opcode == OP_PING: + # We need to send a pong frame + if __debug__: LOGGER.debug("Sending PONG") + self.write_frame(OP_PONG, data) + # And then wait to receive + continue + elif opcode == OP_CONT: + # This is a continuation of a previous frame + raise NotImplementedError(opcode) + else: + raise ValueError(opcode) + + def send(self, buf): + """Send data to the websocket.""" + + assert self.open + + if isinstance(buf, str): + opcode = OP_TEXT + buf = buf.encode('utf-8') + elif isinstance(buf, bytes): + opcode = OP_BYTES + else: + raise TypeError() + + self.write_frame(opcode, buf) + + def close(self, code=CLOSE_OK, reason=''): + """Close the websocket.""" + if not self.open: + return + + buf = struct.pack('!H', code) + reason.encode('utf-8') + + self.write_frame(OP_CLOSE, buf) + self._close() + + def _close(self): + if __debug__: LOGGER.debug("Connection closed") + self.open = False + self.sock.close() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..a800999 --- /dev/null +++ b/main.py @@ -0,0 +1,29 @@ +import ujson +import utime as time +from uwebsockets import client + +HOST="ws://10.64.12.20:8100" +NODEID="esp8266-1234" +NAME="esp8266-1234" + +def register(ws): + ret = { "command": "register", "nodeid": NODEID, "name": NAME, "ts": time.time()} + ws.send(ujson.dumps(ret)) + +def listen(ws): + while (True): + msg = ws.recv() + if msg: + print("< {}".format(msg)) + +def main(): + ws = client.connect(HOST) + while (True): + print("register...") + register(ws) + print("listen...") + listen(ws) + time.sleep(5) + +if __name__ == "__main__": + main() \ No newline at end of file