diff --git a/c_mpos/src/webcam.c b/c_mpos/src/webcam.c index 4ae15994..8b0e9194 100644 --- a/c_mpos/src/webcam.c +++ b/c_mpos/src/webcam.c @@ -30,17 +30,24 @@ typedef struct _webcam_obj_t { int frame_count; unsigned char *gray_buffer; // For grayscale uint16_t *rgb565_buffer; // For RGB565 + int input_width; // Webcam capture width (from V4L2) + int input_height; // Webcam capture height (from V4L2) + int output_width; // Configurable output width (default OUTPUT_WIDTH) + int output_height; // Configurable output height (default OUTPUT_HEIGHT) } webcam_obj_t; -static void yuyv_to_rgb565_240x240(unsigned char *yuyv, uint16_t *rgb565, int in_width, int in_height) { - int crop_size = 480; +static void yuyv_to_rgb565(unsigned char *yuyv, uint16_t *rgb565, int in_width, int in_height, int out_width, int out_height) { + // Crop to largest square that fits in the input frame + int crop_size = (in_width < in_height) ? in_width : in_height; int crop_x_offset = (in_width - crop_size) / 2; int crop_y_offset = (in_height - crop_size) / 2; - float x_ratio = (float)crop_size / OUTPUT_WIDTH; - float y_ratio = (float)crop_size / OUTPUT_HEIGHT; - for (int y = 0; y < OUTPUT_HEIGHT; y++) { - for (int x = 0; x < OUTPUT_WIDTH; x++) { + // Calculate scaling ratios + float x_ratio = (float)crop_size / out_width; + float y_ratio = (float)crop_size / out_height; + + for (int y = 0; y < out_height; y++) { + for (int x = 0; x < out_width; x++) { int src_x = (int)(x * x_ratio) + crop_x_offset; int src_y = (int)(y * y_ratio) + crop_y_offset; int src_index = (src_y * in_width + src_x) * 2; @@ -65,24 +72,27 @@ static void yuyv_to_rgb565_240x240(unsigned char *yuyv, uint16_t *rgb565, int in uint16_t g6 = (g >> 2) & 0x3F; uint16_t b5 = (b >> 3) & 0x1F; - rgb565[y * OUTPUT_WIDTH + x] = (r5 << 11) | (g6 << 5) | b5; + rgb565[y * out_width + x] = (r5 << 11) | (g6 << 5) | b5; } } } -static void yuyv_to_grayscale_240x240(unsigned char *yuyv, unsigned char *gray, int in_width, int in_height) { - int crop_size = 480; +static void yuyv_to_grayscale(unsigned char *yuyv, unsigned char *gray, int in_width, int in_height, int out_width, int out_height) { + // Crop to largest square that fits in the input frame + int crop_size = (in_width < in_height) ? in_width : in_height; int crop_x_offset = (in_width - crop_size) / 2; int crop_y_offset = (in_height - crop_size) / 2; - float x_ratio = (float)crop_size / OUTPUT_WIDTH; - float y_ratio = (float)crop_size / OUTPUT_HEIGHT; - for (int y = 0; y < OUTPUT_HEIGHT; y++) { - for (int x = 0; x < OUTPUT_WIDTH; x++) { + // Calculate scaling ratios + float x_ratio = (float)crop_size / out_width; + float y_ratio = (float)crop_size / out_height; + + for (int y = 0; y < out_height; y++) { + for (int x = 0; x < out_width; x++) { int src_x = (int)(x * x_ratio) + crop_x_offset; int src_y = (int)(y * y_ratio) + crop_y_offset; int src_index = (src_y * in_width + src_x) * 2; - gray[y * OUTPUT_WIDTH + x] = yuyv[src_index]; + gray[y * out_width + x] = yuyv[src_index]; } } } @@ -174,8 +184,22 @@ static int init_webcam(webcam_obj_t *self, const char *device) { } self->frame_count = 0; - self->gray_buffer = (unsigned char *)malloc(OUTPUT_WIDTH * OUTPUT_HEIGHT * sizeof(unsigned char)); - self->rgb565_buffer = (uint16_t *)malloc(OUTPUT_WIDTH * OUTPUT_HEIGHT * sizeof(uint16_t)); + + // Store the input dimensions from V4L2 format + self->input_width = WIDTH; + self->input_height = HEIGHT; + + // Initialize output dimensions with defaults if not already set + if (self->output_width == 0) self->output_width = OUTPUT_WIDTH; + if (self->output_height == 0) self->output_height = OUTPUT_HEIGHT; + + WEBCAM_DEBUG_PRINT("Webcam initialized: input %dx%d, output %dx%d\n", + self->input_width, self->input_height, + self->output_width, self->output_height); + + // Allocate buffers with configured output dimensions + self->gray_buffer = (unsigned char *)malloc(self->output_width * self->output_height * sizeof(unsigned char)); + self->rgb565_buffer = (uint16_t *)malloc(self->output_width * self->output_height * sizeof(uint16_t)); if (!self->gray_buffer || !self->rgb565_buffer) { WEBCAM_DEBUG_PRINT("Cannot allocate buffers: %s\n", strerror(errno)); free(self->gray_buffer); @@ -227,13 +251,13 @@ static mp_obj_t capture_frame(mp_obj_t self_in, mp_obj_t format) { } if (!self->gray_buffer) { - self->gray_buffer = (unsigned char *)malloc(OUTPUT_WIDTH * OUTPUT_HEIGHT * sizeof(unsigned char)); + self->gray_buffer = (unsigned char *)malloc(self->output_width * self->output_height * sizeof(unsigned char)); if (!self->gray_buffer) { mp_raise_OSError(MP_ENOMEM); } } if (!self->rgb565_buffer) { - self->rgb565_buffer = (uint16_t *)malloc(OUTPUT_WIDTH * OUTPUT_HEIGHT * sizeof(uint16_t)); + self->rgb565_buffer = (uint16_t *)malloc(self->output_width * self->output_height * sizeof(uint16_t)); if (!self->rgb565_buffer) { mp_raise_OSError(MP_ENOMEM); } @@ -241,22 +265,26 @@ static mp_obj_t capture_frame(mp_obj_t self_in, mp_obj_t format) { const char *fmt = mp_obj_str_get_str(format); if (strcmp(fmt, "grayscale") == 0) { - yuyv_to_grayscale_240x240(self->buffers[buf.index], self->gray_buffer, WIDTH, HEIGHT); + yuyv_to_grayscale(self->buffers[buf.index], self->gray_buffer, + self->input_width, self->input_height, + self->output_width, self->output_height); // char filename[32]; // snprintf(filename, sizeof(filename), "frame_%03d.raw", self->frame_count++); - // save_raw(filename, self->gray_buffer, OUTPUT_WIDTH, OUTPUT_HEIGHT); - mp_obj_t result = mp_obj_new_memoryview('b', OUTPUT_WIDTH * OUTPUT_HEIGHT, self->gray_buffer); + // save_raw(filename, self->gray_buffer, self->output_width, self->output_height); + mp_obj_t result = mp_obj_new_memoryview('b', self->output_width * self->output_height, self->gray_buffer); res = ioctl(self->fd, VIDIOC_QBUF, &buf); if (res < 0) { mp_raise_OSError(-res); } return result; } else { - yuyv_to_rgb565_240x240(self->buffers[buf.index], self->rgb565_buffer, WIDTH, HEIGHT); + yuyv_to_rgb565(self->buffers[buf.index], self->rgb565_buffer, + self->input_width, self->input_height, + self->output_width, self->output_height); // char filename[32]; // snprintf(filename, sizeof(filename), "frame_%03d.rgb565", self->frame_count++); - // save_raw_rgb565(filename, self->rgb565_buffer, OUTPUT_WIDTH, OUTPUT_HEIGHT); - mp_obj_t result = mp_obj_new_memoryview('b', OUTPUT_WIDTH * OUTPUT_HEIGHT * 2, self->rgb565_buffer); + // save_raw_rgb565(filename, self->rgb565_buffer, self->output_width, self->output_height); + mp_obj_t result = mp_obj_new_memoryview('b', self->output_width * self->output_height * 2, self->rgb565_buffer); res = ioctl(self->fd, VIDIOC_QBUF, &buf); if (res < 0) { mp_raise_OSError(-res); @@ -277,6 +305,10 @@ static mp_obj_t webcam_init(size_t n_args, const mp_obj_t *args) { self->fd = -1; self->gray_buffer = NULL; self->rgb565_buffer = NULL; + self->input_width = 0; // Will be set from V4L2 format in init_webcam + self->input_height = 0; // Will be set from V4L2 format in init_webcam + self->output_width = 0; // Will use default OUTPUT_WIDTH in init_webcam + self->output_height = 0; // Will use default OUTPUT_HEIGHT in init_webcam int res = init_webcam(self, device); if (res < 0) { @@ -309,6 +341,65 @@ static mp_obj_t webcam_capture_frame(mp_obj_t self_in, mp_obj_t format) { } MP_DEFINE_CONST_FUN_OBJ_2(webcam_capture_frame_obj, webcam_capture_frame); +static mp_obj_t webcam_reconfigure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + // NOTE: This function only changes OUTPUT resolution (what Python receives). + // The INPUT resolution (what the webcam captures from V4L2) remains fixed at 640x480. + // The conversion functions will crop/scale from input to output resolution. + // TODO: Add support for changing input resolution (requires V4L2 reinit) + + enum { ARG_self, ARG_output_width, ARG_output_height }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_self, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_output_width, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_output_height, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + webcam_obj_t *self = MP_OBJ_TO_PTR(args[ARG_self].u_obj); + + // Get new dimensions (keep current if not specified) + int new_width = args[ARG_output_width].u_int; + int new_height = args[ARG_output_height].u_int; + + if (new_width == 0) new_width = self->output_width; + if (new_height == 0) new_height = self->output_height; + + // Validate dimensions + if (new_width <= 0 || new_height <= 0 || new_width > 1920 || new_height > 1920) { + mp_raise_ValueError(MP_ERROR_TEXT("Invalid output dimensions")); + } + + // If dimensions changed, reallocate buffers + if (new_width != self->output_width || new_height != self->output_height) { + // Free old buffers + free(self->gray_buffer); + free(self->rgb565_buffer); + + // Update dimensions + self->output_width = new_width; + self->output_height = new_height; + + // Allocate new buffers + self->gray_buffer = (unsigned char *)malloc(self->output_width * self->output_height * sizeof(unsigned char)); + self->rgb565_buffer = (uint16_t *)malloc(self->output_width * self->output_height * sizeof(uint16_t)); + + if (!self->gray_buffer || !self->rgb565_buffer) { + free(self->gray_buffer); + free(self->rgb565_buffer); + self->gray_buffer = NULL; + self->rgb565_buffer = NULL; + mp_raise_OSError(MP_ENOMEM); + } + + WEBCAM_DEBUG_PRINT("Webcam reconfigured to %dx%d\n", self->output_width, self->output_height); + } + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(webcam_reconfigure_obj, 1, webcam_reconfigure); + static const mp_obj_type_t webcam_type = { { &mp_type_type }, .name = MP_QSTR_Webcam, @@ -321,6 +412,7 @@ static const mp_rom_map_elem_t mp_module_webcam_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_capture_frame), MP_ROM_PTR(&webcam_capture_frame_obj) }, { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&webcam_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR_free_buffer), MP_ROM_PTR(&webcam_free_buffer_obj) }, + { MP_ROM_QSTR(MP_QSTR_reconfigure), MP_ROM_PTR(&webcam_reconfigure_obj) }, }; static MP_DEFINE_CONST_DICT(mp_module_webcam_globals, mp_module_webcam_globals_table); diff --git a/internal_filesystem/apps/com.micropythonos.camera/META-INF/MANIFEST.JSON b/internal_filesystem/apps/com.micropythonos.camera/META-INF/MANIFEST.JSON index 360dd3c8..1a2cde4f 100644 --- a/internal_filesystem/apps/com.micropythonos.camera/META-INF/MANIFEST.JSON +++ b/internal_filesystem/apps/com.micropythonos.camera/META-INF/MANIFEST.JSON @@ -22,6 +22,11 @@ "category": "default" } ] + }, + { + "entrypoint": "assets/camera_app.py", + "classname": "CameraSettingsActivity", + "intent_filters": [] } ] } diff --git a/internal_filesystem/apps/com.micropythonos.camera/assets/camera_app.py b/internal_filesystem/apps/com.micropythonos.camera/assets/camera_app.py index 3d9eb8b0..6890f0f9 100644 --- a/internal_filesystem/apps/com.micropythonos.camera/assets/camera_app.py +++ b/internal_filesystem/apps/com.micropythonos.camera/assets/camera_app.py @@ -13,6 +13,8 @@ except Exception as e: print(f"Info: could not import webcam module: {e}") from mpos.apps import Activity +from mpos.config import SharedPreferences +from mpos.content.intent import Intent import mpos.time class CameraApp(Activity): @@ -20,6 +22,9 @@ class CameraApp(Activity): width = 240 height = 240 + # Resolution preferences + prefs = None + status_label_text = "No camera found." status_label_text_searching = "Searching QR codes...\n\nHold still and try varying scan distance (10-25cm) and QR size (4-12cm). Ensure proper lighting." status_label_text_found = "Decoding QR..." @@ -42,7 +47,24 @@ class CameraApp(Activity): status_label = None status_label_cont = None + def load_resolution_preference(self): + """Load resolution preference from SharedPreferences and update width/height.""" + if not self.prefs: + self.prefs = SharedPreferences("com.micropythonos.camera") + + resolution_str = self.prefs.get_string("resolution", "240x240") + try: + width_str, height_str = resolution_str.split('x') + self.width = int(width_str) + self.height = int(height_str) + print(f"Camera resolution loaded: {self.width}x{self.height}") + except Exception as e: + print(f"Error parsing resolution '{resolution_str}': {e}, using default 240x240") + self.width = 240 + self.height = 240 + def onCreate(self): + self.load_resolution_preference() self.scanqr_mode = self.getIntent().extras.get("scanqr_mode") main_screen = lv.obj() main_screen.set_style_pad_all(0, 0) @@ -56,6 +78,16 @@ class CameraApp(Activity): 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(main_screen) + settings_button.set_size(60,60) + settings_button.align(lv.ALIGN.TOP_LEFT, 0, 0) + 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.snap_button = lv.button(main_screen) self.snap_button.set_size(60, 60) self.snap_button.align(lv.ALIGN.RIGHT_MID, 0, 0) @@ -103,10 +135,10 @@ class CameraApp(Activity): self.setContentView(main_screen) def onResume(self, screen): - self.cam = init_internal_cam() + self.cam = init_internal_cam(self.width, self.height) if not self.cam: # try again because the manual i2c poweroff leaves it in a bad state - self.cam = init_internal_cam() + self.cam = init_internal_cam(self.width, self.height) if self.cam: self.image.set_rotation(900) # internal camera is rotated 90 degrees else: @@ -114,6 +146,9 @@ class CameraApp(Activity): try: self.cam = webcam.init("/dev/video0") self.use_webcam = True + # Reconfigure webcam to use saved resolution + print(f"Reconfiguring webcam to {self.width}x{self.height}") + webcam.reconfigure(self.cam, output_width=self.width, output_height=self.height) except Exception as e: print(f"camera app: webcam exception: {e}") if self.cam: @@ -241,7 +276,42 @@ class CameraApp(Activity): self.start_qr_decoding() else: self.stop_qr_decoding() - + + def open_settings(self): + """Launch the camera settings activity.""" + intent = Intent(activity_class=CameraSettingsActivity) + self.startActivityForResult(intent, self.handle_settings_result) + + def handle_settings_result(self, result): + """Handle result from settings activity.""" + if result.get("result_code") == True: + print("Settings changed, reloading resolution...") + # Reload resolution preference + self.load_resolution_preference() + + # Recreate image descriptor with new dimensions + self.image_dsc["header"]["w"] = self.width + self.image_dsc["header"]["h"] = self.height + self.image_dsc["header"]["stride"] = self.width * 2 + self.image_dsc["data_size"] = self.width * self.height * 2 + + # Reconfigure camera if active + if self.cam: + if self.use_webcam: + print(f"Reconfiguring webcam to {self.width}x{self.height}") + webcam.reconfigure(self.cam, output_width=self.width, output_height=self.height) + else: + # For internal camera, need to reinitialize + print(f"Reinitializing internal camera to {self.width}x{self.height}") + if self.capture_timer: + self.capture_timer.delete() + self.cam.deinit() + self.cam = init_internal_cam(self.width, self.height) + if self.cam: + self.capture_timer = lv.timer_create(self.try_capture, 100, None) + + self.set_image_size() + def try_capture(self, event): #print("capturing camera frame") try: @@ -262,9 +332,36 @@ class CameraApp(Activity): # Non-class functions: -def init_internal_cam(): +def init_internal_cam(width=240, height=240): + """Initialize internal camera with specified resolution.""" 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, + (640, 480): FrameSize.VGA, + (800, 600): FrameSize.SVGA, + (1024, 768): FrameSize.XGA, + (1280, 720): FrameSize.HD, + (1280, 1024): FrameSize.SXGA, + (1600, 1200): FrameSize.UXGA, + (1920, 1080): FrameSize.FHD, + } + + frame_size = resolution_map.get((width, height), FrameSize.R240X240) + print(f"init_internal_cam: Using FrameSize for {width}x{height}") + cam = Camera( data_pins=[12,13,15,11,14,10,7,2], vsync_pin=6, @@ -277,15 +374,9 @@ def init_internal_cam(): powerdown_pin=-1, reset_pin=-1, pixel_format=PixelFormat.RGB565, - #pixel_format=PixelFormat.GRAYSCALE, - frame_size=FrameSize.R240X240, - grab_mode=GrabMode.LATEST + frame_size=frame_size, + grab_mode=GrabMode.LATEST ) - #cam.init() automatically done when creating the Camera() - #cam.reconfigure(frame_size=FrameSize.HVGA) - #frame_size=FrameSize.HVGA, # 480x320 - #frame_size=FrameSize.QVGA, # 320x240 - #frame_size=FrameSize.QQVGA # 160x120 cam.set_vflip(True) return cam except Exception as e: @@ -311,3 +402,124 @@ def remove_bom(buffer): if buffer.startswith(bom): return buffer[3:] return buffer + + +class CameraSettingsActivity(Activity): + """Settings activity for camera resolution configuration.""" + + # Resolution options for desktop/webcam + WEBCAM_RESOLUTIONS = [ + ("160x120", "160x120"), + ("240x240", "240x240"), # Default + ("320x240", "320x240"), + ("480x320", "480x320"), + ("640x480", "640x480"), + ("800x600", "800x600"), + ("1024x768", "1024x768"), + ("1280x720", "1280x720"), + ] + + # Resolution options for internal camera (ESP32) - all available FrameSize options + ESP32_RESOLUTIONS = [ + ("96x96", "96x96"), + ("160x120", "160x120"), + ("128x128", "128x128"), + ("176x144", "176x144"), + ("240x176", "240x176"), + ("240x240", "240x240"), # Default + ("320x240", "320x240"), + ("320x320", "320x320"), + ("400x296", "400x296"), + ("480x320", "480x320"), + ("640x480", "640x480"), + ("800x600", "800x600"), + ("1024x768", "1024x768"), + ("1280x720", "1280x720"), + ("1280x1024", "1280x1024"), + ("1600x1200", "1600x1200"), + ("1920x1080", "1920x1080"), + ] + + dropdown = None + current_resolution = None + + def onCreate(self): + # Load preferences + prefs = SharedPreferences("com.micropythonos.camera") + self.current_resolution = prefs.get_string("resolution", "240x240") + + # Create main screen + screen = lv.obj() + screen.set_size(lv.pct(100), lv.pct(100)) + screen.set_style_pad_all(10, 0) + + # Title + title = lv.label(screen) + title.set_text("Camera Settings") + title.align(lv.ALIGN.TOP_MID, 0, 10) + + # Resolution label + resolution_label = lv.label(screen) + resolution_label.set_text("Resolution:") + resolution_label.align(lv.ALIGN.TOP_LEFT, 0, 50) + + # Detect if we're on desktop or ESP32 based on available modules + try: + import webcam + resolutions = self.WEBCAM_RESOLUTIONS + print("Using webcam resolutions") + except: + resolutions = self.ESP32_RESOLUTIONS + print("Using ESP32 camera resolutions") + + # Create dropdown + self.dropdown = lv.dropdown(screen) + self.dropdown.set_size(200, 40) + self.dropdown.align(lv.ALIGN.TOP_LEFT, 0, 80) + + # Build dropdown options string + options_str = "\n".join([label for label, _ in resolutions]) + self.dropdown.set_options(options_str) + + # Set current selection + for idx, (label, value) in enumerate(resolutions): + if value == self.current_resolution: + self.dropdown.set_selected(idx) + break + + # Save button + save_button = lv.button(screen) + save_button.set_size(100, 50) + save_button.align(lv.ALIGN.BOTTOM_MID, -60, -10) + save_button.add_event_cb(lambda e: self.save_and_close(resolutions), lv.EVENT.CLICKED, None) + save_label = lv.label(save_button) + save_label.set_text("Save") + save_label.center() + + # Cancel button + cancel_button = lv.button(screen) + cancel_button.set_size(100, 50) + cancel_button.align(lv.ALIGN.BOTTOM_MID, 60, -10) + 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() + + self.setContentView(screen) + + def save_and_close(self, resolutions): + """Save selected resolution and return result.""" + selected_idx = self.dropdown.get_selected() + _, new_resolution = resolutions[selected_idx] + + # Save to preferences + prefs = SharedPreferences("com.micropythonos.camera") + editor = prefs.edit() + editor.put_string("resolution", new_resolution) + editor.commit() + + print(f"Camera resolution saved: {new_resolution}") + + # Return success result + self.setResult(True, {"resolution": new_resolution}) + 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 3b98029c..9e193572 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/wifi.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.wifi/assets/wifi.py @@ -160,7 +160,7 @@ class WiFi(Activity): def password_page_result_cb(self, result): print(f"PasswordPage finished, result: {result}") - if result.get("result_code"): + if result.get("result_code") is True: data = result.get("data") if data: self.start_attempt_connecting(data.get("ssid"), data.get("password"))