From 68d0ac572d6c1f17eca4ec4b3089029dc29ac84f Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 6 Jan 2026 13:48:01 +0100 Subject: [PATCH] Wifi app: add support for scanning wifi QR codes to "Add Network" --- CHANGELOG.md | 2 + .../assets/camera_app.py | 610 ++++++++++++++++++ .../assets/camera_settings.py | 604 +++++++++++++++++ .../com.micropythonos.wifi/assets/wifi.py | 54 +- 4 files changed, 1261 insertions(+), 9 deletions(-) create mode 100644 internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/camera_app.py create mode 100644 internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/camera_settings.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bfbaf7d..f34f12c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ 0.5.3 ===== - App framework: simplify MANIFEST.JSON +- Simplify: don't rate-limit update_ui_threadsafe_if_foreground - WiFi app: check "hidden" in EditNetwork +- Wifi app: add support for scanning wifi QR codes to "Add Network" 0.5.2 ===== diff --git a/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/camera_app.py b/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/camera_app.py new file mode 100644 index 00000000..23675283 --- /dev/null +++ b/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/camera_app.py @@ -0,0 +1,610 @@ +import lvgl as lv +import time + +try: + import webcam +except Exception as e: + print(f"Info: could not import webcam module: {e}") + +import mpos.time +from mpos.apps import Activity +from mpos.content.intent import Intent + +from camera_settings import CameraSettingsActivity + +class CameraApp(Activity): + + PACKAGE = "com.micropythonos.camera" + CONFIGFILE = "config.json" + SCANQR_CONFIG = "config_scanqr_mode.json" + + button_width = 75 + button_height = 50 + + STATUS_NO_CAMERA = "No camera found." + STATUS_SEARCHING_QR = "Searching QR codes...\n\nHold still and try varying scan distance (10-25cm) and make the QR code big (4-12cm). Ensure proper lighting." + STATUS_FOUND_QR = "Found QR, trying to decode... hold still..." + + cam = None + current_cam_buffer = None # Holds the current memoryview to prevent garba + width = None + height = None + colormode = False + + image_dsc = None + scanqr_mode = False + scanqr_intent = False + use_webcam = False + capture_timer = None + + prefs = None # regular prefs + scanqr_prefs = None # qr code scanning prefs + + # Widgets: + main_screen = None + image = None + qr_label = None + qr_button = None + snap_button = None + status_label = None + status_label_cont = None + + def onCreate(self): + self.main_screen = lv.obj() + self.main_screen.set_style_pad_all(1, 0) + self.main_screen.set_style_border_width(0, 0) + self.main_screen.set_size(lv.pct(100), lv.pct(100)) + self.main_screen.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) + # Initialize LVGL image widget + self.image = lv.image(self.main_screen) + self.image.align(lv.ALIGN.LEFT_MID, 0, 0) + close_button = lv.button(self.main_screen) + close_button.set_size(self.button_width, self.button_height) + close_button.align(lv.ALIGN.TOP_RIGHT, 0, 0) + close_label = lv.label(close_button) + close_label.set_text(lv.SYMBOL.CLOSE) + close_label.center() + close_button.add_event_cb(lambda e: self.finish(),lv.EVENT.CLICKED,None) + # Settings button + settings_button = lv.button(self.main_screen) + settings_button.set_size(self.button_width, self.button_height) + settings_button.align_to(close_button, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + settings_label = lv.label(settings_button) + settings_label.set_text(lv.SYMBOL.SETTINGS) + settings_label.center() + settings_button.add_event_cb(lambda e: self.open_settings(),lv.EVENT.CLICKED,None) + #self.zoom_button = lv.button(self.main_screen) + #self.zoom_button.set_size(self.button_width, self.button_height) + #self.zoom_button.align(lv.ALIGN.RIGHT_MID, 0, self.button_height + 5) + #self.zoom_button.add_event_cb(self.zoom_button_click,lv.EVENT.CLICKED,None) + #zoom_label = lv.label(self.zoom_button) + #zoom_label.set_text("Z") + #zoom_label.center() + self.qr_button = lv.button(self.main_screen) + self.qr_button.set_size(self.button_width, self.button_height) + self.qr_button.add_flag(lv.obj.FLAG.HIDDEN) + self.qr_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) + self.qr_button.add_event_cb(self.qr_button_click,lv.EVENT.CLICKED,None) + self.qr_label = lv.label(self.qr_button) + self.qr_label.set_text(lv.SYMBOL.EYE_OPEN) + self.qr_label.center() + + self.snap_button = lv.button(self.main_screen) + self.snap_button.set_size(self.button_width, self.button_height) + self.snap_button.align_to(self.qr_button, lv.ALIGN.OUT_TOP_MID, 0, -10) + self.snap_button.add_flag(lv.obj.FLAG.HIDDEN) + self.snap_button.add_event_cb(self.snap_button_click,lv.EVENT.CLICKED,None) + snap_label = lv.label(self.snap_button) + snap_label.set_text(lv.SYMBOL.OK) + snap_label.center() + + + self.status_label_cont = lv.obj(self.main_screen) + width = mpos.ui.pct_of_display_width(70) + height = mpos.ui.pct_of_display_width(60) + self.status_label_cont.set_size(width,height) + center_w = round((mpos.ui.pct_of_display_width(100) - self.button_width - 5 - width)/2) + center_h = round((mpos.ui.pct_of_display_height(100) - height)/2) + self.status_label_cont.set_pos(center_w,center_h) + self.status_label_cont.set_style_bg_color(lv.color_white(), 0) + self.status_label_cont.set_style_bg_opa(66, 0) + self.status_label_cont.set_style_border_width(0, 0) + self.status_label = lv.label(self.status_label_cont) + self.status_label.set_text(self.STATUS_NO_CAMERA) + self.status_label.set_long_mode(lv.label.LONG_MODE.WRAP) + self.status_label.set_width(lv.pct(100)) + self.status_label.center() + self.setContentView(self.main_screen) + + def onResume(self, screen): + self.scanqr_intent = self.getIntent().extras.get("scanqr_intent") + self.status_label_cont.add_flag(lv.obj.FLAG.HIDDEN) + if self.scanqr_mode or self.scanqr_intent: + self.start_qr_decoding() + if not self.cam and self.scanqr_mode: + print("No camera found, stopping camera app") + self.finish() + else: + self.load_settings_cached() + self.start_cam() + self.qr_button.remove_flag(lv.obj.FLAG.HIDDEN) + self.snap_button.remove_flag(lv.obj.FLAG.HIDDEN) + + def onPause(self, screen): + print("camera app backgrounded, cleaning up...") + self.stop_cam() + print("camera app cleanup done.") + + def start_cam(self): + # Init camera: + self.cam = self.init_internal_cam(self.width, self.height) + if self.cam: + self.image.set_rotation(900) # internal camera is rotated 90 degrees + # Apply saved camera settings, only for internal camera for now: + self.apply_camera_settings(self.scanqr_prefs if self.scanqr_mode else self.prefs, self.cam, self.use_webcam) # needs to be done AFTER the camera is initialized + else: + print("camera app: no internal camera found, trying webcam on /dev/video0") + try: + # Initialize webcam with desired resolution directly + print(f"Initializing webcam at {self.width}x{self.height}") + self.cam = webcam.init("/dev/video0", width=self.width, height=self.height) + self.use_webcam = True + except Exception as e: + print(f"camera app: webcam exception: {e}") + # Start refreshing: + if self.cam: + print("Camera app initialized, continuing...") + self.update_preview_image() + self.capture_timer = lv.timer_create(self.try_capture, 100, None) + + def stop_cam(self): + if self.capture_timer: + self.capture_timer.delete() + if self.use_webcam: + webcam.deinit(self.cam) + elif self.cam: + self.cam.deinit() + # Power off, otherwise it keeps using a lot of current + try: + from machine import Pin, I2C + i2c = I2C(1, scl=Pin(16), sda=Pin(21)) # Adjust pins and frequency + #devices = i2c.scan() + #print([hex(addr) for addr in devices]) # finds it on 60 = 0x3C after init + camera_addr = 0x3C # for OV5640 + reg_addr = 0x3008 + reg_high = (reg_addr >> 8) & 0xFF # 0x30 + reg_low = reg_addr & 0xFF # 0x08 + power_off_command = 0x42 # Power off command + i2c.writeto(camera_addr, bytes([reg_high, reg_low, power_off_command])) + except Exception as e: + print(f"Warning: powering off camera got exception: {e}") + self.cam = None + if self.image_dsc: # it's important to delete the image when stopping the camera, otherwise LVGL might try to display it and crash + print("emptying self.current_cam_buffer...") + self.image_dsc.data = None + + def load_settings_cached(self): + from mpos.config import SharedPreferences + if self.scanqr_mode: + print("loading scanqr settings...") + if not self.scanqr_prefs: + # Merge common and scanqr-specific defaults + scanqr_defaults = {} + scanqr_defaults.update(CameraSettingsActivity.COMMON_DEFAULTS) + scanqr_defaults.update(CameraSettingsActivity.SCANQR_DEFAULTS) + self.scanqr_prefs = SharedPreferences( + self.PACKAGE, + filename=self.SCANQR_CONFIG, + defaults=scanqr_defaults + ) + # Defaults come from constructor, no need to pass them here + self.width = self.scanqr_prefs.get_int("resolution_width") + self.height = self.scanqr_prefs.get_int("resolution_height") + self.colormode = self.scanqr_prefs.get_bool("colormode") + else: + if not self.prefs: + # Merge common and normal-specific defaults + normal_defaults = {} + normal_defaults.update(CameraSettingsActivity.COMMON_DEFAULTS) + normal_defaults.update(CameraSettingsActivity.NORMAL_DEFAULTS) + self.prefs = SharedPreferences(self.PACKAGE, defaults=normal_defaults) + # Defaults come from constructor, no need to pass them here + self.width = self.prefs.get_int("resolution_width") + self.height = self.prefs.get_int("resolution_height") + self.colormode = self.prefs.get_bool("colormode") + + def update_preview_image(self): + self.image_dsc = lv.image_dsc_t({ + "header": { + "magic": lv.IMAGE_HEADER_MAGIC, + "w": self.width, + "h": self.height, + "stride": self.width * (2 if self.colormode else 1), + "cf": lv.COLOR_FORMAT.RGB565 if self.colormode else lv.COLOR_FORMAT.L8 + }, + 'data_size': self.width * self.height * (2 if self.colormode else 1), + 'data': None # Will be updated per frame + }) + self.image.set_src(self.image_dsc) + disp = lv.display_get_default() + target_h = disp.get_vertical_resolution() + #target_w = disp.get_horizontal_resolution() - self.button_width - 5 # leave 5px for border + target_w = target_h # square + print(f"scaling to size: {target_w}x{target_h}") + scale_factor_w = round(target_w * 256 / self.width) + scale_factor_h = round(target_h * 256 / self.height) + print(f"scale_factors: {scale_factor_w},{scale_factor_h}") + self.image.set_size(target_w, target_h) + #self.image.set_scale(max(scale_factor_w,scale_factor_h)) # fills the entire screen but cuts off borders + self.image.set_scale(min(scale_factor_w,scale_factor_h)) + + def qrdecode_one(self): + try: + result = None + before = time.ticks_ms() + import qrdecode + if self.colormode: + result = qrdecode.qrdecode_rgb565(self.current_cam_buffer, self.width, self.height) + else: + result = qrdecode.qrdecode(self.current_cam_buffer, self.width, self.height) + after = time.ticks_ms() + print(f"qrdecode took {after-before}ms") + except ValueError as e: + print("QR ValueError: ", e) + self.status_label.set_text(self.STATUS_SEARCHING_QR) + except TypeError as e: + print("QR TypeError: ", e) + self.status_label.set_text(self.STATUS_FOUND_QR) + except Exception as e: + print("QR got other error: ", e) + #result = bytearray("INSERT_TEST_QR_DATA_HERE", "utf-8") + if result is None: + return + result = self.remove_bom(result) + result = self.print_qr_buffer(result) + print(f"QR decoding found: {result}") + self.stop_qr_decoding() + if self.scanqr_intent: + self.setResult(True, result) + self.finish() + else: + self.status_label.set_text(result) # in the future, the status_label text should be copy-paste-able + + def snap_button_click(self, e): + print("Taking picture...") + # Would be nice to check that there's enough free space here, and show an error if not... + import os + path = "data/images" + try: + os.mkdir("data") + except OSError: + pass + try: + os.mkdir(path) + except OSError: + pass + if self.current_cam_buffer is None: + print("snap_button_click: won't save empty image") + return + # Check enough free space? + stat = os.statvfs("data/images") + free_space = stat[0] * stat[3] + size_needed = len(self.current_cam_buffer) + print(f"Free space {free_space} and size needed {size_needed}") + if free_space < size_needed: + self.status_label.set_text(f"Free storage space is {free_space}, need {size_needed}, not saving...") + self.status_label_cont.remove_flag(lv.obj.FLAG.HIDDEN) + return + colorname = "RGB565" if self.colormode else "GRAY" + filename=f"{path}/picture_{mpos.time.epoch_seconds()}_{self.width}x{self.height}_{colorname}.raw" + try: + with open(filename, 'wb') as f: + f.write(self.current_cam_buffer) # This takes around 17 seconds to store 921600 bytes, so ~50KB/s, so would be nice to show some progress bar + report = f"Successfully wrote image to {filename}" + print(report) + self.status_label.set_text(report) + self.status_label_cont.remove_flag(lv.obj.FLAG.HIDDEN) + except OSError as e: + print(f"Error writing to file: {e}") + + def start_qr_decoding(self): + print("Activating live QR decoding...") + self.scanqr_mode = True + oldwidth = self.width + oldheight = self.height + oldcolormode = self.colormode + # Activate QR mode settings + self.load_settings_cached() + # Check if it's necessary to restart the camera: + if not self.cam or self.width != oldwidth or self.height != oldheight or self.colormode != oldcolormode: + if self.cam: + self.stop_cam() + self.start_cam() + self.qr_label.set_text(lv.SYMBOL.EYE_CLOSE) + self.status_label_cont.remove_flag(lv.obj.FLAG.HIDDEN) + self.status_label.set_text(self.STATUS_SEARCHING_QR) + + def stop_qr_decoding(self): + print("Deactivating live QR decoding...") + self.scanqr_mode = False + self.qr_label.set_text(lv.SYMBOL.EYE_OPEN) + status_label_text = self.status_label.get_text() + if status_label_text in (self.STATUS_NO_CAMERA, self.STATUS_SEARCHING_QR, self.STATUS_FOUND_QR): # if it found a QR code, leave it + self.status_label_cont.add_flag(lv.obj.FLAG.HIDDEN) + # Check if it's necessary to restart the camera: + oldwidth = self.width + oldheight = self.height + oldcolormode = self.colormode + # Activate non-QR mode settings + self.load_settings_cached() + # Check if it's necessary to restart the camera: + if self.width != oldwidth or self.height != oldheight or self.colormode != oldcolormode: + self.stop_cam() + self.start_cam() + + def qr_button_click(self, e): + if not self.scanqr_mode: + self.start_qr_decoding() + else: + self.stop_qr_decoding() + + def open_settings(self): + intent = Intent(activity_class=CameraSettingsActivity, extras={"prefs": self.prefs if not self.scanqr_mode else self.scanqr_prefs, "use_webcam": self.use_webcam, "scanqr_mode": self.scanqr_mode}) + self.startActivity(intent) + + def try_capture(self, event): + try: + if self.use_webcam and self.cam: + self.current_cam_buffer = webcam.capture_frame(self.cam, "rgb565" if self.colormode else "grayscale") + elif self.cam and self.cam.frame_available(): + self.current_cam_buffer = self.cam.capture() + except Exception as e: + print(f"Camera capture exception: {e}") + return + # Display the image: + self.image_dsc.data = self.current_cam_buffer + #self.image.invalidate() # does not work so do this: + self.image.set_src(self.image_dsc) + if self.scanqr_mode: + self.qrdecode_one() + if not self.use_webcam and self.cam: + self.cam.free_buffer() # After QR decoding, free the old buffer, otherwise the camera doesn't provide a new one + + def init_internal_cam(self, width, height): + """Initialize internal camera with specified resolution. + + Automatically retries once if initialization fails (to handle I2C poweroff issue). + """ + try: + from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling + + # Map resolution to FrameSize enum + # Format: (width, height): FrameSize + resolution_map = { + (96, 96): FrameSize.R96X96, + (160, 120): FrameSize.QQVGA, + (128, 128): FrameSize.R128X128, + (176, 144): FrameSize.QCIF, + (240, 176): FrameSize.HQVGA, + (240, 240): FrameSize.R240X240, + (320, 240): FrameSize.QVGA, + (320, 320): FrameSize.R320X320, + (400, 296): FrameSize.CIF, + (480, 320): FrameSize.HVGA, + (480, 480): FrameSize.R480X480, + (640, 480): FrameSize.VGA, + (640, 640): FrameSize.R640X640, + (720, 720): FrameSize.R720X720, + (800, 600): FrameSize.SVGA, + (800, 800): FrameSize.R800X800, + (960, 960): FrameSize.R960X960, + (1024, 768): FrameSize.XGA, + (1024,1024): FrameSize.R1024X1024, + (1280, 720): FrameSize.HD, + (1280, 1024): FrameSize.SXGA, + (1280, 1280): FrameSize.R1280X1280, + (1600, 1200): FrameSize.UXGA, + (1920, 1080): FrameSize.FHD, + } + + frame_size = resolution_map.get((width, height), FrameSize.QVGA) + print(f"init_internal_cam: Using FrameSize for {width}x{height}") + + # Try to initialize, with one retry for I2C poweroff issue + max_attempts = 3 + for attempt in range(max_attempts): + try: + cam = Camera( + data_pins=[12,13,15,11,14,10,7,2], + vsync_pin=6, + href_pin=4, + sda_pin=21, + scl_pin=16, + pclk_pin=9, + xclk_pin=8, + xclk_freq=20000000, + powerdown_pin=-1, + reset_pin=-1, + pixel_format=PixelFormat.RGB565 if self.colormode else PixelFormat.GRAYSCALE, + frame_size=frame_size, + #grab_mode=GrabMode.WHEN_EMPTY, + grab_mode=GrabMode.LATEST, + fb_count=1 + ) + cam.set_vflip(True) + return cam + except Exception as e: + if attempt < max_attempts-1: + print(f"init_cam attempt {attempt} failed: {e}, retrying...") + else: + print(f"init_cam final exception: {e}") + return None + except Exception as e: + print(f"init_cam exception: {e}") + return None + + def print_qr_buffer(self, buffer): + try: + # Try to decode buffer as a UTF-8 string + result = buffer.decode('utf-8') + # Check if the string is printable (ASCII printable characters) + if all(32 <= ord(c) <= 126 for c in result): + return result + except Exception as e: + pass + # If not a valid string or not printable, convert to hex + hex_str = ' '.join([f'{b:02x}' for b in buffer]) + return hex_str.lower() + + # Byte-Order-Mark is added sometimes + def remove_bom(self, buffer): + bom = b'\xEF\xBB\xBF' + if buffer.startswith(bom): + return buffer[3:] + return buffer + + + def apply_camera_settings(self, prefs, cam, use_webcam): + """Apply all saved camera settings to the camera. + + Only applies settings when use_webcam is False (ESP32 camera). + Settings are applied in dependency order (master switches before dependent values). + + Args: + cam: Camera object + use_webcam: Boolean indicating if using webcam + """ + if not cam or use_webcam: + print("apply_camera_settings: Skipping (no camera or webcam mode)") + return + + try: + # Basic image adjustments + brightness = prefs.get_int("brightness") + cam.set_brightness(brightness) + + contrast = prefs.get_int("contrast") + cam.set_contrast(contrast) + + saturation = prefs.get_int("saturation") + cam.set_saturation(saturation) + + # Orientation + hmirror = prefs.get_bool("hmirror") + cam.set_hmirror(hmirror) + + vflip = prefs.get_bool("vflip") + cam.set_vflip(vflip) + + # Special effect + special_effect = prefs.get_int("special_effect") + cam.set_special_effect(special_effect) + + # Exposure control (apply master switch first, then manual value) + exposure_ctrl = prefs.get_bool("exposure_ctrl") + cam.set_exposure_ctrl(exposure_ctrl) + + if not exposure_ctrl: + aec_value = prefs.get_int("aec_value") + cam.set_aec_value(aec_value) + + # Mode-specific default comes from constructor + ae_level = prefs.get_int("ae_level") + cam.set_ae_level(ae_level) + + aec2 = prefs.get_bool("aec2") + cam.set_aec2(aec2) + + # Gain control (apply master switch first, then manual value) + gain_ctrl = prefs.get_bool("gain_ctrl") + cam.set_gain_ctrl(gain_ctrl) + + if not gain_ctrl: + agc_gain = prefs.get_int("agc_gain") + cam.set_agc_gain(agc_gain) + + gainceiling = prefs.get_int("gainceiling") + cam.set_gainceiling(gainceiling) + + # White balance (apply master switch first, then mode) + whitebal = prefs.get_bool("whitebal") + cam.set_whitebal(whitebal) + + if not whitebal: + wb_mode = prefs.get_int("wb_mode") + cam.set_wb_mode(wb_mode) + + awb_gain = prefs.get_bool("awb_gain") + cam.set_awb_gain(awb_gain) + + # Sensor-specific settings (try/except for unsupported sensors) + try: + sharpness = prefs.get_int("sharpness") + cam.set_sharpness(sharpness) + except: + pass # Not supported on OV2640? + + try: + denoise = prefs.get_int("denoise") + cam.set_denoise(denoise) + except: + pass # Not supported on OV2640? + + # Advanced corrections + colorbar = prefs.get_bool("colorbar") + cam.set_colorbar(colorbar) + + dcw = prefs.get_bool("dcw") + cam.set_dcw(dcw) + + bpc = prefs.get_bool("bpc") + cam.set_bpc(bpc) + + wpc = prefs.get_bool("wpc") + cam.set_wpc(wpc) + + # Mode-specific default comes from constructor + raw_gma = prefs.get_bool("raw_gma") + print(f"applying raw_gma: {raw_gma}") + cam.set_raw_gma(raw_gma) + + lenc = prefs.get_bool("lenc") + cam.set_lenc(lenc) + + # JPEG quality (only relevant for JPEG format) + #try: + # quality = prefs.get_int("quality", 85) + # cam.set_quality(quality) + #except: + # pass # Not in JPEG mode + + print("Camera settings applied successfully") + + except Exception as e: + print(f"Error applying camera settings: {e}") + + + + +""" + def zoom_button_click_unused(self, e): + print("zooming...") + if self.use_webcam: + print("zoom_button_click is not supported for webcam") + return + if self.cam: + startX = self.prefs.get_int("startX", CameraSettingsActivity.startX_default) + startY = self.prefs.get_int("startX", CameraSettingsActivity.startY_default) + endX = self.prefs.get_int("startX", CameraSettingsActivity.endX_default) + endY = self.prefs.get_int("startX", CameraSettingsActivity.endY_default) + offsetX = self.prefs.get_int("startX", CameraSettingsActivity.offsetX_default) + offsetY = self.prefs.get_int("startX", CameraSettingsActivity.offsetY_default) + totalX = self.prefs.get_int("startX", CameraSettingsActivity.totalX_default) + totalY = self.prefs.get_int("startX", CameraSettingsActivity.totalY_default) + outputX = self.prefs.get_int("startX", CameraSettingsActivity.outputX_default) + outputY = self.prefs.get_int("startX", CameraSettingsActivity.outputY_default) + scale = self.prefs.get_bool("scale", CameraSettingsActivity.scale_default) + binning = self.prefs.get_bool("binning", CameraSettingsActivity.binning_default) + result = self.cam.set_res_raw(startX,startY,endX,endY,offsetX,offsetY,totalX,totalY,outputX,outputY,scale,binning) + print(f"self.cam.set_res_raw returned {result}") +""" diff --git a/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/camera_settings.py b/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/camera_settings.py new file mode 100644 index 00000000..8bf90ecc --- /dev/null +++ b/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/camera_settings.py @@ -0,0 +1,604 @@ +import lvgl as lv + +import mpos.ui +from mpos.apps import Activity +from mpos.config import SharedPreferences +from mpos.content.intent import Intent + +class CameraSettingsActivity(Activity): + + # Original: { 2560, 1920, 0, 0, 2623, 1951, 32, 16, 2844, 1968 } + # Worked for digital zoom in C: { 2560, 1920, 0, 0, 2623, 1951, 992, 736, 2844, 1968 } + startX_default=0 + startY_default=0 + endX_default=2623 + endY_default=1951 + offsetX_default=32 + offsetY_default=16 + totalX_default=2844 + totalY_default=1968 + outputX_default=640 + outputY_default=480 + scale_default=False + binning_default=False + + # Common defaults shared by both normal and scanqr modes (25 settings) + COMMON_DEFAULTS = { + # Basic image adjustments + "brightness": 0, + "contrast": 0, + "saturation": 0, + # Orientation + "hmirror": False, + "vflip": True, + # Visual effects + "special_effect": 0, + # Exposure control + "exposure_ctrl": True, + "aec_value": 300, + "aec2": False, + # Gain control + "gain_ctrl": True, + "agc_gain": 0, + "gainceiling": 0, + # White balance + "whitebal": True, + "wb_mode": 0, + "awb_gain": True, + # Sensor-specific + "sharpness": 0, + "denoise": 0, + # Advanced corrections + "colorbar": False, + "dcw": True, + "bpc": False, + "wpc": True, + "lenc": True, + } + + # Normal mode specific defaults + NORMAL_DEFAULTS = { + "resolution_width": 240, + "resolution_height": 240, + "colormode": True, + "ae_level": 0, + "raw_gma": True, + } + + # Scanqr mode specific defaults + SCANQR_DEFAULTS = { + "resolution_width": 960, + "resolution_height": 960, + "colormode": False, + "ae_level": 2, # Higher auto-exposure compensation + "raw_gma": False, # Disable raw gamma for better contrast + } + + # Resolution options for both ESP32 and webcam + # Webcam supports all ESP32 resolutions via automatic cropping/padding + RESOLUTIONS = [ + ("96x96", "96x96"), + ("160x120", "160x120"), + ("128x128", "128x128"), + ("176x144", "176x144"), + ("240x176", "240x176"), + ("240x240", "240x240"), + ("320x240", "320x240"), + ("320x320", "320x320"), + ("400x296", "400x296"), + ("480x320", "480x320"), + ("480x480", "480x480"), + ("640x480", "640x480"), + ("640x640", "640x640"), + ("720x720", "720x720"), + ("800x600", "800x600"), + ("800x800", "800x800"), + ("960x960", "960x960"), + ("1024x768", "1024x768"), + ("1024x1024","1024x1024"), + ("1280x720", "1280x720"), + ("1280x1024", "1280x1024"), + ("1280x1280", "1280x1280"), + ("1600x1200", "1600x1200"), + ("1920x1080", "1920x1080"), + ] + + # These are taken from the Intent: + use_webcam = False + prefs = None + scanqr_mode = False + + # Widgets: + button_cont = None + + def __init__(self): + super().__init__() + self.ui_controls = {} + self.control_metadata = {} # Store pref_key and option_values for each control + self.dependent_controls = {} + + def onCreate(self): + self.use_webcam = self.getIntent().extras.get("use_webcam") + self.prefs = self.getIntent().extras.get("prefs") + self.scanqr_mode = self.getIntent().extras.get("scanqr_mode") + + # Create main screen + screen = lv.obj() + screen.set_size(lv.pct(100), lv.pct(100)) + screen.set_style_pad_all(1, 0) + + # Create tabview + tabview = lv.tabview(screen) + tabview.set_tab_bar_size(mpos.ui.pct_of_display_height(15)) + #tabview.set_size(lv.pct(100), mpos.ui.pct_of_display_height(80)) + + # Create Basic tab (always) + basic_tab = tabview.add_tab("Basic") + self.create_basic_tab(basic_tab, self.prefs) + + # Create Advanced and Expert tabs only for ESP32 camera + if not self.use_webcam or True: # for now, show all tabs + advanced_tab = tabview.add_tab("Advanced") + self.create_advanced_tab(advanced_tab, self.prefs) + + expert_tab = tabview.add_tab("Expert") + self.create_expert_tab(expert_tab, self.prefs) + + #raw_tab = tabview.add_tab("Raw") + #self.create_raw_tab(raw_tab, self.prefs) + + self.setContentView(screen) + + def create_slider(self, parent, label_text, min_val, max_val, default_val, pref_key): + """Create slider with label showing current value.""" + cont = lv.obj(parent) + cont.set_size(lv.pct(100), 60) + cont.set_style_pad_all(3, 0) + + label = lv.label(cont) + label.set_text(f"{label_text}: {default_val}") + label.align(lv.ALIGN.TOP_LEFT, 0, 0) + + slider = lv.slider(cont) + slider.set_size(lv.pct(90), 15) + slider.set_range(min_val, max_val) + slider.set_value(default_val, False) + slider.align(lv.ALIGN.BOTTOM_MID, 0, -10) + + def slider_changed(e): + val = slider.get_value() + label.set_text(f"{label_text}: {val}") + + slider.add_event_cb(slider_changed, lv.EVENT.VALUE_CHANGED, None) + + return slider, label, cont + + def create_checkbox(self, parent, label_text, default_val, pref_key): + """Create checkbox with label.""" + cont = lv.obj(parent) + cont.set_size(lv.pct(100), 35) + cont.set_style_pad_all(3, 0) + + checkbox = lv.checkbox(cont) + checkbox.set_text(label_text) + if default_val: + checkbox.add_state(lv.STATE.CHECKED) + checkbox.align(lv.ALIGN.LEFT_MID, 0, 0) + + return checkbox, cont + + def create_dropdown(self, parent, label_text, options, default_idx, pref_key): + """Create dropdown with label.""" + cont = lv.obj(parent) + cont.set_size(lv.pct(100), lv.SIZE_CONTENT) + cont.set_style_pad_all(2, 0) + + label = lv.label(cont) + label.set_text(label_text) + label.set_size(lv.pct(50), lv.SIZE_CONTENT) + label.align(lv.ALIGN.LEFT_MID, 0, 0) + + dropdown = lv.dropdown(cont) + dropdown.set_size(lv.pct(50), lv.SIZE_CONTENT) + dropdown.align(lv.ALIGN.RIGHT_MID, 0, 0) + + options_str = "\n".join([text for text, _ in options]) + dropdown.set_options(options_str) + dropdown.set_selected(default_idx) + + # Store metadata separately + option_values = [val for _, val in options] + self.control_metadata[id(dropdown)] = { + "pref_key": pref_key, + "type": "dropdown", + "option_values": option_values + } + + return dropdown, cont + + def create_textarea(self, parent, label_text, min_val, max_val, default_val, pref_key): + cont = lv.obj(parent) + cont.set_size(lv.pct(100), lv.SIZE_CONTENT) + cont.set_style_pad_all(3, 0) + + label = lv.label(cont) + label.set_text(f"{label_text}:") + label.align(lv.ALIGN.TOP_LEFT, 0, 0) + + textarea = lv.textarea(cont) + textarea.set_width(lv.pct(50)) + textarea.set_one_line(True) # might not be good for all settings but it's good for most + textarea.set_text(str(default_val)) + textarea.align(lv.ALIGN.TOP_RIGHT, 0, 0) + + # Initialize keyboard (hidden initially) + from mpos.ui.keyboard import MposKeyboard + keyboard = MposKeyboard(parent) + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) + keyboard.add_flag(lv.obj.FLAG.HIDDEN) + keyboard.set_textarea(textarea) + + return textarea, cont + + def add_buttons(self, parent): + # Save/Cancel buttons at bottom + button_cont = lv.obj(parent) + button_cont.set_size(lv.pct(100), mpos.ui.pct_of_display_height(20)) + button_cont.remove_flag(lv.obj.FLAG.SCROLLABLE) + button_cont.align(lv.ALIGN.BOTTOM_MID, 0, 0) + button_cont.set_style_border_width(0, 0) + + save_button = lv.button(button_cont) + save_button.set_size(lv.SIZE_CONTENT, lv.SIZE_CONTENT) + save_button.align(lv.ALIGN.BOTTOM_LEFT, 0, 0) + save_button.add_event_cb(lambda e: self.save_and_close(), lv.EVENT.CLICKED, None) + save_label = lv.label(save_button) + savetext = "Save" + if self.scanqr_mode: + savetext += " QR tweaks" + save_label.set_text(savetext) + save_label.center() + + cancel_button = lv.button(button_cont) + cancel_button.set_size(mpos.ui.pct_of_display_width(25), lv.SIZE_CONTENT) + if self.scanqr_mode: + cancel_button.align(lv.ALIGN.BOTTOM_MID, mpos.ui.pct_of_display_width(10), 0) + else: + cancel_button.align(lv.ALIGN.BOTTOM_MID, 0, 0) + cancel_button.add_event_cb(lambda e: self.finish(), lv.EVENT.CLICKED, None) + cancel_label = lv.label(cancel_button) + cancel_label.set_text("Cancel") + cancel_label.center() + + erase_button = lv.button(button_cont) + erase_button.set_size(mpos.ui.pct_of_display_width(20), lv.SIZE_CONTENT) + erase_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) + erase_button.add_event_cb(lambda e: self.erase_and_close(), lv.EVENT.CLICKED, None) + erase_label = lv.label(erase_button) + erase_label.set_text("Erase") + erase_label.center() + + + def create_basic_tab(self, tab, prefs): + """Create Basic settings tab.""" + tab.set_flex_flow(lv.FLEX_FLOW.COLUMN) + #tab.set_scrollbar_mode(lv.SCROLLBAR_MODE.AUTO) + tab.set_style_pad_all(1, 0) + + # Color Mode + colormode = prefs.get_bool("colormode") + checkbox, cont = self.create_checkbox(tab, "Color Mode (slower)", colormode, "colormode") + self.ui_controls["colormode"] = checkbox + + # Resolution dropdown + print(f"self.scanqr_mode: {self.scanqr_mode}") + current_resolution_width = prefs.get_int("resolution_width") + current_resolution_height = prefs.get_int("resolution_height") + dropdown_value = f"{current_resolution_width}x{current_resolution_height}" + print(f"looking for {dropdown_value}") + resolution_idx = 0 + for idx, (_, value) in enumerate(self.RESOLUTIONS): + print(f"got {value}") + if value == dropdown_value: + resolution_idx = idx + print(f"found it! {idx}") + break + + dropdown, cont = self.create_dropdown(tab, "Resolution:", self.RESOLUTIONS, resolution_idx, "resolution") + self.ui_controls["resolution"] = dropdown + + # Brightness + brightness = prefs.get_int("brightness") + slider, label, cont = self.create_slider(tab, "Brightness", -2, 2, brightness, "brightness") + self.ui_controls["brightness"] = slider + + # Contrast + contrast = prefs.get_int("contrast") + slider, label, cont = self.create_slider(tab, "Contrast", -2, 2, contrast, "contrast") + self.ui_controls["contrast"] = slider + + # Saturation + saturation = prefs.get_int("saturation") + slider, label, cont = self.create_slider(tab, "Saturation", -2, 2, saturation, "saturation") + self.ui_controls["saturation"] = slider + + # Horizontal Mirror + hmirror = prefs.get_bool("hmirror") + checkbox, cont = self.create_checkbox(tab, "Horizontal Mirror", hmirror, "hmirror") + self.ui_controls["hmirror"] = checkbox + + # Vertical Flip + vflip = prefs.get_bool("vflip") + checkbox, cont = self.create_checkbox(tab, "Vertical Flip", vflip, "vflip") + self.ui_controls["vflip"] = checkbox + + self.add_buttons(tab) + + def create_advanced_tab(self, tab, prefs): + """Create Advanced settings tab.""" + tab.set_flex_flow(lv.FLEX_FLOW.COLUMN) + tab.set_style_pad_all(1, 0) + + # Auto Exposure Control (master switch) + exposure_ctrl = prefs.get_bool("exposure_ctrl") + aec_checkbox, cont = self.create_checkbox(tab, "Auto Exposure", exposure_ctrl, "exposure_ctrl") + self.ui_controls["exposure_ctrl"] = aec_checkbox + + # Manual Exposure Value (dependent) + aec_value = prefs.get_int("aec_value") + me_slider, label, me_cont = self.create_slider(tab, "Manual Exposure", 0, 1200, aec_value, "aec_value") + self.ui_controls["aec_value"] = me_slider + + # Auto Exposure Level (dependent) + ae_level = prefs.get_int("ae_level") + ae_slider, label, ae_cont = self.create_slider(tab, "Auto Exposure Level", -2, 2, ae_level, "ae_level") + self.ui_controls["ae_level"] = ae_slider + + # Add dependency handler + def exposure_ctrl_changed(e=None): + is_auto = aec_checkbox.get_state() & lv.STATE.CHECKED + if is_auto: + mpos.ui.anim.smooth_hide(me_cont, duration=1000) + mpos.ui.anim.smooth_show(ae_cont, delay=1000) + else: + mpos.ui.anim.smooth_hide(ae_cont, duration=1000) + mpos.ui.anim.smooth_show(me_cont, delay=1000) + + aec_checkbox.add_event_cb(exposure_ctrl_changed, lv.EVENT.VALUE_CHANGED, None) + exposure_ctrl_changed() + + # Night Mode (AEC2) + aec2 = prefs.get_bool("aec2") + checkbox, cont = self.create_checkbox(tab, "Night Mode (AEC2)", aec2, "aec2") + self.ui_controls["aec2"] = checkbox + + # Auto Gain Control (master switch) + gain_ctrl = prefs.get_bool("gain_ctrl") + agc_checkbox, cont = self.create_checkbox(tab, "Auto Gain", gain_ctrl, "gain_ctrl") + self.ui_controls["gain_ctrl"] = agc_checkbox + + # Manual Gain Value (dependent) + agc_gain = prefs.get_int("agc_gain") + slider, label, agc_cont = self.create_slider(tab, "Manual Gain", 0, 30, agc_gain, "agc_gain") + self.ui_controls["agc_gain"] = slider + + def gain_ctrl_changed(e=None): + is_auto = agc_checkbox.get_state() & lv.STATE.CHECKED + gain_slider = self.ui_controls["agc_gain"] + if is_auto: + mpos.ui.anim.smooth_hide(agc_cont, duration=1000) + else: + mpos.ui.anim.smooth_show(agc_cont, duration=1000) + + agc_checkbox.add_event_cb(gain_ctrl_changed, lv.EVENT.VALUE_CHANGED, None) + gain_ctrl_changed() + + # Gain Ceiling + gainceiling_options = [ + ("2X", 0), ("4X", 1), ("8X", 2), ("16X", 3), + ("32X", 4), ("64X", 5), ("128X", 6) + ] + gainceiling = prefs.get_int("gainceiling") + dropdown, cont = self.create_dropdown(tab, "Gain Ceiling:", gainceiling_options, gainceiling, "gainceiling") + self.ui_controls["gainceiling"] = dropdown + + # Auto White Balance (master switch) + whitebal = prefs.get_bool("whitebal") + wbcheckbox, cont = self.create_checkbox(tab, "Auto White Balance", whitebal, "whitebal") + self.ui_controls["whitebal"] = wbcheckbox + + # White Balance Mode (dependent) + wb_mode_options = [ + ("Auto", 0), ("Sunny", 1), ("Cloudy", 2), ("Office", 3), ("Home", 4) + ] + wb_mode = prefs.get_int("wb_mode") + wb_dropdown, wb_cont = self.create_dropdown(tab, "WB Mode:", wb_mode_options, wb_mode, "wb_mode") + self.ui_controls["wb_mode"] = wb_dropdown + + def whitebal_changed(e=None): + is_auto = wbcheckbox.get_state() & lv.STATE.CHECKED + if is_auto: + mpos.ui.anim.smooth_hide(wb_cont, duration=1000) + else: + mpos.ui.anim.smooth_show(wb_cont, duration=1000) + wbcheckbox.add_event_cb(whitebal_changed, lv.EVENT.VALUE_CHANGED, None) + whitebal_changed() + + # AWB Gain + awb_gain = prefs.get_bool("awb_gain") + checkbox, cont = self.create_checkbox(tab, "AWB Gain", awb_gain, "awb_gain") + self.ui_controls["awb_gain"] = checkbox + + self.add_buttons(tab) + + # Special Effect + special_effect_options = [ + ("None", 0), ("Negative", 1), ("Grayscale", 2), + ("Reddish", 3), ("Greenish", 4), ("Blue", 5), ("Retro", 6) + ] + special_effect = prefs.get_int("special_effect") + dropdown, cont = self.create_dropdown(tab, "Special Effect:", special_effect_options, + special_effect, "special_effect") + self.ui_controls["special_effect"] = dropdown + + def create_expert_tab(self, tab, prefs): + """Create Expert settings tab.""" + #tab.set_scrollbar_mode(lv.SCROLLBAR_MODE.AUTO) + tab.set_flex_flow(lv.FLEX_FLOW.COLUMN) + tab.set_style_pad_all(1, 0) + + # Sharpness + sharpness = prefs.get_int("sharpness") + slider, label, cont = self.create_slider(tab, "Sharpness", -3, 3, sharpness, "sharpness") + self.ui_controls["sharpness"] = slider + + # Denoise + denoise = prefs.get_int("denoise") + slider, label, cont = self.create_slider(tab, "Denoise", 0, 8, denoise, "denoise") + self.ui_controls["denoise"] = slider + + # JPEG Quality + # Disabled because JPEG is not used right now + #quality = prefs.get_int("quality", 85) + #slider, label, cont = self.create_slider(tab, "JPEG Quality", 0, 100, quality, "quality") + #self.ui_controls["quality"] = slider + + # Color Bar + colorbar = prefs.get_bool("colorbar") + checkbox, cont = self.create_checkbox(tab, "Color Bar Test", colorbar, "colorbar") + self.ui_controls["colorbar"] = checkbox + + # DCW Mode + dcw = prefs.get_bool("dcw") + checkbox, cont = self.create_checkbox(tab, "Downsize Crop Window", dcw, "dcw") + self.ui_controls["dcw"] = checkbox + + # Black Point Compensation + bpc = prefs.get_bool("bpc") + checkbox, cont = self.create_checkbox(tab, "Black Point Compensation", bpc, "bpc") + self.ui_controls["bpc"] = checkbox + + # White Point Compensation + wpc = prefs.get_bool("wpc") + checkbox, cont = self.create_checkbox(tab, "White Point Compensation", wpc, "wpc") + self.ui_controls["wpc"] = checkbox + + # Raw Gamma Mode + raw_gma = prefs.get_bool("raw_gma") + checkbox, cont = self.create_checkbox(tab, "Raw Gamma Mode", raw_gma, "raw_gma") + self.ui_controls["raw_gma"] = checkbox + + # Lens Correction + lenc = prefs.get_bool("lenc") + checkbox, cont = self.create_checkbox(tab, "Lens Correction", lenc, "lenc") + self.ui_controls["lenc"] = checkbox + + self.add_buttons(tab) + + def create_raw_tab(self, tab, prefs): + tab.set_flex_flow(lv.FLEX_FLOW.COLUMN) + tab.set_style_pad_all(0, 0) + + # This would be nice but does not provide adequate resolution: + #startX, label, cont = self.create_slider(tab, "startX", 0, 2844, startX, "startX") + + startX = prefs.get_int("startX", self.startX_default) + textarea, cont = self.create_textarea(tab, "startX", 0, 2844, startX, "startX") + self.ui_controls["startX"] = textarea + + startY = prefs.get_int("startY", self.startY_default) + textarea, cont = self.create_textarea(tab, "startY", 0, 2844, startY, "startY") + self.ui_controls["startY"] = textarea + + endX = prefs.get_int("endX", self.endX_default) + textarea, cont = self.create_textarea(tab, "endX", 0, 2844, endX, "endX") + self.ui_controls["endX"] = textarea + + endY = prefs.get_int("endY", self.endY_default) + textarea, cont = self.create_textarea(tab, "endY", 0, 2844, endY, "endY") + self.ui_controls["endY"] = textarea + + offsetX = prefs.get_int("offsetX", self.offsetX_default) + textarea, cont = self.create_textarea(tab, "offsetX", 0, 2844, offsetX, "offsetX") + self.ui_controls["offsetX"] = textarea + + offsetY = prefs.get_int("offsetY", self.offsetY_default) + textarea, cont = self.create_textarea(tab, "offsetY", 0, 2844, offsetY, "offsetY") + self.ui_controls["offsetY"] = textarea + + totalX = prefs.get_int("totalX", self.totalX_default) + textarea, cont = self.create_textarea(tab, "totalX", 0, 2844, totalX, "totalX") + self.ui_controls["totalX"] = textarea + + totalY = prefs.get_int("totalY", self.totalY_default) + textarea, cont = self.create_textarea(tab, "totalY", 0, 2844, totalY, "totalY") + self.ui_controls["totalY"] = textarea + + outputX = prefs.get_int("outputX", self.outputX_default) + textarea, cont = self.create_textarea(tab, "outputX", 0, 2844, outputX, "outputX") + self.ui_controls["outputX"] = textarea + + outputY = prefs.get_int("outputY", self.outputY_default) + textarea, cont = self.create_textarea(tab, "outputY", 0, 2844, outputY, "outputY") + self.ui_controls["outputY"] = textarea + + scale = prefs.get_bool("scale", self.scale_default) + checkbox, cont = self.create_checkbox(tab, "Scale?", scale, "scale") + self.ui_controls["scale"] = checkbox + + binning = prefs.get_bool("binning", self.binning_default) + checkbox, cont = self.create_checkbox(tab, "Binning?", binning, "binning") + self.ui_controls["binning"] = checkbox + + self.add_buttons(tab) + + def erase_and_close(self): + self.prefs.edit().remove_all().commit() + self.setResult(True, {"settings_changed": True}) + self.finish() + + def save_and_close(self): + """Save all settings to SharedPreferences and return result.""" + editor = self.prefs.edit() + + # Save all UI control values + for pref_key, control in self.ui_controls.items(): + print(f"saving {pref_key} with {control}") + control_id = id(control) + metadata = self.control_metadata.get(control_id, {}) + + if isinstance(control, lv.slider): + value = control.get_value() + editor.put_int(pref_key, value) + elif isinstance(control, lv.checkbox): + is_checked = control.get_state() & lv.STATE.CHECKED + editor.put_bool(pref_key, bool(is_checked)) + elif isinstance(control, lv.textarea): + try: + value = int(control.get_text()) + editor.put_int(pref_key, value) + except Exception as e: + print(f"Error while trying to save {pref_key}: {e}") + elif isinstance(control, lv.dropdown): + selected_idx = control.get_selected() + option_values = metadata.get("option_values", []) + if pref_key == "resolution": + try: + # Resolution stored as 2 ints + value = option_values[selected_idx] + width_str, height_str = value.split('x') + editor.put_int("resolution_width", int(width_str)) + editor.put_int("resolution_height", int(height_str)) + except Exception as e: + print(f"Error parsing resolution '{value}': {e}") + else: + # Other dropdowns store integer enum values + value = option_values[selected_idx] + editor.put_int(pref_key, value) + + editor.commit() + print("Camera settings saved") + + # Return success result + self.setResult(True, {"settings_changed": True}) + self.finish() diff --git a/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/wifi.py b/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/wifi.py index a59a156a..ae1f72e6 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/wifi.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/wifi.py @@ -8,6 +8,7 @@ from mpos.ui.keyboard import MposKeyboard import mpos.apps from mpos.net.wifi_service import WifiService +from camera_app import CameraApp class WiFi(Activity): """ @@ -222,6 +223,10 @@ class EditNetwork(Activity): keyboard = None connect_button = None cancel_button = None + forget_button = None + + action_button_label_forget = "Forget" + action_button_label_scanqr = "Scan QR" def onCreate(self): password_page = lv.obj() @@ -275,14 +280,16 @@ class EditNetwork(Activity): buttons.set_height(lv.SIZE_CONTENT) buttons.set_style_bg_opa(lv.OPA.TRANSP, 0) buttons.set_style_border_width(0, lv.PART.MAIN) - # Delete button + # Forget / Scan QR button + self.forget_button = lv.button(buttons) + self.forget_button.align(lv.ALIGN.LEFT_MID, 0, 0) + self.forget_button.add_event_cb(self.forget_cb, lv.EVENT.CLICKED, None) + label = lv.label(self.forget_button) + label.center() if self.selected_ssid: - self.forget_button = lv.button(buttons) - self.forget_button.align(lv.ALIGN.LEFT_MID, 0, 0) - self.forget_button.add_event_cb(self.forget_cb, lv.EVENT.CLICKED, None) - label = lv.label(self.forget_button) - label.set_text("Forget") - label.center() + label.set_text(self.action_button_label_forget) + else: + label.set_text(self.action_button_label_scanqr) # Close button self.cancel_button = lv.button(buttons) self.cancel_button.center() @@ -321,5 +328,34 @@ class EditNetwork(Activity): self.finish() def forget_cb(self, event): - self.setResult(True, {"ssid": self.selected_ssid, "forget": True}) - self.finish() + label = self.forget_button.get_child(0) + if not label: + return + action = label.get_text() + print(f"{action} button clicked") + if action == self.action_button_label_forget: + print("Closing Activity") + self.setResult(True, {"ssid": self.selected_ssid, "forget": True}) + self.finish() + else: + print("Opening CameraApp") + self.startActivityForResult(Intent(activity_class=CameraApp).putExtra("scanqr_intent", True), self.gotqr_result_callback) + + def gotqr_result_callback(self, result): + print(f"QR capture finished, result: {result}") + if result.get("result_code"): + data = result.get("data") + print(f"Setting textarea data: {data}") + authentication_type, ssid, password, hidden = decode_wifi_qr_code(data) + if ssid and self.ssid_ta: # not always present + self.ssid_ta.set_text(ssid) + if password: + self.password_ta.set_text(ssid) + if hidden is True: + self.hidden_cb.set_state(lv.STATE.CHECKED, True) + elif hidden is False: + self.hidden_cb.remove_state(lv.STATE.CHECKED) + + @staticmethod + def decode_wifi_qr_code(to_decode): + print(f"decoding {todecode}") \ No newline at end of file