So you want your friends/family/strangers to enjoy the luxury of modern technology, scanning a QR-code to join a wifi network, but wait...you want to save that in settings.toml
too? Sheeesh, a tall order, lets get on with it then!
We'll need to check a button at boot to decide if the Memento camera has write access to the flash drive, or the PC. Then we can update the settings.toml
file if it's writable, and either way we can offer to join the network listed in the QRcode.
The code example can be found here:
https://github.com/tyeth/Adafruit_CircuitPython_PyCamera/tree/wifi-qrsetup/examples/qrio-update-wifi
There are two files, the boot.py
which handles the first load when the device is turned on or reset button is pressed. It prints a message to the screen and waits 2.5seconds before checking for the shutter button being held down, and then does the switching / flicking between read-only for the USB / PC, or read-only for the Memento camera (circuitpython). See here: https://github.com/tyeth/Adafruit_CircuitPython_PyCamera/blob/wifi-qrsetup/examples/qrio-update-wifi/boot.py
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries # SPDX-FileCopyrightText: 2024 Tyeth Gundry for Adafruit Industries # # SPDX-License-Identifier: Unlicense """Automatically create the /sd mount point at boot time""" import os import storage import time import digitalio import board from digitalio import Pull storage.remount("/", readonly=False) try: os.mkdir("/sd") except OSError: pass # It's probably 'file exists', OK to ignore shutter_button = digitalio.DigitalInOut(board.BUTTON) shutter_button.switch_to_input(Pull.UP) print("") print("Waiting 2.5s before checking if Shutter button held") time.sleep(2.5) if not shutter_button.value: print("") print("") print("Button held, MEMENTO = Flash WRITEABLE") print("") print("") else: print("") print("") print("Button not held, USB = WRITEABLE") print("") print("") storage.remount("/", readonly=True)
Second is the main code.py
file, which is a modified version of the qrio
example, https://github.com/tyeth/Adafruit_CircuitPython_PyCamera/blob/wifi-qrsetup/examples/qrio-update-wifi/code.py.
The changes to code.py
begin after verifying if the QR-code is unseen compared to the last seen code, on line 47. This means that once a QRcode for a wifi network has been seen, another qrcode must be seen before you can show that wifi one again. After this check we then verify if the beginning of the QR-Code's data payload contains the characters WIFI:
, as that prefix designates a Wi-Fi network related QR-code.
Here are two examples, one of my open home network: WIFI:S:free4all;t:WEP;P:password;;
and my phone hotspot WIFI:S:THERMAL;t:SAE;P:password;;
(WPA2/3) or hotspot with no security WIFI:S:THERMAL;t:nopass;P:;;
[I think I've previously seen t:open;
on other phones/networks with security disabled, and the type designator t:
should probably be capitalised, but I hand typed these from memory]
If a valid Wi-Fi QR code is found then we run all the magic, first decoding the WiFi network name, a.k.a. SSID (between S:
and the next delimiter ;
), then the password (whose field begins P:
), and finally we use os.getenv
to read the current values saved in settings.toml
and then if different we attempt to save the updated values.
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries # SPDX-FileCopyrightText: 2024 Tyeth Gundry for Adafruit Industries # # SPDX-License-Identifier: Unlicense """ This demo is designed for the Adafruit Memento ESP32-S3 Camera board. It allows the shutter button to be checked in boot.py, and if pressed, it will make the filesystem read-write, and then this code will allow updating the wifi settings by scanning a QR code. Existing valid TOML entries will be saved, and the new ones will be written to the settings.toml file. """ import time import toml from toml._dotty import Dotty import qrio import storage import os from adafruit_pycamera import PyCamera pycam = PyCamera() pycam._mode_label.text = "QR SCAN" # pylint: disable=protected-access pycam._res_label.text = "" # pylint: disable=protected-access pycam.effect = 0 pycam.camera.hmirror = False pycam.display.refresh() qrdecoder = qrio.QRDecoder(pycam.camera.width, pycam.camera.height) old_payload = None while True: new_frame = pycam.continuous_capture() if new_frame is None: continue pycam.blit(new_frame) for row in qrdecoder.decode(new_frame, qrio.PixelPolicy.RGB565_SWAPPED): print(row) payload = row.payload try: payload = payload.decode("utf-8") except UnicodeError: payload = str(payload) if payload != old_payload: pycam.tone(200, 0.1) if payload.startswith("WIFI:"): pycam.display_message("WIFI", color=0xFFFFFF, scale=2) pycam.tone(140, 0.5) old_ssid=os.getenv("CIRCUITPY_WIFI_SSID", None) old_password=os.getenv("CIRCUITPY_WIFI_PASSWORD", None) start_of_ssid = payload.find("S:")+2 new_ssid = str(payload[ start_of_ssid : start_of_ssid + payload[start_of_ssid:].find(";") ]) start_of_pass = payload.find("P:")+2 new_pass = str(payload[start_of_pass:start_of_pass + payload[start_of_pass:].find(";")]) print("Supplied SSID:", new_ssid, "PASS:", new_pass) if old_ssid != new_ssid or old_password != new_pass: # os.setenv("CIRCUITPY_WIFI_SSID", new_ssid) #get file object: print("Updating settings.toml") with open("/settings.toml", "r") as f: try: old_toml = dict() lines = f.readlines() for i,w in enumerate(lines): try: print(f"TOML Line #{i}: {str(w).strip()}") new_toml_section = toml.loads(str(w).strip()) old_toml.update(new_toml_section._data) # print("Added:",new_toml_section) except (toml.TOMLError, KeyError) as e: print("TOML ERROR - Skipping key:", e) pass print(old_toml) except: raise old_toml = {} old_toml["CIRCUITPY_WIFI_SSID"] = new_ssid old_toml["CIRCUITPY_WIFI_PASSWORD"] = new_pass pycam.display_message("WIFI CHANGING...", color=0xFFFFFF, scale=2) pycam.tone(320, 0.25) # check if circuitpython fs is writeable if storage.getmount("/").readonly: pycam.display_message("FS READONLY!\n Hold Shutter \nwhen bootlog shown", color=0xFFFFFF, scale=2) print("FS READONLY! Hold select when bootlog shown") pycam.tone(560, 0.25) time.sleep(2.5) else: with open("/settings.toml", "w") as f: f.write(toml.dumps(old_toml)) pycam.display_message("WIFI CHANGED!", color=0xFFFFFF, scale=2) print("WIFI CHANGED! Settings.toml updated, Reboot to take affect") pycam.tone(440, 0.25) time.sleep(1) timeleft=65 msg = "Would you like to \njoin network now? \nPress Shutter if yes\n %s" pycam.keys_debounce() while not pycam.shutter.pressed and timeleft > 0: timeleft -= 1 # NB: note that this is really bad! it should only update when needed, # or it will take longer than expected updating with the same image pycam.display_message(msg % (timeleft//20), color=0xFFFFFF, scale=2) time.sleep(0.05) pycam.keys_debounce() if pycam.shutter.pressed: pycam.display_message("Joining Network...", color=0xFFFFFF, scale=2) import wifi wifi.radio.enabled = False time.sleep(0.1) wifi.radio.enabled = True wifi.radio.connect(new_ssid, new_pass) time.sleep(0.75) pycam.display_message("", color=0xFFFFFF, scale=2) print(payload) pycam.display_message(payload, color=0xFFFFFF, scale=1) time.sleep(1) old_payload = payload
To safely overwrite the settings.toml file with the new values we first need to use the toml
library to read all of the settings.toml entries (lines - each one is a toml entry), then we update the SSID and password (already forced to strings to handle empty values), and write the new settings dictionary back to the file after first converting it back to toml format.
Lastly, and especially useful if the saving fails due to the flash drive being read-only to the Memento, the user is asked if they'd like to connect to the wifi network immediately? A count-down happens and the shutter button is watched, and if pressed then the new wifi network is joined. The only problems here were the count-down being uneven between numbers, the click event defaulting to 200ms hold-time, and crashes if network not found. All of those would be cleaned up / handled in a real project.
Job done!
Obviously this is not a real complete project, instead the code would be included as a portion of another project, that benefitted from being on a wireless network. Like the IP-Cam examples in the pycamera repository,
https://github.com/adafruit/Adafruit_CircuitPython_PyCamera, or maybe you'll use the web-workflow to connect remotely to the device and take photos / modify code that way...