Browse Source

fw/applib/ui: add QRCode

Add a new API to render QR codes. Note that the implementation is not
really optimal in terms of allocated memory when rendering. LVGL, for
example, added some extra APIs to the underlying QR library to
pre-compute the minimal version given data size, thus minimizing
allocated memory. This can be improved later without changing the API
though.

Regarding app_malloc.json entry: I have no clue on how that is supposed
to work, so I just guessed values also with the "help" of static
asserts.

Signed-off-by: Gerard Marull-Paretas <gerard@teslabs.com>
Gerard Marull-Paretas 1 month ago
parent
commit
20a246184b
4 changed files with 314 additions and 0 deletions
  1. 7 0
      src/fw/applib/applib_malloc.json
  2. 188 0
      src/fw/applib/ui/qr_code.c
  3. 118 0
      src/fw/applib/ui/qr_code.h
  4. 1 0
      src/fw/applib/wscript

+ 7 - 0
src/fw/applib/applib_malloc.json

@@ -40,6 +40,7 @@
         "applib/ui/number_window.h",
         "applib/ui/option_menu_window.h",
         "applib/ui/property_animation_private.h",
+        "applib/ui/qr_code.h",
         "applib/ui/rotate_bitmap_layer.h",
         "applib/ui/scroll_layer.h",
         "applib/ui/selection_layer.h",
@@ -278,6 +279,12 @@
         "size_3x": 876,
         "dependencies": ["Window", "StatusBarLayer", "MenuLayer", "GBitmap", "GBitmap"],
         "_comment": "Not exported yet (only 3.x)"
+    }, {
+        "name": "QRCode",
+        "size_3x_padding": 0,
+        "size_3x": 72,
+        "dependencies": ["Layer"],
+        "_comment": "Not exported yet (only 3.x)"
     }]
 }
 

+ 188 - 0
src/fw/applib/ui/qr_code.c

@@ -0,0 +1,188 @@
+/*
+ * Copyright 2025 Core Devices LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "qr_code.h"
+
+#include <string.h>
+
+#include "applib/applib_malloc.auto.h"
+#include "applib/graphics/graphics.h"
+#include "system/passert.h"
+
+#include <qrcodegen.h>
+
+static inline enum qrcodegen_Ecc prv_ecc_to_qrcodegen(QRCodeECC ecc) {
+  switch (ecc) {
+    case QRCodeECCLow:
+      return qrcodegen_Ecc_LOW;
+    case QRCodeECCMedium:
+      return qrcodegen_Ecc_MEDIUM;
+    case QRCodeECCQuartile:
+      return qrcodegen_Ecc_QUARTILE;
+    case QRCodeECCHigh:
+      return qrcodegen_Ecc_HIGH;
+    default:
+      return qrcodegen_Ecc_MEDIUM;
+  }
+}
+
+static void prv_qr_code_update_proc(QRCode *qr_code, GContext *ctx) {
+  uint8_t *qr_code_buf;
+  uint8_t *tmp_buf;
+  int qr_size;
+  int mod_size;
+  int rend_size;
+  int offset_x;
+  int offset_y;
+  GColor old_fill_color;
+  bool ret;
+
+  if ((qr_code->data == NULL) || (qr_code->data_len == 0U) ||
+      (qr_code->layer.bounds.size.w <= 0) || (qr_code->layer.bounds.size.h <= 0)) {
+    return;
+  }
+
+  // NOTE: using maximum buffer length as we use qrcodegen_VERSION_MAX.
+  // We could potentially optimize this by calculating the minimum required version
+  // for the given input. LVGL does by adding some extra APIs to qrcodegen.
+  qr_code_buf = applib_malloc(qrcodegen_BUFFER_LEN_MAX);
+  if (qr_code_buf == NULL) {
+    return;
+  }
+
+  tmp_buf = applib_malloc(qrcodegen_BUFFER_LEN_MAX);
+  if (tmp_buf == NULL) {
+    applib_free(qr_code_buf);
+    return;
+  }
+
+  memcpy(tmp_buf, qr_code->data, qr_code->data_len);
+
+  ret = qrcodegen_encodeBinary(tmp_buf, qr_code->data_len, qr_code_buf,
+                               prv_ecc_to_qrcodegen(qr_code->ecc), qrcodegen_VERSION_MIN,
+                               qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
+  if (!ret) {
+    applib_free(tmp_buf);
+    applib_free(qr_code_buf);
+    return;
+  }
+
+  // Compute module size
+  qr_size = qrcodegen_getSize(qr_code_buf);
+  mod_size = MIN(qr_code->layer.bounds.size.w / qr_size, qr_code->layer.bounds.size.h / qr_size);
+  if (mod_size == 0) {
+    applib_free(tmp_buf);
+    applib_free(qr_code_buf);
+    return;
+  }
+
+  // Calculate actual rendered size
+  rend_size = qr_size * mod_size;
+
+  // Center the QR code
+  offset_x = qr_code->layer.bounds.origin.x + (qr_code->layer.bounds.size.w - rend_size) / 2;
+  offset_y = qr_code->layer.bounds.origin.y + (qr_code->layer.bounds.size.h - rend_size) / 2;
+
+  // Save current context state
+  old_fill_color = ctx->draw_state.fill_color;
+
+  // Draw background
+  graphics_context_set_fill_color(ctx, qr_code->bg_color);
+  graphics_fill_rect(ctx, &qr_code->layer.bounds);
+
+  // Draw QR code modules
+  graphics_context_set_fill_color(ctx, qr_code->fg_color);
+
+  for (int y = 0; y < qr_size; y++) {
+    for (int x = 0; x < qr_size; x++) {
+      if (qrcodegen_getModule(qr_code_buf, x, y)) {
+        GRect module_rect;
+
+        module_rect = GRect(offset_x + x * mod_size, offset_y + y * mod_size, mod_size, mod_size);
+
+        graphics_fill_rect(ctx, &module_rect);
+      }
+    }
+  }
+
+  // Restore context state
+  graphics_context_set_fill_color(ctx, old_fill_color);
+
+  // Free buffers
+  applib_free(qr_code_buf);
+  applib_free(tmp_buf);
+}
+
+void qr_code_init_with_parameters(QRCode *qr_code, const GRect *frame, const void *data,
+                                  size_t data_len, QRCodeECC ecc, GColor fg_color, GColor bg_color) {
+  PBL_ASSERTN(qr_code);
+  *qr_code = (QRCode){};
+  qr_code->layer.frame = *frame;
+  qr_code->layer.bounds = (GRect){{0, 0}, frame->size};
+  qr_code->layer.update_proc = (LayerUpdateProc)prv_qr_code_update_proc;
+  qr_code->data = data;
+  qr_code->data_len = data_len;
+  qr_code->ecc = ecc;
+  qr_code->fg_color = fg_color;
+  qr_code->bg_color = bg_color;
+  layer_set_clips(&qr_code->layer, true);
+  
+  layer_mark_dirty(&qr_code->layer);
+}
+
+void qr_code_init(QRCode *qr_code, const GRect *frame) {
+  qr_code_init_with_parameters(qr_code, frame, NULL, 0, QRCodeECCMedium, 
+                               GColorBlack, GColorWhite);
+}
+
+QRCode* qr_code_create(GRect frame) {
+  QRCode* qr_code = applib_type_malloc(QRCode);
+  if (qr_code) {
+    qr_code_init(qr_code, &frame);
+  }
+  return qr_code;
+}
+
+void qr_code_destroy(QRCode* qr_code) {
+  if (qr_code) {
+    applib_free(qr_code);
+  }
+}
+
+void qr_code_set_data(QRCode *qr_code, const void *data, size_t data_len) {
+  PBL_ASSERTN(qr_code);
+  qr_code->data = data;
+  qr_code->data_len = data_len;
+  layer_mark_dirty(&qr_code->layer);
+}
+
+void qr_code_set_ecc(QRCode *qr_code, QRCodeECC ecc) {
+  PBL_ASSERTN(qr_code);
+  qr_code->ecc = ecc;
+  layer_mark_dirty(&qr_code->layer);
+}
+
+void qr_code_set_bg_color(QRCode *qr_code, GColor color) {
+  PBL_ASSERTN(qr_code);
+  qr_code->bg_color = color;
+  layer_mark_dirty(&qr_code->layer);
+}
+
+void qr_code_set_fg_color(QRCode *qr_code, GColor color) {
+  PBL_ASSERTN(qr_code);
+  qr_code->fg_color = color;
+  layer_mark_dirty(&qr_code->layer);
+}

+ 118 - 0
src/fw/applib/ui/qr_code.h

@@ -0,0 +1,118 @@
+/*
+ * Copyright 2025 Core Devices LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <stddef.h>
+
+#include "applib/graphics/gtypes.h"
+#include "applib/ui/layer.h"
+
+//! @file qr_code.h
+//! @addtogroup UI
+//! @{
+//!   @addtogroup QR Code
+//!   @{
+
+//! QR Code error correction levels
+typedef enum {
+  //! Low error correction level (7% recovery capability)
+  QRCodeECCLow = 0,
+  //! Medium error correction level (15% recovery capability)
+  QRCodeECCMedium = 1,
+  //! Quartile error correction level (25% recovery capability)
+  QRCodeECCQuartile = 2,
+  //! High error correction level (30% recovery capability)
+  QRCodeECCHigh = 3
+} QRCodeECC;
+
+//! QR code structure
+typedef struct QRCode {
+  //! Layer
+  Layer layer;
+  //! QR code data buffer
+  const void *data;
+  //! Size of the QR code (number of modules per side)
+  size_t data_len;
+  //! Error correction level used
+  QRCodeECC ecc;
+  //! Foreground color of the QR code
+  GColor fg_color;
+  //! Background color of the QR code
+  GColor bg_color;
+} QRCode;
+
+//! Initializes the QRCode with given frame
+//! All previous contents are erased and the following default values are set:
+//!
+//! * Empty data
+//! * ECC: \ref QRCodeECCMedium
+//! * Foreground color: \ref GColorBlack
+//! * Background color: \ref GColorWhite
+//!
+//! The QR code is automatically marked dirty after this operation.
+//! @param qr_code The QRCode to initialize
+//! @param frame The frame with which to initialze the QRCode
+void qr_code_init(QRCode *qr_code, const GRect *frame);
+
+//! Creates a new QRCode on the heap and initializes it with the default values.
+//!
+//! * Empty data
+//! * ECC: \ref QRCodeECCMedium
+//! * Foreground color: \ref GColorBlack
+//! * Background color: \ref GColorWhite
+//!
+//! @param frame The frame with which to initialze the QRCode
+//! @return A pointer to the QRCode. `NULL` if the QRCode could not be created
+QRCode* qr_code_create(GRect frame);
+
+//! Destroys a QRCode previously created by qr_code_create.
+void qr_code_destroy(QRCode* qr_code);
+
+//! Sets the pointer to the data where the QRCode is supposed to find the data
+//! at a later point in time, when it needs to draw itself.
+//! @param qr_code The QRCode of which to set the text
+//! @param data The new data to set onto the QRCode.
+//! @param data_len Length of the data in bytes
+//! @note The data is not copied, so its buffer most likely cannot be stack allocated,
+//! but is recommended to be a buffer that is long-lived, at least as long as the QRCode
+//! is part of a visible Layer hierarchy.
+//! @see qr_code_get_text
+void qr_code_set_data(QRCode *qr_code, const void *data, size_t data_len);
+
+//! Sets the error correction level of the QR code
+//! @param qr_code The QRCode of which to set the error correction level
+//! @param ecc The new \ref QRCodeECC to set the error correction level to
+void qr_code_set_ecc(QRCode *qr_code, QRCodeECC ecc);
+
+//! Sets the background color of the QR code
+//! @param qr_code The QRCode of which to set the background color
+//! @param color The new \ref GColor to set the background to
+//! @see qr_code_set_fg_color
+void qr_code_set_bg_color(QRCode *qr_code, GColor color);
+
+//! Sets the foreground color of the QR code
+//! @param qr_code The QRCode of which to set the foreground color
+//! @param color The new \ref GColor to set the foreground color to
+//! @see qr_code_set_bg_color
+void qr_code_set_fg_color(QRCode *qr_code, GColor color);
+
+//! @internal
+void qr_code_init_with_parameters(QRCode *qr_code, const GRect *frame, const void *data,
+                                  size_t data_len, QRCodeECC ecc, GColor fg_color, GColor bg_color);
+
+//!   @} // end addtogroup QR Code
+//! @} // end addtogroup UI

+ 1 - 0
src/fw/applib/wscript

@@ -92,6 +92,7 @@ def build(bld):
                    'libbtutil_includes',
                    'fw_includes',
                    'upng',
+                   'qr_code_generator',
                    'jerry_port_includes',
                    'jerry_runtime_config',
                    'jerry_common_config',