diff --git a/README.md b/README.md new file mode 100644 index 00000000..c0ecc504 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +PiggyOS +======= + +This is an operating system for microcontrollers like the ESP32. + +It's written entirely in MicroPython, including device drivers, interrupt handlers, boot code, multitasking, display handling. + +The architecure is inspired by the Android operating system for smartphones: +- 'thin' operating system with facilities for apps +- 'everything is an app' idea +- making it as simple as possible for developers to build new apps + +## Apps + +The operating system comes with a few apps built-in that are necessary to bootstrap: +- launcher: to be able to start apps +- wificonf: to be able to connect to the wifi +- appstore: to be able to download and install new apps + +Furthermore, these apps are also built-in for convenience: +- osupdate: to download and install operating system updates +- camera: to take pictures and videos +- imutest: to test the Inertial Measurement Unit (accelerometer) + +## Supported hardware + +- https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-2 + +## Architecture + +- boot.py: initializes the hardware +- main.py: initializes the User Interface, contains helper functions for apps, and starts the launcher app + +## Filesystem layout: + +- /apps: new apps are downloaded and installed here +- /apps/com.example.app1: example app1 installation directory +- /apps/com.example.app1/MANIFEST.MF: info about app1 such as Name, Start-Script +- /apps/com.example.app1/mipmap-mdpi: medium dpi images for app1 +- /apps/com.example.app1/mipmap-mdpi/icon_64x64.bin: icon for app1 (64x64 pixels) +- /builtin/: read-only filesystem that's compiled in and mounted at boot by main.py +- /builtin/apps: apps that are builtin and necessary for minimal facilities (launcher, wificonf, appstore etc) +- /builtin/res/mipmap-mdpi/default_icon_64x64.bin: default icon for apps that don't have one + diff --git a/files_to_scripts.py b/draft_code/files_to_scripts.py similarity index 100% rename from files_to_scripts.py rename to draft_code/files_to_scripts.py diff --git a/draft_code/include_in_build.py b/draft_code/include_in_build.py new file mode 100644 index 00000000..bbb0fbb2 --- /dev/null +++ b/draft_code/include_in_build.py @@ -0,0 +1,2 @@ +print("This script will be included in the build.") +print("You can then run it with: import include_in_build") diff --git a/draft_code/microdot_webserver.py b/draft_code/microdot_webserver.py new file mode 100644 index 00000000..3a030501 --- /dev/null +++ b/draft_code/microdot_webserver.py @@ -0,0 +1,114 @@ + + + +#import mip +#mip.install('github:miguelgrinberg/microdot/src/microdot/microdot.py') +# http://192.168.1.122/upload +# http://192.168.1.122/files// + + +from microdot import Microdot, Response +import os + + +# HTML template for the upload form +UPLOAD_FORM = ''' + + +File Manager + +

File Manager

+

View Files

+

Upload File

+
+ + +
+ + +''' + +app = Microdot() + +@app.route('/') +async def index(request): + return 'Files' + + +@app.route('/files/') +async def list_files(req, path=''): + try: + # Sanitize path to prevent directory traversal + path = path.strip('/') + if '..' in path: + return Response('Invalid path', status_code=400) + # Get directory contents + full_path = '/' + path + files = os.listdir(full_path) if path else os.listdir('/') + html = '

File Manager

' + return Response(html, headers={'Content-Type': 'text/html'}) + except OSError as e: + return Response(f'Error: {e}', status_code=500) + + +@app.route('/download/') +async def download_file(req, path): + try: + full_path = '/' + path.strip('/') + with open(full_path, 'rb') as f: + content = f.read() # Read in chunks for large files + return Response(content, headers={ + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': f'attachment; filename="{path.split("/")[-1]}"' + }) + except OSError as e: + return Response(f'Error: {e}', status_code=404) + + + +@app.route('/upload', methods=['GET']) +async def upload_form(req): + return Response(UPLOAD_FORM, headers={'Content-Type': 'text/html'}) + + + +@app.route('/upload', methods=['POST']) +async def upload_file(req): + try: + # Check if form data contains a file + if 'file' not in req.form or not req.files['file']['filename']: + return Response('No file selected', status_code=400) + # Get file details + filename = req.files['file']['filename'] + file_content = req.files['file']['body'] + # Sanitize filename to prevent path traversal + filename = filename.split('/')[-1].split('\\')[-1] + if not filename: + return Response('Invalid filename', status_code=400) + # Save file to filesystem + file_path = f'/{filename}' + with open(file_path, 'wb') as f: + f.write(file_content) # Write in one go for small files + # Free memory + gc.collect() + # Redirect to file listing + return Response(status_code=302, headers={'Location': '/files'}) + except OSError as e: + return Response(f'Error saving file: {e}', status_code=500) + except MemoryError: + return Response('File too large for available memory', status_code=507) + + + +def startit(): + app.run(port=80) + # http://192.168.1.115:5000 + + +import _thread +_thread.start_new_thread(startit, ()) + diff --git a/draft_code/qmi8658_reworked.py b/draft_code/qmi8658_reworked.py new file mode 100644 index 00000000..e9e8fae8 --- /dev/null +++ b/draft_code/qmi8658_reworked.py @@ -0,0 +1,142 @@ +import struct +import time + +from machine import I2C + + +# Sensor constants +_QMI8685_PARTID = const(0x05) +_REG_PARTID = const(0x00) +_REG_REVISION = const(0x01) + +_REG_CTRL1 = const(0x02) # Serial interface and sensor enable +_REG_CTRL2 = const(0x03) # Accelerometer settings +_REG_CTRL3 = const(0x04) # Gyroscope settings +_REG_CTRL4 = const(0x05) # Magnetomer settings (support not implemented in this driver yet) +_REG_CTRL5 = const(0x06) # Sensor data processing settings +_REG_CTRL6 = const(0x07) # Attitude Engine ODR and Motion on Demand +_REG_CTRL7 = const(0x08) # Enable Sensors and Configure Data Reads + +_REG_TEMP = const(0x33) # Temperature sensor. + +_REG_AX_L = const(0x35) # Read accelerometer +_REG_AX_H = const(0x36) +_REG_AY_L = const(0x37) +_REG_AY_H = const(0x38) +_REG_AZ_L = const(0x39) +_REG_AZ_H = const(0x3A) + +_REG_GX_L = const(0x3B) # read gyro +_REG_GX_H = const(0x3C) +_REG_GY_L = const(0x3D) +_REG_GY_H = const(0x3E) +_REG_GZ_L = const(0x3F) +_REG_GZ_H = const(0x40) + +_QMI8658_I2CADDR_DEFAULT = const(0X6B) + + +_ACCELSCALE_RANGE_2G = const(0b00) +_ACCELSCALE_RANGE_4G = const(0b01) +_ACCELSCALE_RANGE_8G = const(0b10) +_ACCELSCALE_RANGE_16G = const(0b11) + +_GYROSCALE_RANGE_16DPS = const(0b000) +_GYROSCALE_RANGE_32DPS = const(0b001) +_GYROSCALE_RANGE_64DPS = const(0b010) +_GYROSCALE_RANGE_128DPS = const(0b011) +_GYROSCALE_RANGE_256DPS = const(0b100) +_GYROSCALE_RANGE_512DPS = const(0b101) +_GYROSCALE_RANGE_1024DPS = const(0b110) +_GYROSCALE_RANGE_2048DPS = const(0b111) + +_ODR_8000HZ = const(0b0000) +_ODR_4000HZ = const(0b0001) +_ODR_2000HZ = const(0b0010) +_ODR_1000HZ = const(0b0011) +_ODR_500HZ = const(0b0100) +_ODR_250HZ = const(0b0101) +_ODR_125HZ = const(0b0110) +_ODR_62_5HZ = const(0b0111) + + +class QMI8658: + global _QMI8658_I2CADDR_DEFAULT + def __init__(self,i2c_bus: I2C,address: int = _QMI8658_I2CADDR_DEFAULT,accel_scale: int = _ACCELSCALE_RANGE_8G,gyro_scale: int = _GYROSCALE_RANGE_256DPS): + self.i2c = i2c_bus + self.address = address + # Verify sensor part ID + if self._read_u8(_REG_PARTID) != _QMI8685_PARTID: + raise AttributeError("Cannot find a QMI8658") + # Setup initial configuration + self._configure_sensor(accel_scale, gyro_scale) + # Configure scales/divisors for the driver + self.acc_scale_divisor = { + _ACCELSCALE_RANGE_2G: 1 << 14, + _ACCELSCALE_RANGE_4G: 1 << 13, + _ACCELSCALE_RANGE_8G: 1 << 12, + _ACCELSCALE_RANGE_16G: 1 << 11, + }[accel_scale] + self.gyro_scale_divisor = { + _GYROSCALE_RANGE_16DPS: 2048, + _GYROSCALE_RANGE_32DPS: 1024, + _GYROSCALE_RANGE_64DPS: 512, + _GYROSCALE_RANGE_128DPS: 256, + _GYROSCALE_RANGE_256DPS: 128, + _GYROSCALE_RANGE_512DPS: 64, + _GYROSCALE_RANGE_1024DPS: 32, + _GYROSCALE_RANGE_2048DPS: 16, + }[gyro_scale] + + + def _configure_sensor(self, accel_scale: int, gyro_scale: int): + # Initialize accelerometer and gyroscope settings + self._write_u8(_REG_CTRL1, 0x60) # Set SPI auto increment and big endian (Ctrl 1) + self._write_u8(_REG_CTRL2, (accel_scale << 4) | _ODR_1000HZ) # Accel Config + self._write_u8(_REG_CTRL3, (gyro_scale << 4) | _ODR_1000HZ) # Gyro Config + self._write_u8(_REG_CTRL5, 0x01) # Low-pass filter enable + self._write_u8(_REG_CTRL7, 0x03) # Enable accel and gyro + time.sleep_ms(100) + + + # Helper functions for register operations + def _read_u8(self, reg:int) -> int: + return self.i2c.readfrom_mem(self.address, reg, 1)[0] + + def _read_xyz(self, reg:int) -> tuple[int, int, int]: + data = self.i2c.readfrom_mem(self.address, reg, 6) + return struct.unpack(' float: + """Get the device temperature.""" + temp_raw = self._read_u8(_REG_TEMP) + return temp_raw / 256 + + @property + def acceleration(self) -> tuple[float, float, float]: + """Get current acceleration reading.""" + raw_accel = self._read_xyz(_REG_AX_L) + return tuple(val / self.acc_scale_divisor for val in raw_accel) + + @property + def gyro(self) -> tuple[float, float, float]: + """Get current gyroscope reading.""" + raw_gyro = self._read_xyz(_REG_GX_L) + return tuple(val / self.gyro_scale_divisor for val in raw_gyro) + + +import machine +sensor = QMI8658(I2C(0, sda=machine.Pin(48), scl=machine.Pin(47))) +while True: + print(f""" +QMI8685 +{sensor.temperature=} +{sensor.acceleration=} +{sensor.gyro=} +""") + time.sleep(1) diff --git a/draft_code/saved_functions.py b/draft_code/saved_functions.py new file mode 100644 index 00000000..36eaf0e0 --- /dev/null +++ b/draft_code/saved_functions.py @@ -0,0 +1,546 @@ + + + +import network +import time +wlan = network.WLAN(network.STA_IF) +wlan.active(True) +wlan.connect("SSIDHERE", "PASSWORDHERE") +print("Connecting to Wi-Fi...") +time.sleep(5) +start_app("/apps/com.example.appstore") + + + + +import mip +mip.install('github:echo-lalia/qmi8658-micropython') +# remove multi line comments + +from machine import Pin, I2C +from qmi8658 import QMI8658 +import time +import machine +sensor = QMI8658(I2C(0, sda=machine.Pin(48), scl=machine.Pin(47))) +print(f""" +QMI8685 +{sensor.temperature=} +{sensor.acceleration=} +{sensor.gyro=} +""") + + + +wifi_icon = lv.label(lv.screen_active()) +wifi_icon.set_text("Test label") +wifi_icon.align(lv.ALIGN.CENTER, 0, 0) +wifi_icon.set_style_text_color(lv.color_hex(0x0000FF), 0) + + + + +# This file is executed on every boot (including wake-boot from deepsleep) +#import esp +#esp.osdebug(None) +#import webrepl +#webrepl.start() + + + +# Fetch Bitcoin block height from mempool.space +def get_block_height(): + try: + response = urequests.get("https://mempool.space/api/blocks/tip/height") + if response.status_code == 200: + height = response.text.strip() # Returns plain text (e.g., "853123") + response.close() + return height + else: + response.close() + return "Error: HTTP " + str(response.status_code) + except Exception as e: + return "Error: " + str(e) + +def show_block_height(): + # Create a label for block height + label = lv.label(scr) + label.set_text("Bitcoin Block Height: Fetching...") + label.set_style_text_color(lv.color_make(0, 255, 0), 0) # Green text + label.set_style_text_font(lv.font_montserrat_16, 0) # Larger font (if available) + label.align(lv.ALIGN.TOP_LEFT, 10, 200) + #label.center() + + # Style for label background + style = lv.style_t() + style.init() + style.set_bg_color(lv.palette_main(lv.PALETTE.DARK)) # Dark background + style.set_border_width(2) + style.set_border_color(lv.color_make(255, 255, 255)) # White border + style.set_pad_all(10) + style.set_radius(10) + label.add_style(style, 0) + + height = get_block_height() + label.set_text(f"Block Height: {height}") + + + + + + + + +# Create file explorer widget +file_explorer = lv.file_explorer(subwindow) +file_explorer.set_root_path("/") +file_explorer.explorer_open_dir('/') +file_explorer.set_size(210, 210) +file_explorer.set_mode(lv.FILE_EXPLORER.MODE.DEFAULT) # Default browsing mode +file_explorer.set_sort(lv.FILE_EXPLORER.SORT.NAME_ASC) # Sort by name, ascending +file_explorer.align(lv.ALIGN.CENTER, 0, 0) +def file_explorer_event_cb(e): + code = e.get_code() + obj = e.get_target_obj() + if code == lv.EVENT.VALUE_CHANGED: + #selected_path = obj.get_selected_file_name() + selected_path = file_explorer.explorer_get_selected_file_name + print("Selected:", selected_path) + if obj.is_selected_dir(): + print("This is a directory") + else: + print("This is a file") + + +# Attach event callback +file_explorer.add_event_cb(file_explorer_event_cb, lv.EVENT.VALUE_CHANGED, None) + + + + + +#show_block_height() + +# Connect to Wi-Fi and fetch block height +#if connect_wifi(): +#else: +# label.set_text("Block Height: Wi-Fi Error") + + +import os +print(os.listdir('/')) +try: + with open('/boot.py', 'r') as file: + print("Contents of /boot.py:") + print("-" * 20) + for line in file: + print(line.rstrip()) # Remove trailing newlines for clean output +except OSError as e: + print("Error reading /boot.py:", e) + +#with open('/block_height.txt', 'w') as f: +# f.write('853123') + + + + + + +# ffmpeg isn't compiled in... +# Create video widget +subwindow.clean() +video = lv.video(subwindow) +video.set_size(320, 200) +video.center() + +# Open and play video using LVGL's FFmpeg backend +video_path = "/video/video_320x180.avi" +video.set_src(f"file://{video_path}") +video.set_play_mode(lv.VIDEO_PLAY_MODE.PLAY) # Start playback +video.play() + + +script_globals = { + 'lv': lv, + #'subwindow': lvgl_obj, +} + +with open("/launcher.py", 'r') as f: + script_source = f.read() + + +exec(script_source, script_globals) + + + + + + + +# Debug file system +image_path = "/test.jpg" +try: + print("Checking file system...") + print(f"Apps dir: {uos.listdir('/apps')}") + print(f"App1 dir: {uos.listdir('/apps/com.example.app1')}") + print(f"Res dir: {uos.listdir('/apps/com.example.app1/res')}") + print(f"Mipmap dir: {uos.listdir('/apps/com.example.app1/res/mipmap-mdpi')}") + print(f"File exists: {image_path in uos.listdir('/apps/com.example.app1/res/mipmap-mdpi')}") +except Exception as e: + print(f"File system error: {e}") + +# Load and display the image +print("Creating image widget...") +image = lv.image(lv.screen_active()) +try: + print(f"Loading image: {image_path}") + image.set_src(image_path) + image.align(lv.ALIGN.CENTER, 0, 0) + print("Image loaded and aligned") +except Exception as e: + print(f"Image load error: {e}") + +# Debug screen and widget +print(f"Screen active: {lv.screen_active()}") +print(f"Image widget: {image}") + + + + + + + +# Debug display +#print(f"Display registered: {lv.disp}") +#print(f"Display resolution: {lv.display_get_horizontal_resolution(lv.disp)}x{lv.display_get_vertical_resolution(lv.disp)}") + +# lv.lodepng_init() not needed + + +# PNG: +with open("/icon_64x64.png", 'rb') as f: + image_data = f.read() + +image_dsc = lv.image_dsc_t({ + 'data_size': len(image_data), + 'data': image_data +}) + +image1 = lv.image(lv.screen_active()) +image1.set_src(image_dsc) +image1.set_pos(150,100) + + + + +# BIN +with open("/icon_64x64.bin", 'rb') as f: + image_data = f.read() + +image_dsc = lv.image_dsc_t({ + 'data_size': len(image_data), + 'data': image_data +}) +image1 = lv.image(subwindow) +image1.set_src(image_data) +image1.set_pos(150,100) + + + + +# BIN +with open("/icon_64x64.bin", 'rb') as f: + image_data = f.read() + +image_dsc = lv.image_dsc_t({ + 'data_size': len(image_data), + 'data': image_data +}) +image1 = lv.image(subwindow) +image1.set_src("A:/icon_64x64.bin") +image1.set_pos(150,100) +image1 = lv.image(subwindow) +image1.set_src("/icon_64x64.bin") +image1.set_pos(150,100) + +# WORKING BIN: +import lvgl as lv +import utime + +# Initialize LVGL (assuming already done) +lv.init() + +# Load the binary image data (including 12-byte header) +with open("/icon_64x64.bin", 'rb') as f: + image_data = f.read() + +# Verify the data size (64x64 RGB565 + 12-byte header = 8204 bytes) +if len(image_data) != 8204: + raise ValueError("Invalid image data size") + +# Keep image_data alive to prevent garbage collection +global_image_data = image_data # Store globally to pin in memory + +# Create an lv_image_dsc_t descriptor, letting LVGL parse the header +image_dsc = lv.image_dsc_t({ + "header": { + "magic": lv.IMAGE_HEADER_MAGIC, + "w": 64, + "h": 64, + "stride": 64 * 2, + "cf": lv.COLOR_FORMAT.RGB565 + }, + "data": global_image_data, # Entire 8204 bytes (header + pixel data) + "data_size": len(image_data) # Total size (8204 bytes) +}) + +# Create an image widget +img = lv.image(lv.screen_active()) +img.set_src(image_dsc) # Set the image source to the descriptor +img.center() # Center the image on the screen + + + + + + + + + +try: + print("Checking file system...") + print(f"Apps dir: {uos.listdir('/apps')}") + print(f"App1 dir: {uos.listdir('/apps/com.example.app1')}") + print(f"Res dir: {uos.listdir('/apps/com.example.app1/res')}") + print(f"Mipmap dir: {uos.listdir('/apps/com.example.app1/res/mipmap-mdpi')}") + print(f"File exists: {image_path in uos.listdir('/apps/com.example.app1/res/mipmap-mdpi')}") +except Exception as e: + print(f"File system error: {e}") + +# Create a label to ensure something displays +label = lv.label(lv.screen_active()) +label.set_text("Loading image...") +label.align(lv.ALIGN.TOP_MID, 0, 10) +print("Label created") + +# Load the image + + +# Load and display the image +print("Creating image widget...") +image = lv.image(lv.screen_active()) +try: + print(f"Loading image: {image_path}") + image.set_src(image_path) + image.align(lv.ALIGN.CENTER, 0, 0) + print("Image loaded and aligned") +except Exception as e: + print(f"Image load error: {e}") + print("Falling back to SYMBOL.DUMMY") + image.set_src(lv.SYMBOL.DUMMY) + image.align(lv.ALIGN.CENTER, 0, 0) + +# Debug image properties +print(f"Screen active: {lv.screen_active()}") +print(f"Image widget: {image}") +print(f"Image source: {image.get_src()}") +print(f"Image size: {image.get_width()}x{image.get_height()}") + + + +wifi_icon = lv.label(lv.screen_active()) +wifi_icon.set_text(lv.SYMBOL.WIFI) +wifi_icon.align(lv.ALIGN.RIGHT_CENTER, 0, 0) +wifi_icon.set_style_text_color(COLOR_TEXT_WHITE, 0) + + + + + +# Create a label +label = lv.label(subwindow) +label.set_text("Monospace Text") +label.align(lv.ALIGN.CENTER, 0, -40) + +# Create a style for the label +style = lv.style_t() +style.init() +style.set_text_font(lv.font_montserrat_12) +label.add_style(style, 0) # Apply style to the label + + +# Create a label +label = lv.label(subwindow) +label.set_text("Monospace Text") +label.align(lv.ALIGN.CENTER, 0, -20) + +# Create a style for the label +style = lv.style_t() +style.init() +style.set_text_font(lv.font_montserrat_14) # Default font +label.add_style(style, 0) # Apply style to the label + + + +# Create a label +label = lv.label(subwindow) +label.set_text("Monospace Text") +label.align(lv.ALIGN.CENTER, 0, 0) + +# Create a style for the label +style = lv.style_t() +style.init() +style.set_text_font(lv.font_montserrat_16) +label.add_style(style, 0) # Apply style to the label + + +# Create a label +label = lv.label(subwindow) +label.set_text("Monospace Text") +label.align(lv.ALIGN.CENTER, 0, 40) + +# Create a style for the label +style = lv.style_t() +style.init() +style.set_text_font(lv.font_unscii_8) # Set monospace font +label.add_style(style, 0) # Apply style to the label + +# Create a label +label = lv.label(subwindow) +label.set_text("Monospace Text") +label.align(lv.ALIGN.CENTER, 0, 60) + +# Create a style for the label +style = lv.style_t() +style.init() +style.set_text_font(lv.font_unscii_16) # Set monospace font +label.add_style(style, 0) # Apply style to the label + + + + + +# delete folder: +import mip +mip.install("shutil") +import shutil +shutil.rmtree('/apps/com.example.files') + + + +import vfs +vfs.umount('/') +vfs.VfsLfs2.mkfs(bdev) +vfs.mount(bdev, '/') + + + +def memoryview_to_hex_spaced(mv: memoryview) -> str: + """Convert the first 50 bytes of a memoryview to a spaced hex string.""" + sliced = mv[:50] + return ' '.join('{:02x}'.format(b & 0xFF) for b in sliced) + + + + + + + + + + + + + + + + +subwindow.clean() +canary = lv.obj(subwindow) +canary.add_flag(lv.obj.FLAG.HIDDEN) + +subwindow.set_style_bg_color(lv.color_black(), 0) + +label = lv.label(subwindow) +label.set_text("Hello \uf0ac") # Unicode for a symbol (e.g., globe) +label.align(lv.ALIGN.CENTER, 0, 0) + +label2 = lv.label(subwindow) +label2.set_text("Hello " + lv.SYMBOL.DOWNLOAD) +label2.align(lv.ALIGN.CENTER, 0, 25) + + +label2 = lv.label(subwindow) +label2.set_text("Hello " + lv.SYMBOL.DUMMY) +label2.align(lv.ALIGN.CENTER, 0, 50) + + +image = lv.image(subwindow) +image.align(lv.ALIGN.CENTER, 0, -50) +image.set_src(lv.SYMBOL.STOP) # Or use a default image + + + + + +>>> import ota.update +>>> ota.update.from_file("https://demo.lnpiggy.com/static/firmware/ESP32_GENERIC_S3-SPIRAM_OCT_micropython.bin") + + +temp_zip_path = "/apps/temp.zip" +print('\nReading file') +with ZipFile(temp_zip_path) as myzip: + with myzip.open('com.example.files/META-INF/MANIFEST.MF') as myfile: + print(myfile.read()) + + + +import os +try: + import zipfile +except ImportError: + zipfile = None + + +temp_zip_path = "/apps/temp.zip" +print('\nReading file') +with zipfile.ZipFile(temp_zip_path) as myzip: + with myzip.open('com.example.files/assets/files.py') as myfile: + print(myfile.read()) + + +import os +try: + import zipfile +except ImportError: + zipfile = None + +temp_zip_path = "/apps/temp.zip" +print(f"Stat says: {os.stat(temp_zip_path)}") +with zipfile.ZipFile(temp_zip_path, "r") as zip_ref: + print("extracting...") + zip_ref.extractall("/apps") + + + + + + + + + + + +subwindow = lv.obj(newscreen) +subwindow.set_size(TFT_HOR_RES, TFT_VER_RES - NOTIFICATION_BAR_HEIGHT) +subwindow.set_pos(0, NOTIFICATION_BAR_HEIGHT) +subwindow.set_style_border_width(0, 0) +subwindow.set_style_pad_all(0, 0) +lv.screen_load(newscreen) +script_globals = { + 'lv': lv, + 'appscreen': newscreen, + 'subwindow': subwindow, + 'start_app': start_app, # for launcher apps + 'parse_manifest': parse_manifest, # for launcher apps + '__name__': "__main__" +} diff --git a/draft_code/saved_uasyncio_demo.py b/draft_code/saved_uasyncio_demo.py new file mode 100644 index 00000000..8c9f2706 --- /dev/null +++ b/draft_code/saved_uasyncio_demo.py @@ -0,0 +1,133 @@ +import lvgl as lv +import uasyncio as asyncio +import utime +import gc + +# Create a subwindow for the child script (half the 320x240 display) +screen = lv.screen_active() +subwindow = lv.obj(screen) +subwindow.set_size(160, 240) # Half width, full height +subwindow.align(lv.ALIGN.LEFT_MID, 0, 0) # Left side +subwindow.set_style_bg_color(lv.color_hex(0xDDDDDD), lv.PART.MAIN) + +# Create a label for parent updates +parent_label = lv.label(screen) +parent_label.set_text("Parent: 0") +parent_label.set_style_text_font(lv.font_montserrat_12, 0) +parent_label.align(lv.ALIGN.TOP_RIGHT, -10, 10) + +# Create a parent button +parent_button = lv.button(screen) +parent_button.set_size(80, 40) +parent_button.align(lv.ALIGN.BOTTOM_RIGHT, -10, -50) +parent_button_label = lv.label(parent_button) +parent_button_label.set_text("Parent Btn") +parent_button_label.set_style_text_font(lv.font_montserrat_12, 0) + +# Create a parent slider +parent_slider = lv.slider(screen) +parent_slider.set_size(100, 10) +parent_slider.set_range(0, 100) +parent_slider.align(lv.ALIGN.BOTTOM_RIGHT, -10, -10) + +# Parent button callback +def parent_button_cb(e): + print("Parent button clicked") + +parent_button.add_event_cb(parent_button_cb, lv.EVENT.CLICKED, None) + +# Parent slider callback +def parent_slider_cb(e): + value = parent_slider.get_value() + print("Parent slider value:", value) + +parent_slider.add_event_cb(parent_slider_cb, lv.EVENT.VALUE_CHANGED, None) + +# Function to execute the child script as a coroutine +async def execute_script(script_source, lvgl_obj): + try: + script_globals = { + 'lv': lv, + 'subwindow': lvgl_obj, + 'asyncio': asyncio, + 'utime': utime + } + print("Child script: Compiling") + code = compile(script_source, "", "exec") + exec(code, script_globals) + update_child = script_globals.get('update_child') + if update_child: + print("Child script: Starting update_child") + await update_child() + else: + print("Child script error: No update_child function defined") + except Exception as e: + print("Child script error:", e) + +# Child script buffer: updates label, adds button and slider +script_buffer = """ +import asyncio +async def update_child(): + print("Child coroutine: Creating UI") + # Label + label = lv.label(subwindow) + label.set_text("Child: 0") + label.set_style_text_font(lv.font_montserrat_12, 0) + label.align(lv.ALIGN.TOP_MID, 0, 10) + # Button + button = lv.button(subwindow) + button.set_size(80, 40) + button.align(lv.ALIGN.BOTTOM_MID, 0, -50) + button_label = lv.label(button) + button_label.set_text("Child Btn") + button_label.set_style_text_font(lv.font_montserrat_12, 0) + # Slider + slider = lv.slider(subwindow) + slider.set_size(100, 10) + slider.set_range(0, 100) + slider.align(lv.ALIGN.BOTTOM_MID, 0, -10) + # Button callback + def button_cb(e): + print("Child button clicked") + button.add_event_cb(button_cb, lv.EVENT.CLICKED, None) + # Slider callback + def slider_cb(e): + value = slider.get_value() + print("Child slider value:", value) + slider.add_event_cb(slider_cb, lv.EVENT.VALUE_CHANGED, None) + # Update loop + count = 0 + while True: + count += 1 + print("Child coroutine: Updating label to", count) + label.set_text(f"Child: {count}") + await asyncio.sleep_ms(2000) # Update every 2s +""" + +# Parent coroutine: updates parent label every 1 second +async def update_parent(): + count = 0 + while True: + count += 1 + print("Parent coroutine: Updating label to", count) + parent_label.set_text(f"Parent: {count}") + gc.collect() + print("Parent coroutine: Free memory:", gc.mem_free()) + await asyncio.sleep_ms(1000) # Update every 1s + +# Main async function to run all tasks +async def main(): + print("Main: Starting tasks") + asyncio.create_task(update_parent()) + asyncio.create_task(execute_script(script_buffer, subwindow)) + while True: + await asyncio.sleep_ms(100) + +# Run the event loop +gc.collect() +print("Free memory before loop:", gc.mem_free()) +try: + asyncio.run(main()) +except Exception as e: + print("Main error:", e) + diff --git a/draft_code/simple_thread.py b/draft_code/simple_thread.py new file mode 100644 index 00000000..ab6c8960 --- /dev/null +++ b/draft_code/simple_thread.py @@ -0,0 +1,77 @@ +import _thread + +# Function to execute the child script as a coroutine +def execute_script(script_source, lvgl_obj): + try: + script_globals = { + 'lv': lv, + 'subwindow': lvgl_obj, + } + print("Child script: Compiling") + code = compile(script_source, "", "exec") + exec(code, script_globals) + app_main = script_globals.get('app_main') + if app_main: + print("Child script: Starting app_main") + app_main() + print("Script finished!") + else: + print("Child script error: No app_main function defined") + except Exception as e: + print("Child script error:", e) + + +# Child script buffer: updates label, adds button and slider +script_buffer = """ +import time +def app_main(): + print("Child coroutine: Creating UI") + # Label + label = lv.label(subwindow) + label.set_text("Child: 0") + label.set_style_text_font(lv.font_montserrat_12, 0) + label.align(lv.ALIGN.TOP_MID, 0, 10) + # Button + button = lv.button(subwindow) + button.set_size(80, 40) + button.align(lv.ALIGN.CENTER, 0, 0) + button_label = lv.label(button) + button_label.set_text("Quit") + button_label.set_style_text_font(lv.font_montserrat_12, 0) + # Slider + slider = lv.slider(subwindow) + slider.set_range(0, 100) + slider.align(lv.ALIGN.BOTTOM_MID, 0, -30) + # Quit flag + should_continue = True + # Button callback + def button_cb(e): + nonlocal should_continue + print("Quit button clicked, exiting child") + should_continue = False + button.add_event_cb(button_cb, lv.EVENT.CLICKED, None) + # Slider callback + def slider_cb(e): + value = slider.get_value() + print("Child slider value:", value) + slider.add_event_cb(slider_cb, lv.EVENT.VALUE_CHANGED, None) + # Update loop + count = 0 + while should_continue: + count += 1 + print("Child coroutine: Updating label to", count) + label.set_text(f"Child: {count}") + time.sleep_ms(1000) + print("Child coroutine: Exiting") +""" + + +# Start the event loop in a background thread +gc.collect() +print("Free memory before loop:", gc.mem_free()) +try: + _thread.stack_size(8192) + _thread.start_new_thread(execute_script, (script_buffer, subwindow)) + print("Event loop started in background thread") +except Exception as e: + print("Error starting event loop thread:", e) diff --git a/draft_code/video_player/README.md b/draft_code/video_player/README.md new file mode 100644 index 00000000..d9a12061 --- /dev/null +++ b/draft_code/video_player/README.md @@ -0,0 +1,2 @@ +This doesn't work because the build with ffmpeg in it fails, +because it needs that STDIO MEM allocator in lib/lv_conf.h and that has a compilation issue. diff --git a/draft_code/video_player/convert_video.sh b/draft_code/video_player/convert_video.sh new file mode 100755 index 00000000..324594a2 --- /dev/null +++ b/draft_code/video_player/convert_video.sh @@ -0,0 +1,3 @@ +# https://sample-videos.com/ + +ffmpeg -i SampleVideo_640x360_1mb.mp4 -c:v mjpeg -q:v 7 -vf "fps=15,scale=320:180:flags=lanczos" -c:a pcm_u8 video_320x180.avi diff --git a/trash/appstore.py b/trash/appstore.py deleted file mode 100644 index e48c4788..00000000 --- a/trash/appstore.py +++ /dev/null @@ -1,538 +0,0 @@ -from machine import Pin, SPI -import st7789 -import lcd_bus -import machine -import cst816s -import i2c -import urequests - -import lvgl as lv -import task_handler - -# Pin configuration -SPI_BUS = 2 -SPI_FREQ = 40000000 -LCD_SCLK = 39 -LCD_MOSI = 38 -LCD_MISO = 40 -LCD_DC = 42 -LCD_CS = 45 -LCD_BL = 1 - -I2C_BUS = 0 -I2C_FREQ = 100000 -TP_SDA = 48 -TP_SCL = 47 -TP_ADDR = 0x15 -TP_REGBITS = 8 - -TFT_HOR_RES=320 -TFT_VER_RES=240 - -#lv.init() not needed -spi_bus = machine.SPI.Bus( - host=SPI_BUS, - mosi=LCD_MOSI, - miso=LCD_MISO, - sck=LCD_SCLK -) -display_bus = lcd_bus.SPIBus( - spi_bus=spi_bus, - freq=SPI_FREQ, - dc=LCD_DC, - cs=LCD_CS, -) -display = st7789.ST7789( - data_bus=display_bus, - display_width=TFT_VER_RES, - display_height=TFT_HOR_RES, - backlight_pin=LCD_BL, - backlight_on_state=STATE_PWM, - color_space=lv.COLOR_FORMAT.RGB565, - color_byte_order=st7789.BYTE_ORDER_BGR, - rgb565_byte_swap=True, -) -display.init() -display.set_power(True) -display.set_backlight(100) - -# Touch handling: -i2c_bus = i2c.I2C.Bus(host=I2C_BUS, scl=TP_SCL, sda=TP_SDA, freq=I2C_FREQ, use_locks=False) -touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=TP_ADDR, reg_bits=TP_REGBITS) -indev=cst816s.CST816S(touch_dev,startup_rotation=lv.DISPLAY_ROTATION._180) # button in top left, good - -task_handler.TaskHandler() - -#def task_thread(): -# task_handler.TaskHandler() -#import _thread -#_thread.start_new_thread(task_thread, ()) - -display.set_rotation(lv.DISPLAY_ROTATION._90) - -# Custom touch interrupt handler: -indev._write_reg(0xEC,0x06) -indev._write_reg(0xFA,0x50) -irq_pin=machine.Pin(46,machine.Pin.IN,machine.Pin.PULL_UP) -# gesture ids: -# 0: press -# 1: swipe from left to USB port -# 2: swipe from USB port to left -# 3: top to bottom -# 4: bottom to top -# 5: release -# 12: long press -def handle_gesture(pin): - indev._read_reg(0x01) - gesture_id=indev._rx_buf[0] - indev._read_reg(0x02) - finger_num=indev._rx_buf[0] - indev._read_reg(0x03) - x_h=indev._rx_buf[0] - indev._read_reg(0x04) - x_l=indev._rx_buf[0] - x=((x_h&0x0F)<<8)|x_l - indev._read_reg(0x05) - y_h=indev._rx_buf[0] - indev._read_reg(0x06) - y_l=indev._rx_buf[0] - y=((y_h&0x0F)<<8)|y_l - #print(f"GestureID={gesture_id},FingerNum={finger_num},X={x},Y={y}") - if gesture_id==0x04: - #print("Swipe Up Detected") - close_drawer() - elif gesture_id==0x03: - #print("Swipe Down Detected") - open_drawer() - -irq_pin.irq(trigger=machine.Pin.IRQ_FALLING,handler=handle_gesture) - - - - - - - -# GUI: -# Below works at https://sim.lvgl.io/v9.0/micropython/ports/webassembly/index.html - -import time - -# Constants -TFT_HOR_RES=320 -TFT_VER_RES=240 -NOTIFICATION_BAR_HEIGHT=24 -BUTTON_WIDTH=100 -BUTTON_HEIGHT=40 -PADDING_TINY=5 -PADDING_SMALL=10 -PADDING_MEDIUM=20 -PADDING_LARGE=30 -DRAWER_ANIM_DURATION=300 -SLIDER_MIN_VALUE=1 -SLIDER_MAX_VALUE=100 -SLIDER_DEFAULT_VALUE=80 -OFFSET_WIFI_ICON = -60 -OFFSET_BATTERY_ICON = -40 -TIME_UPDATE_INTERVAL = 1000 - -# Color palette -DARKPINK = lv.color_hex(0xEC048C) -MEDIUMPINK = lv.color_hex(0xF480C5) -LIGHTPINK = lv.color_hex(0xF9E9F2) -DARKYELLOW = lv.color_hex(0xFBDC05) -LIGHTYELLOW = lv.color_hex(0xFBE499) -PUREBLACK = lv.color_hex(0x000000) - -COLOR_DRAWER_BG=MEDIUMPINK -COLOR_TEXT_WHITE=LIGHTPINK -COLOR_NOTIF_BAR_BG = DARKPINK -COLOR_DRAWER_BUTTON_BG=DARKYELLOW -COLOR_DRAWER_BUTTONTEXT=PUREBLACK -COLOR_SLIDER_BG=LIGHTPINK -COLOR_SLIDER_KNOB=DARKYELLOW -COLOR_SLIDER_INDICATOR=LIGHTPINK - - - -drawer=None -wifi_screen=None -drawer_open=False - -scr = lv.screen_active() -scr.set_style_bg_color(lv.color_hex(0x000000), 0) - - - -def open_drawer(): - global drawer_open - if not drawer_open: - drawer.set_y(NOTIFICATION_BAR_HEIGHT) - drawer_open=True - - -def close_drawer(): - global drawer_open - if drawer_open: - drawer.set_y(-TFT_VER_RES+NOTIFICATION_BAR_HEIGHT) - drawer_open=False - - -def toggle_drawer(event): - global drawer_open - if drawer_open: - close_drawer() - else: - open_drawer() - -# Create notification bar object -notification_bar = lv.obj(lv.screen_active()) -notification_bar.set_style_bg_color(COLOR_NOTIF_BAR_BG, 0) -notification_bar.set_size(320, NOTIFICATION_BAR_HEIGHT) -notification_bar.set_pos(0, 0) -notification_bar.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) -notification_bar.set_scroll_dir(lv.DIR.VER) -notification_bar.set_style_border_width(0, 0) -notification_bar.set_style_radius(0, 0) -# Time label -time_label = lv.label(notification_bar) -time_label.set_text("12:00") -time_label.align(lv.ALIGN.LEFT_MID, PADDING_TINY, 0) -time_label.set_style_text_color(COLOR_TEXT_WHITE, 0) -# Notification icon (bell) -notif_icon = lv.label(notification_bar) -notif_icon.set_text(lv.SYMBOL.BELL) -notif_icon.align_to(time_label, lv.ALIGN.OUT_RIGHT_MID, PADDING_LARGE, 0) -notif_icon.set_style_text_color(COLOR_TEXT_WHITE, 0) -# WiFi icon -wifi_icon = lv.label(notification_bar) -wifi_icon.set_text(lv.SYMBOL.WIFI) -wifi_icon.align(lv.ALIGN.RIGHT_MID, OFFSET_WIFI_ICON, 0) -wifi_icon.set_style_text_color(COLOR_TEXT_WHITE, 0) -# Battery icon -battery_icon = lv.label(notification_bar) -battery_icon.set_text(lv.SYMBOL.BATTERY_FULL) -battery_icon.align(lv.ALIGN.RIGHT_MID, OFFSET_BATTERY_ICON, 0) -battery_icon.set_style_text_color(COLOR_TEXT_WHITE, 0) -# Battery percentage -battery_label = lv.label(notification_bar) -battery_label.set_text("100%") -battery_label.align(lv.ALIGN.RIGHT_MID, 0, 0) -battery_label.set_style_text_color(COLOR_TEXT_WHITE, 0) -# Timer to update time every second -def update_time(timer): - ticks = time.ticks_ms() - hours = (ticks // 3600000) % 24 - minutes = (ticks // 60000) % 60 - seconds = (ticks // 1000) % 60 - time_label.set_text(f"{hours:02d}:{minutes:02d}:{seconds:02d}") -lv.timer_create(update_time, TIME_UPDATE_INTERVAL, None) -notification_bar.add_event_cb(toggle_drawer, lv.EVENT.CLICKED, None) - - - - - -# Subwindow is created before drawer so that drawer is on top -screen = lv.screen_active() -subwindow = lv.obj(screen) -subwindow.set_size(TFT_HOR_RES, TFT_VER_RES - NOTIFICATION_BAR_HEIGHT) -subwindow.set_pos(0, NOTIFICATION_BAR_HEIGHT) -subwindow.set_style_border_width(0, 0) -subwindow.set_style_pad_all(0, 0) - - - -def create_drawer(): - global drawer,wifi_screen - drawer=lv.obj(lv.screen_active()) - drawer.set_size(TFT_HOR_RES,TFT_VER_RES-NOTIFICATION_BAR_HEIGHT) - drawer.set_pos(0,-TFT_VER_RES+NOTIFICATION_BAR_HEIGHT) - drawer.set_style_bg_color(COLOR_DRAWER_BG,0) - drawer.set_scroll_dir(lv.DIR.NONE) - slider=lv.slider(drawer) - slider.set_range(SLIDER_MIN_VALUE,SLIDER_MAX_VALUE) - slider.set_value(SLIDER_DEFAULT_VALUE,False) - slider.set_width(TFT_HOR_RES-PADDING_MEDIUM) - slider.align(lv.ALIGN.TOP_MID,0,PADDING_SMALL) - slider.set_style_bg_color(COLOR_SLIDER_BG,lv.PART.MAIN) - slider.set_style_bg_color(COLOR_SLIDER_INDICATOR,lv.PART.INDICATOR) - slider.set_style_bg_color(COLOR_SLIDER_KNOB,lv.PART.KNOB) - slider_label=lv.label(drawer) - slider_label.set_text("80%") - slider_label.set_style_text_color(COLOR_TEXT_WHITE,0) - slider_label.align_to(slider,lv.ALIGN.OUT_TOP_MID,0,-5) - # works here - def slider_event(e): - slider=e.get_target() - label=e.get_user_data() - value=slider.get_value() - label.set_text(f"{value}%") - display.set_backlight(value) - # this crashes it: slider.add_event_cb(slider_event,lv.EVENT.VALUE_CHANGED,slider_label) - # this crashes it: slider.add_event_cb(slider_event,lv.EVENT.VALUE_CHANGED,slider_label) - wifi_btn=lv.button(drawer) - wifi_btn.set_size(BUTTON_WIDTH,BUTTON_HEIGHT) - wifi_btn.align(lv.ALIGN.LEFT_MID,PADDING_SMALL,0) - wifi_btn.set_style_bg_color(COLOR_DRAWER_BUTTON_BG,0) - wifi_label=lv.label(wifi_btn) - wifi_label.set_text(lv.SYMBOL.WIFI+" WiFi") - wifi_label.center() - wifi_label.set_style_text_color(COLOR_DRAWER_BUTTONTEXT,0) - def wifi_event(e): - global drawer_open - #wifi_screen.set_y(0) # TODO: make this - close_drawer() - drawer_open=False - wifi_btn.add_event_cb(wifi_event,lv.EVENT.CLICKED,None) - # - settings_btn=lv.button(drawer) - settings_btn.set_size(BUTTON_WIDTH,BUTTON_HEIGHT) - settings_btn.align(lv.ALIGN.RIGHT_MID,-PADDING_SMALL,0) - settings_btn.set_style_bg_color(COLOR_DRAWER_BUTTON_BG,0) - settings_label=lv.label(settings_btn) - settings_label.set_text(lv.SYMBOL.SETTINGS+" Settings") - settings_label.center() - settings_label.set_style_text_color(COLOR_DRAWER_BUTTONTEXT,0) - def settings_event(e): - global drawer_open - close_drawer() - drawer_open=False - settings_btn.add_event_cb(settings_event,lv.EVENT.CLICKED,None) - # - launcher_btn=lv.button(drawer) - launcher_btn.set_size(BUTTON_WIDTH,BUTTON_HEIGHT) - launcher_btn.align(lv.ALIGN.BOTTOM_LEFT,PADDING_SMALL,0) - launcher_btn.set_style_bg_color(COLOR_DRAWER_BUTTON_BG,0) - launcher_label=lv.label(launcher_btn) - launcher_label.set_text(lv.SYMBOL.HOME+" Launcher") - launcher_label.center() - launcher_label.set_style_text_color(COLOR_DRAWER_BUTTONTEXT,0) - def launcher_event(e): - print("Launcher button pressed!") - global drawer_open - close_drawer() - drawer_open=False - run_app(launcher_script,False) - launcher_btn.add_event_cb(launcher_event,lv.EVENT.CLICKED,None) - # - restart_btn=lv.button(drawer) - restart_btn.set_size(BUTTON_WIDTH,BUTTON_HEIGHT) - restart_btn.align(lv.ALIGN.BOTTOM_RIGHT,-PADDING_SMALL,0) - restart_btn.set_style_bg_color(COLOR_DRAWER_BUTTON_BG,0) - restart_label=lv.label(restart_btn) - restart_label.set_text(lv.SYMBOL.POWER+" Reset") - restart_label.center() - restart_label.set_style_text_color(COLOR_DRAWER_BUTTONTEXT,0) - def launcher_event(e): - print("Reset button pressed!") - global drawer_open - close_drawer() - drawer_open=False - run_app(launcher_script,False) - restart_btn.add_event_cb(lambda event: machine.reset(),lv.EVENT.CLICKED,None) - - -create_drawer() - - - - - - -# uasyncio and _thread aren't available on web - -import _thread - -# Function to execute the child script as a coroutine -def execute_script(script_source, is_file, lvgl_obj, return_to_launcher): - thread_id = _thread.get_ident(); - print(f"Thread {thread_id}: executing script") - try: - script_globals = { - 'lv': lv, - 'subwindow': lvgl_obj, - 'run_app': run_app, - 'app1_script': app1_script, - 'app2_script': app2_script - } - if is_file: - print(f"Thread {thread_id}: reading script from file: {script_source}") - with open(script_source, 'r') as f: - script_source = f.read() - print(f"Thread {thread_id}: starting script") - exec(script_source, script_globals) - print(f"Thread {thread_id}: script finished") - if return_to_launcher: - print(f"Thread {thread_id}: running launcher_script") - run_app(launcher_script,False,False) - except Exception as e: - print(f"Thread {thread_id}: error ", e) - - -def run_app(scriptname,is_file,return_to_launcher=True): - # Start the event loop in a background thread - gc.collect() - print("Free memory before starting new script thread:", gc.mem_free()) - try: - subwindow.clean() - # 168KB maximum at startup but 136KB after loading display, drivers, LVGL gui etc so let's go for 128KB for now, still a lot... - # But then no additional threads can be created. So 32KB seems like a good balance, allowing for 4 threads in apps... - #_thread.stack_size(32768) - _thread.stack_size(16384) - _thread.start_new_thread(execute_script, (scriptname, False, subwindow, return_to_launcher)) - print("Event loop started in background thread") - except Exception as e: - print("Error starting event loop thread:", e) - - -# app1: updates label, adds button and slider -app1_script = """ -import time -print("Child coroutine: Creating UI") -# Label -label = lv.label(subwindow) -label.set_text("App1: 0") -label.align(lv.ALIGN.TOP_MID, 0, 10) -# Button -button = lv.button(subwindow) -button.set_size(100, 60) -button.align(lv.ALIGN.CENTER, 0, 0) -button_label = lv.label(button) -button_label.set_text("Quit") -button_label.center() -# Slider -slider = lv.slider(subwindow) -slider.set_range(0, 100) -slider.set_value(50, lv.ANIM.OFF) -slider.align(lv.ALIGN.BOTTOM_MID, 0, -30) -# Quit flag -should_continue = True -# Button callback -def button_cb(e): - global should_continue - print("Quit button clicked, exiting child") - should_continue = False -button.add_event_cb(button_cb, lv.EVENT.CLICKED, None) -# Slider callback -def slider_cb(e): - value = slider.get_value() - #print("Child slider value:", value) -slider.add_event_cb(slider_cb, lv.EVENT.VALUE_CHANGED, None) -# Update loop -count = 0 -while should_continue: - count += 1 - #print("Child coroutine: Updating label to", count) - label.set_text(f"App1: {count}") - time.sleep_ms(100) # shorter makes it more responive to the quit button -print("Child coroutine: Exiting") -""" - -app2_script = """ -import time -import _thread -print("App2 running") - -# Quit flag -should_continue = True - -canary = lv.obj(subwindow) -canary.add_flag(0x0001) # LV_OBJ_FLAG_HIDDEN is 0x0001 (don't know why I can't find it!) - -def app2_thread(): - count=0 - while should_continue and canary.get_class(): - print(f"app2_thread: thread_id {_thread.get_ident()} - {count}") - count+=1 - time.sleep(1) - -_thread.start_new_thread(app2_thread, ()) - - -# Label -label = lv.label(subwindow) -label.set_text("App2: 0") -label.align(lv.ALIGN.TOP_MID, 0, 10) -# Button -button = lv.button(subwindow) -button.set_size(100, 60) -button.align(lv.ALIGN.CENTER, 0, 0) -button_label = lv.label(button) -button_label.set_text("Quit") -button_label.center() -# Slider -slider = lv.slider(subwindow) -slider.set_range(0, 100) -slider.set_value(50, lv.ANIM.OFF) -slider.align(lv.ALIGN.BOTTOM_MID, 0, -30) -# Button callback -def button_cb(e): - global should_continue - print("Quit button clicked, exiting child") - should_continue = False -button.add_event_cb(button_cb, lv.EVENT.CLICKED, None) -# Slider callback -def slider_cb(e): - value = slider.get_value() - #print("Child slider value:", value) -slider.add_event_cb(slider_cb, lv.EVENT.VALUE_CHANGED, None) -# Update loop -count = 0 -while should_continue: - count += 1 - #print("Child coroutine: Updating label to", count) - label.set_text(f"App2: {count}") - time.sleep_ms(1000) # shorter makes it more responive to the quit button -print("Child coroutine: Exiting") -""" - - -launcher_script = """ -print("Launcher script running") -app1_button = lv.button(subwindow) -app1_button.set_size(120, 40) -app1_button.align(lv.ALIGN.LEFT_MID, 20, 0) -app1_button_label = lv.label(app1_button) -app1_button_label.set_text("Start App 1") -app1_button_label.center() -app1_button.add_event_cb(lambda event: run_app(app1_script,False), lv.EVENT.CLICKED, None) -app2_button = lv.button(subwindow) -app2_button.set_size(120, 40) -app2_button.align(lv.ALIGN.RIGHT_MID, -20, 0) -app2_button_label = lv.label(app2_button) -app2_button_label.set_text("Start App 2") -app2_button_label.center() -app2_button.add_event_cb(lambda event: run_app(app2_script,False), lv.EVENT.CLICKED, None) -print("Launcher script exiting") -""" - - -run_app(launcher_script,False,False) - - - - -import network -import time - -# Connect to Wi-Fi -def connect_wifi(): - wlan = network.WLAN(network.STA_IF) - wlan.active(True) - wlan.connect("SSIDHERE", "PASSWORDHERE") - print("Connecting to Wi-Fi...", end="") - for _ in range(30): # Wait up to 30 seconds - if wlan.isconnected(): - print(" Connected!") - print("IP:", wlan.ifconfig()[0]) - return True - time.sleep(1) - print(".", end="") - print(" Failed to connect!") - return False - - -#connect_wifi() - -