diff --git a/README.md b/README.md index dc726e8..20ba171 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,11 @@ print(connection.get_status()) ------------------------------------------------------------------- +## Limitations & Considerations + +- This library disables concurrent write protection- +If your code stores something to a file, be careful when writing to it while the board is plugged in to avoid corruption! + ### Testing Status: | MCU | Description | | ----------- | ----------- | diff --git a/boot.py b/boot.py new file mode 100644 index 0000000..982abe0 --- /dev/null +++ b/boot.py @@ -0,0 +1,10 @@ +# OFFICIAL CubeServer Helper Code - DO NOT REMOVE! +""" This file is run on hard resets and during code updates. + Copyright (c) 2023 Joseph R. Freeston +""" + +import storage + +print("Mounting storage with concurrent write protection off.") +storage.remount("/", False, disable_concurrent_write_protection=True) + diff --git a/code.py b/code.py index 8808998..2e93963 100644 --- a/code.py +++ b/code.py @@ -2,11 +2,23 @@ from servercom import Connection, Text +# Connect to server: print("Connecting to the server...") connection = Connection() print("Connected!") -connection.post(Text("Test from CircuitPython!")) +# Turn off WiFi to save power: +connection.close_wifi() +# Reconnect WiFi when ready: +connection.connect_wifi() + +# Check for code updates from the server: +connection.code_update() + +# If none, post Hello World: +connection.post(Text("Hello World!")) + +# Get status: print("Getting status:") print(connection.get_status()) diff --git a/cubeserver-api-python.zip b/cubeserver-api-python.zip new file mode 100644 index 0000000..2e4e663 Binary files /dev/null and b/cubeserver-api-python.zip differ diff --git a/package.sh b/package.sh index dca1db6..b8a0318 100755 --- a/package.sh +++ b/package.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash ./update_version.sh -zip -r cubeserver-api-python.zip . -x .gitignore -x ./.git 2>&1 > /dev/null +zip -r cubeserver-api-python.zip . -x .gitignore -x ./.git -x package.sh -x update_version.sh -x version.txt 2>&1 > /dev/null echo cubeserver-api-python.zip diff --git a/LICENSE b/servercom/LICENSE similarity index 94% rename from LICENSE rename to servercom/LICENSE index 01adb25..f92d82e 100644 --- a/LICENSE +++ b/servercom/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Joseph R. Freeston (snorklerjoe) +Copyright (c) 2022-2023 Joseph R. Freeston (snorklerjoe) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/servercom/implementations/circuitpy.py b/servercom/implementations/circuitpy.py index 0c7d5ba..7b60660 100644 --- a/servercom/implementations/circuitpy.py +++ b/servercom/implementations/circuitpy.py @@ -4,13 +4,23 @@ import ssl import wifi +import time import socketpool from gc import collect from errno import EAGAIN -from binascii import b2a_base64 +from binascii import b2a_base64, a2b_base64 from json import loads +from microcontroller import reset # Helpers: +def _replace_code(new_code: bytes, do_reset=True): + """Replaces the contents of code.py with a new bytes + """ + with open("/code.py", "wb") as fp: + fp.write(new_code) + if do_reset: + reset() + def enum(**enums): """Fake enum-maker""" return type('Enum', (), enums) @@ -157,12 +167,21 @@ def __init__( self.context.load_verify_locations(cadata=server_cert) self.connect_wifi() - def connect_wifi(self) -> None: + def connect_wifi(self, attempts=10) -> None: """Creates the wifi connection to the access point""" wifi.radio.enabled = True if self.v: print("Connecting to the access point...") - wifi.radio.connect(self.conf.AP_SSID) + connected = False + while not connected and attempts > 0: + try: + wifi.radio.connect(self.conf.AP_SSID) + continue + except ConnectionError as e: + if self.v: + print(e.with_traceback) + attempts -= 1 + time.sleep(1) if self.v: print("Initializing socket pool...") self.pool = socketpool.SocketPool(wifi.radio) @@ -356,11 +375,19 @@ def request( raise last_error def get_status(self) -> GameStatus: - resp = self.request('GET', '/status') + if self.v: + print("Getting status...") + resp = self.request('GET', '/status', + headers=['User-Agent: CircuitPython, dude!'] + ) resp_json = loads(resp[1]) + if self.v: + print(f"It is {resp_json['unix_time']} seconds since the epoch.") return GameStatus(resp_json['unix_time'], resp_json['status']['score'], resp_json['status']['strikes']) def post(self, point: DataPoint) -> bool: + if self.v: + print("Posting datapoint!") return self.request( 'POST', '/data', @@ -370,6 +397,8 @@ def post(self, point: DataPoint) -> bool: ).code == 201 def email(self, msg: Email) -> bool: + if self.v: + print(f"Sending email {msg.subject}...") return self.request( 'POST', '/email', @@ -378,6 +407,44 @@ def email(self, msg: Email) -> bool: headers=['User-Agent: CircuitPython, dude!'] ).code == 201 + def code_update(self, reset=True) -> bool: + """Checks for code updates from the server (uploaded by team) + call like code_update(reset=False) to download the update but not reset + (Must reset to run the new code) + Otherwise, run like code_update() + Returns True if reset is False and the update is successful. + Reset the microcontroller in this case. + Otherwise, returning False, the update is stale or invalid. + """ + if self.v: + print("Checking for updates...") + response = self.request( + 'GET', + '/update', + headers=['User-Agent: CircuitPython, dude!'] + ) + if response.code != 200: + if self.v: + print(f"Bad response code {response.code}") + return False + print(f"Response: {response[1]}") + resp_json = loads(response[1]) + if resp_json['new']: + if self.v: + print("New Update!") + _replace_code( + a2b_base64( + resp_json['code'] + ), + reset=reset + ) + if self.v: + print("code.py replaced!") + return True + if self.v: + print("Stale update.") + return False + def __exit__(self): if self.v: print("Closing the server connection-") diff --git a/servercom/implementations/cpy.py b/servercom/implementations/cpy.py index 94f8e7c..62aaea8 100644 --- a/servercom/implementations/cpy.py +++ b/servercom/implementations/cpy.py @@ -354,6 +354,8 @@ def email(self, msg: Email) -> bool: content_type = 'application/json', headers=['User-Agent: CPython, dude!'] ).code == 201 + def code_update(reset=True): + pass def __exit__(self): if self.v: print("Closing the server connection-")