Przeglądaj źródła

resolve #2, resolve #3 manual mode selection, qrcodes w/multi lines

Bob Matcuk 2 lat temu
rodzic
commit
202ca0baed
4 zmienionych plików z 364 dodań i 97 usunięć
  1. 69 17
      README.md
  2. 21 25
      qrcode.c
  3. 2 2
      qrcode.h
  4. 272 53
      qrcode_app.c

+ 69 - 17
README.md

@@ -23,22 +23,49 @@ Version: 0
 Message: your content here
 ```
 
+qrcode v2 supports a newer format as well (the old format still works for
+backward compatibility, or, if you don't need the newer features, the app will
+read version "0" files faster):
+
+```
+Filetype: QRCode
+Version: 1
+QRMode: B
+QRVersion: 6
+QRECC: L
+Message: your content here
+Message: multi-line content is possible
+```
+
+In a version "1" file, the `QRMode`, `QRVersion`, and `QRECC` are optional
+(though, must be in that order if more than one are specified). The app will
+attempt to use the specified mode, version, and/or ECC, if the content will
+fit. Otherwise, it may select a different mode, version, and/or ECC. Keep
+reading to learn about the meaning of `QRMode`, `QRVersion`, and `QRECC`.
+
+Version "1" files also support multi-line content. Each line starting with
+`Message:` will be concatenated together with newline characters.
+
+My recommendation is to allow the app to select a mode, version, and ECC level
+for you and, then, if you find that your qrcode reader prefers specific
+settings, update the file appropriately.
+
 ### Message Format
 qrcodes support 4 formats called "modes": numeric, alpha-numeric, binary, and
 kanji. Because of the limited screen real-estate on the [Flipper Zero], you'll
 want to pick the best mode for the data you are trying to display.
 
-The app will automatically detect the best mode to use, so the only thing you
-need to do is make sure the message in your file is formatted to use the best
-mode. For example, if your message is entirely numeric, make sure you don't
-include any extraneous punctuation in your file. If you're only encoding a
-domain name, make sure it's uppercase to take advantage of alpha-numeric mode,
-etc.
+If unspecified in the `.qrcode` file, the app will automatically detect the
+best mode to use based on the message content.
 
-#### Numeric Mode
-Consists of only numbers, nothing else. This mode can encode the most data.
+#### Numeric Mode (QRMode: N)
+Consists of only numbers, nothing else. This mode can encode the most data and
+is useful for things like phone numbers. To use this mode, your message must
+_not_ contain non-numeric characters. For example, a message content of "(xxx)
+xxx-xxxx" can _not_ use numeric mode (it would require "binary" mode, in fact).
+Instead, your message should just be "xxxxxxxxxx".
 
-#### Alpha-Numeric Mode
+#### Alpha-Numeric Mode (QRMode: A)
 This mode can encode numbers, uppercase letters *only*, spaces, and the
 following symbols: `$%*+-./:`. This format _may_ be appropriate for urls, as
 long as you're only encoding the domain name and you remember to use uppercase
@@ -48,7 +75,7 @@ case-sensitive.
 
 A qrcode in alpha-numeric mode can encode ~40% less data than numeric mode.
 
-#### Binary Mode
+#### Binary Mode (QRMode: B)
 This mode is a little bit of a misnomer: binary mode simply means that the
 message will be encoded as 8-bit bytes. The qrcode standard stipulates that
 text will use ISO-8859-1 (also known as Latin-1) encoding, _not_ utf8 as would
@@ -56,22 +83,48 @@ be the standard these days. However, _some_ readers _may_ automatically detect
 utf8. To be standard-compliant, that basically means you can only use Latin
 letters, numbers, and symbols.
 
+Multi-line messages will always be in binary mode, since the other modes cannot
+encode a newline character.
+
 A qrcode in binary mode can encode ~60% less data than numeric mode, and ~30%
 less than alpha-numeric.
 
-#### Kanji Mode
+#### Kanji Mode (QRMode: K)
 This mode is unsupported, so I won't go into detail. A limitation of the
 underlying qrcode library that I'm using, unfortunately. If there's interest,
 perhaps I'll hack in support sometime.
 
+### QRVersion
+A qrcode's version specifies how "big" it is. Higher versions contain more
+"modules" (ie, the "pixels" that make up qrcodes) and, thus, can encode more
+data. A version 1 qrcode contains 21x21 modules, whereas a version 11 code (the
+largest the Flipper Zero can display) contains 61x61 modules. The modules of a
+version 1 code will be 3x3 pixels on the Flipper Zero screen; version 2 and 3
+qrcodes will each have 2x2 pixel modules; and version 4 through 11 qrcodes will
+have single pixel modules.
+
+If unspecified in the `.qrcode` file, the app will automatically select the
+lowest version that can contain all of the message content, given the mode
+selected in the previous step.
+
+### QRECC
+A qrcode's ECC level determines the qrcode's resilience to "damage". In the
+case of the Flipper Zero, "damage" might be a dirty screen, dead pixels, or
+even screen glare. Higher ECC modes are more resilient, but can contain less
+data. The ECC modes are Low, Medium, Quartile, and High and can be specified in
+the `.qrcode` file using the first letter (L, M, Q, and H).
+
+qrcode readers may have an easier time reading qrcodes with higher ECC levels,
+so, if unspecified in the `.qrcode` file, the app will select the highest ECC
+level that can contain all of the message content, given the qrcode mode and
+version selected in the previous steps.
+
 ## Using the App
 The app is fairly straightforward. When it first starts, the file browser will
 automatically open to the `qrcodes` directory and display any `.qrcode` files.
 Select one using the arrow keys and the center button. The qrcode will display.
-If you push the right arrow, some stats will display: the qrcode "Version" -
-which corresponds to how big it is; the ECC level - which determines the
-qrcode's resilience to damage, such as a dirty screen (Low, Medium, Quartile,
-and High); and the qrcode Mode (Numeric, Alpha-Numeric, Binary, or Kanji).
+If you push the right arrow, some stats will display: the qrcode "Version"; the
+ECC level; and the qrcode Mode (Numeric, Alpha-Numeric, Binary, or Kanji).
 
 While viewing the stats, you can select Version or ECC using the up and down
 arrows and the center button. You can then increase or decrease the Version or
@@ -133,7 +186,6 @@ git clone git@github.com:bmatcuk/flipperzero-qrcode.git
 Next, in the base of the [flipperzero-firmware] directory, run fbt:
 
 ```bash
-cd ..
 ./fbt fap_qrcode
 ```
 
@@ -146,7 +198,7 @@ find the .fap, should it change in the future).
 This application uses the [QRCode] library by ricmoo. This is the same library
 that is in the lib directory of the flipper-firmware repo (which was originally
 included for a [now-removed demo app]), but modified slightly to fix some
-compiler errors.
+compiler errors and allow the explicit selection of the qrcode mode.
 
 [now-removed demo app]: https://github.com/flipperdevices/flipperzero-firmware/pull/160/files
 [flipperzero-firmware]: https://github.com/flipperdevices/flipperzero-firmware

+ 21 - 25
qrcode.c

@@ -119,21 +119,21 @@ static int8_t getAlphanumeric(char c) {
     return -1;
 }
 
-static bool isAlphanumeric(const char *text, uint16_t length) {
-    while (length != 0) {
-        if (getAlphanumeric(text[--length]) == -1) { return false; }
-    }
-    return true;
-}
+/* static bool isAlphanumeric(const char *text, uint16_t length) { */
+/*     while (length != 0) { */
+/*         if (getAlphanumeric(text[--length]) == -1) { return false; } */
+/*     } */
+/*     return true; */
+/* } */
 
 
-static bool isNumeric(const char *text, uint16_t length) {
-    while (length != 0) {
-        char c = text[--length];
-        if (c < '0' || c > '9') { return false; }
-    }
-    return true;
-}
+/* static bool isNumeric(const char *text, uint16_t length) { */
+/*     while (length != 0) { */
+/*         char c = text[--length]; */
+/*         if (c < '0' || c > '9') { return false; } */
+/*     } */
+/*     return true; */
+/* } */
 
 
 // We store the following tightly packed (less 8) in modeInfo
@@ -614,11 +614,8 @@ static void rs_getRemainder(uint8_t degree, uint8_t *coeff, uint8_t *data, uint8
 
 
 
-static int8_t encodeDataCodewords(BitBucket *dataCodewords, const uint8_t *text, uint16_t length, uint8_t version) {
-    int8_t mode = MODE_BYTE;
-    
-    if (isNumeric((char*)text, length)) {
-        mode = MODE_NUMERIC;
+static int8_t encodeDataCodewords(BitBucket *dataCodewords, const uint8_t *text, uint16_t length, int8_t mode, uint8_t version) {
+    if (mode == MODE_NUMERIC) {
         bb_appendBits(dataCodewords, 1 << MODE_NUMERIC, 4);
         bb_appendBits(dataCodewords, length, getModeBits(version, MODE_NUMERIC));
 
@@ -639,8 +636,7 @@ static int8_t encodeDataCodewords(BitBucket *dataCodewords, const uint8_t *text,
             bb_appendBits(dataCodewords, accumData, accumCount * 3 + 1);
         }
         
-    } else if (isAlphanumeric((char*)text, length)) {
-        mode = MODE_ALPHANUMERIC;
+    } else if (mode == MODE_ALPHANUMERIC) {
         bb_appendBits(dataCodewords, 1 << MODE_ALPHANUMERIC, 4);
         bb_appendBits(dataCodewords, length, getModeBits(version, MODE_ALPHANUMERIC));
 
@@ -758,7 +754,7 @@ uint16_t qrcode_getBufferSize(uint8_t version) {
 }
 
 // @TODO: Return error if data is too big.
-int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length) {
+int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, int8_t mode, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length) {
     uint8_t size = version * 4 + 17;
     qrcode->version = version;
     qrcode->size = size;
@@ -781,7 +777,7 @@ int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8
     bb_initBuffer(&codewords, codewordBytes, (int32_t)sizeof(codewordBytes));
     
     // Place the data code words into the buffer
-    int8_t mode = encodeDataCodewords(&codewords, data, length, version);
+    mode = encodeDataCodewords(&codewords, data, length, mode, version);
     
     if (mode < 0) { return -1; }
     qrcode->mode = mode;
@@ -834,9 +830,9 @@ int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8
     return 0;
 }
 
-int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data) {
-    return qrcode_initBytes(qrcode, modules, version, ecc, (uint8_t*)data, strlen(data));
-}
+/* int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data) { */
+/*     return qrcode_initBytes(qrcode, modules, version, ecc, (uint8_t*)data, strlen(data)); */
+/* } */
 
 bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y) {
     if (x >= qrcode->size || y >= qrcode->size) {

+ 2 - 2
qrcode.h

@@ -85,8 +85,8 @@ extern "C"{
 
 uint16_t qrcode_getBufferSize(uint8_t version);
 
-int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data);
-int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length);
+/* int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data); */
+int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, int8_t mode, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length);
 
 bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y);
 

+ 272 - 53
qrcode_app.c

@@ -14,7 +14,10 @@
 #define QRCODE_FOLDER ANY_PATH("qrcodes")
 #define QRCODE_EXTENSION ".qrcode"
 #define QRCODE_FILETYPE "QRCode"
-#define QRCODE_FILE_VERSION 0
+#define QRCODE_FILE_VERSION 1
+
+/** Valid modes are Numeric (0), Alpha-Numeric (1), and Binary (2) */
+#define MAX_QRCODE_MODE 2
 
 /**
  * Maximum version is 11 because the f0 screen is only 64 pixels high and
@@ -22,6 +25,9 @@
  */
 #define MAX_QRCODE_VERSION 11
 
+/** Valid ECC levels are Low (0), Medium (1), Quartile (2), and High (3) */
+#define MAX_QRCODE_ECC 3
+
 /** Maximum length by mode, ecc, and version */
 static const uint16_t MAX_LENGTH[3][4][MAX_QRCODE_VERSION] = {
     {
@@ -56,6 +62,8 @@ typedef struct {
     FuriMutex** mutex;
     FuriString* message;
     QRCode* qrcode;
+    uint8_t min_mode;
+    uint8_t max_mode;
     uint8_t min_version;
     uint8_t max_ecc_at_min_version;
     bool loading;
@@ -63,6 +71,7 @@ typedef struct {
     bool show_stats;
     uint8_t selected_idx;
     bool edit;
+    uint8_t set_mode;
     uint8_t set_version;
     uint8_t set_ecc;
 } QRCodeApp;
@@ -81,6 +90,29 @@ static char get_ecc_char(uint8_t ecc) {
     }
 }
 
+/**
+ * @param ecc A character representing an ECC mode (L, M, Q, or H)
+ * @returns the ecc level or 255 representing an unknown ECC mode
+ */
+static uint8_t get_ecc_value(char ecc) {
+    switch (ecc) {
+        case 'L':
+        case 'l':
+            return 0;
+        case 'M':
+        case 'm':
+            return 1;
+        case 'Q':
+        case 'q':
+            return 2;
+        case 'H':
+        case 'h':
+            return 3;
+        default:
+            return 255;
+    }
+}
+
 /**
  * @param mode qrcode mode
  * @returns a character corresponding to the mode
@@ -95,6 +127,26 @@ static char get_mode_char(uint8_t mode) {
     }
 }
 
+/**
+ * @param mode A character representing a qrcode mode (N, A, or B)
+ * @returns the mode or 255 representing an unknown mode
+ */
+static uint8_t get_mode_value(char mode) {
+    switch (mode) {
+        case 'N':
+        case 'n':
+            return 0;
+        case 'A':
+        case 'a':
+            return 1;
+        case 'B':
+        case 'b':
+            return 2;
+        default:
+            return 255;
+    }
+}
+
 /**
  * Render
  * @param canvas The canvas to render to
@@ -140,34 +192,42 @@ static void render_callback(Canvas* canvas, void* ctx) {
             FuriString* str = furi_string_alloc();
 
             if (!instance->edit || instance->selected_idx == 0) {
-                furi_string_printf(str, "Ver: %i", instance->set_version);
-                canvas_draw_str(canvas, left + 5, top + font_height, furi_string_get_cstr(str));
+                furi_string_printf(str, "Mod: %c", get_mode_char(instance->set_mode));
+                canvas_draw_str(canvas, left + 5, font_height + top, furi_string_get_cstr(str));
                 if (instance->selected_idx == 0) {
                     canvas_draw_triangle(canvas, left, top + font_height / 2, font_height - 4, 4, CanvasDirectionLeftToRight);
                 }
                 if (instance->edit) {
-                    uint8_t arrow_left = left + 5 + canvas_string_width(canvas, "Ver: 8") / 2;
+                    uint8_t arrow_left = left + 5 + canvas_string_width(canvas, "Mod: B") / 2;
                     canvas_draw_triangle(canvas, arrow_left, top, font_height - 4, 4, CanvasDirectionBottomToTop);
                     canvas_draw_triangle(canvas, arrow_left, top + font_height + 1, font_height - 4, 4, CanvasDirectionTopToBottom);
                 }
             }
 
             if (!instance->edit || instance->selected_idx == 1) {
-                furi_string_printf(str, "ECC: %c", get_ecc_char(instance->set_ecc));
+                furi_string_printf(str, "Ver: %i", instance->set_version);
                 canvas_draw_str(canvas, left + 5, 2 * font_height + top + 2, furi_string_get_cstr(str));
                 if (instance->selected_idx == 1) {
                     canvas_draw_triangle(canvas, left, 3 * font_height / 2 + top + 2, font_height - 4, 4, CanvasDirectionLeftToRight);
                 }
                 if (instance->edit) {
-                    uint8_t arrow_left = left + 5 + canvas_string_width(canvas, "ECC: H") / 2;
+                    uint8_t arrow_left = left + 5 + canvas_string_width(canvas, "Ver: 8") / 2;
                     canvas_draw_triangle(canvas, arrow_left, font_height + top + 2, font_height - 4, 4, CanvasDirectionBottomToTop);
                     canvas_draw_triangle(canvas, arrow_left, 2 * font_height + top + 3, font_height - 4, 4, CanvasDirectionTopToBottom);
                 }
             }
 
-            if (!instance->edit) {
-                furi_string_printf(str, "Mod: %c", get_mode_char(instance->qrcode->mode));
+            if (!instance->edit || instance->selected_idx == 2) {
+                furi_string_printf(str, "ECC: %c", get_ecc_char(instance->set_ecc));
                 canvas_draw_str(canvas, left + 5, 3 * font_height + top + 4, furi_string_get_cstr(str));
+                if (instance->selected_idx == 2) {
+                    canvas_draw_triangle(canvas, left, 5 * font_height / 2 + top + 4, font_height - 4, 4, CanvasDirectionLeftToRight);
+                }
+                if (instance->edit) {
+                    uint8_t arrow_left = left + 5 + canvas_string_width(canvas, "ECC: H") / 2;
+                    canvas_draw_triangle(canvas, arrow_left, 2 * font_height + top + 4, font_height - 4, 4, CanvasDirectionBottomToTop);
+                    canvas_draw_triangle(canvas, arrow_left, 3 * font_height + top + 5, font_height - 4, 4, CanvasDirectionTopToBottom);
+                }
             }
 
             furi_string_free(str);
@@ -265,19 +325,20 @@ static void qrcode_free(QRCode* qrcode) {
  * sufficiently large enough to encode the full message. It is also assumed
  * that the old qrcode will be free'd by the caller.
  * @param instance The qrcode app instance
+ * @param mode The qrcode mode to use
  * @param version The qrcode version to use
  * @param ecc The qrcode ECC level to use
  * @returns true if the qrcode was successfully created
  */
-static bool rebuild_qrcode(QRCodeApp* instance, uint8_t version, uint8_t ecc) {
+static bool rebuild_qrcode(QRCodeApp* instance, uint8_t mode, uint8_t version, uint8_t ecc) {
     furi_assert(instance);
     furi_assert(instance->message);
 
     const char* cstr = furi_string_get_cstr(instance->message);
-    uint16_t len = strlen(cstr);
+    uint16_t len = (uint16_t)furi_string_size(instance->message);
     instance->qrcode = qrcode_alloc(version);
 
-    int8_t res = qrcode_initBytes(instance->qrcode, instance->qrcode->modules, version, ecc, (uint8_t*)cstr, len);
+    int8_t res = qrcode_initBytes(instance->qrcode, instance->qrcode->modules, (int8_t)mode, version, ecc, (uint8_t*)cstr, len);
     if (res != 0) {
         FURI_LOG_E(TAG, "Could not create qrcode");
 
@@ -289,13 +350,55 @@ static bool rebuild_qrcode(QRCodeApp* instance, uint8_t version, uint8_t ecc) {
     return true;
 }
 
+/**
+ * Determine the minimum version and maximum ECC for a message of a given
+ * length and mode.
+ * @param len The length of the message
+ * @param mode The mode of the encoded message
+ * @param version Pointer to variable that will receive the minimum version
+ * @param ecc Pointer to variable that will receive the maximum ECC
+ * @returns false if the data is too long for the given mode, true otherwise.
+ */
+static bool find_min_version_max_ecc(uint16_t len, uint8_t mode, uint8_t *version, uint8_t *ecc) {
+    // Figure out the smallest qrcode version that'll fit all of the data - we
+    // prefer the smallest version to maximize the pixel size of each module to
+    // improve reader performance. Here, version is the 0-based index. The
+    // qrcode_initBytes function will want a 1-based version number, so we'll
+    // add one later.
+    *ecc = ECC_LOW;
+    *version = 0;
+    while (*version < MAX_QRCODE_VERSION && MAX_LENGTH[mode][*ecc][*version] < len) {
+        (*version)++;
+    }
+
+    if (*version == MAX_QRCODE_VERSION) {
+        return false;
+    }
+
+    // Figure out the maximum ECC we can use. I shouldn't need to bounds-check
+    // ecc in this loop because I already know from the loop above that ECC_LOW
+    // (0) works... don't forget to add one to that version number, since we're
+    // using it as a 0-based number here, but qrcode_initBytes will want a
+    // 1-based number...
+    *ecc = ECC_HIGH;
+    while (MAX_LENGTH[mode][*ecc][*version] < len) {
+        (*ecc)--;
+    }
+    (*version)++;
+
+    return true;
+}
+
 /**
  * Load a qrcode from a string
  * @param instance The qrcode app instance
  * @param str The message to encode as a qrcode
+ * @param desired_mode User selected mode, 255 = unset
+ * @param desired_version User selected version, 255 = unset
+ * @param desired_ecc User selected ECC, 255 = unset
  * @returns true if the string was successfully loaded
  */
-static bool qrcode_load_string(QRCodeApp* instance, FuriString* str) {
+static bool qrcode_load_string(QRCodeApp* instance, FuriString* str, uint8_t desired_mode, uint8_t desired_version, uint8_t desired_ecc) {
     furi_assert(instance);
     furi_assert(str);
 
@@ -316,7 +419,7 @@ static bool qrcode_load_string(QRCodeApp* instance, FuriString* str) {
     bool result = false;
     do {
         const char* cstr = furi_string_get_cstr(str);
-        uint16_t len = strlen(cstr);
+        uint16_t len = (uint16_t)furi_string_size(str);
 
         instance->message = furi_string_alloc_set(str);
         if (!instance->message) {
@@ -324,49 +427,83 @@ static bool qrcode_load_string(QRCodeApp* instance, FuriString* str) {
             break;
         }
 
-        // figure out the qrcode "mode"
-        uint8_t mode = MODE_BYTE;
-        if      (is_numeric(cstr, len))      mode = MODE_NUMERIC;
-        else if (is_alphanumeric(cstr, len)) mode = MODE_ALPHANUMERIC;
-
-        // Figure out the smallest qrcode version that'll fit all of the data -
-        // we prefer the smallest version to maximize the pixel size of each
-        // module to improve reader performance. Here, version is the 0-based
-        // index. The qrcode_initBytes function will want a 1-based version
-        // number, so we'll add one later.
-        uint8_t ecc = ECC_LOW;
-        uint8_t version = 0;
-        while (version < MAX_QRCODE_VERSION && MAX_LENGTH[mode][ecc][version] < len) {
-            version++;
+        // figure out the minimum qrcode "mode"
+        int8_t min_mode = MODE_BYTE;
+        if      (is_numeric(cstr, len))      min_mode = MODE_NUMERIC;
+        else if (is_alphanumeric(cstr, len)) min_mode = MODE_ALPHANUMERIC;
+
+        // determine the maximum "mode"
+        int8_t max_mode = MAX_QRCODE_MODE;
+        uint8_t min_version = 0;
+        uint8_t max_ecc_at_min_version = 0;
+        while (max_mode >= min_mode && !find_min_version_max_ecc(len, (uint8_t)max_mode, &min_version, &max_ecc_at_min_version)) {
+            max_mode--;
         }
 
-        if (version == MAX_QRCODE_VERSION) {
+        // if the max is less than the min, the message is too long
+        if (max_mode < min_mode) {
             instance->too_long = true;
             break;
         }
 
-        // Figure out the maximum ECC we can use. I shouldn't need to
-        // bounds-check ecc in this loop because I already know from the loop
-        // above that ECC_LOW (0) works... don't forget to add one to that
-        // version number...
-        ecc = ECC_HIGH;
-        while (MAX_LENGTH[mode][ecc][version] < len) {
-            ecc--;
+        // pick a mode based on the min/max and desired mode
+        if (desired_mode == 255 || desired_mode < (uint8_t)min_mode) {
+            desired_mode = (uint8_t)min_mode;
+        } else if (desired_mode > (uint8_t)max_mode) {
+            desired_mode = (uint8_t)max_mode;
+        }
+        if (desired_mode != (uint8_t)max_mode) {
+            // if the desired mode equals the max mode, then min_version and
+            // max_ecc_at_min_version are already set appropriately by the max
+            // mode loop above... otherwise, we need to calculate them... this
+            // should always return true because we already know the desired
+            // mode is appropriate for the data, but, just in case...
+            if (!find_min_version_max_ecc(len, desired_mode, &min_version, &max_ecc_at_min_version)) {
+                instance->too_long = true;
+                break;
+            }
+        }
+
+        // ensure desired version and ecc are appropriate
+        if (desired_version == 255 || desired_version < min_version) {
+            desired_version = min_version;
+        } else if (desired_version > MAX_QRCODE_VERSION) {
+            desired_version = MAX_QRCODE_VERSION;
+        }
+        if (desired_version == min_version) {
+            if (desired_ecc > max_ecc_at_min_version) {
+                desired_ecc = max_ecc_at_min_version;
+            }
+        } else if (desired_ecc > MAX_QRCODE_ECC) {
+            desired_ecc = MAX_QRCODE_ECC;
         }
-        version++;
 
         // Build the qrcode
-        if (!rebuild_qrcode(instance, version, ecc)) {
-            furi_string_free(instance->message);
-            instance->message = NULL;
+        if (!rebuild_qrcode(instance, desired_mode, desired_version, desired_ecc)) {
             break;
         }
 
-        instance->min_version = instance->set_version = version;
-        instance->max_ecc_at_min_version = instance->set_ecc = ecc;
+        instance->min_mode = (uint8_t)min_mode;
+        instance->max_mode = (uint8_t)max_mode;
+        instance->set_mode = desired_mode;
+        instance->min_version = min_version;
+        instance->set_version = desired_version;
+        instance->max_ecc_at_min_version = max_ecc_at_min_version;
+        instance->set_ecc = desired_ecc;
         result = true;
     } while (false);
 
+    if (!result) {
+        if (instance->message) {
+            furi_string_free(instance->message);
+            instance->message = NULL;
+        }
+        if (instance->qrcode) {
+            qrcode_free(instance->qrcode);
+            instance->qrcode = NULL;
+        }
+    }
+
     instance->loading = false;
 
     furi_mutex_release(instance->mutex);
@@ -393,20 +530,71 @@ static bool qrcode_load_file(QRCodeApp* instance, const char* file_path) {
     do {
         if (!flipper_format_file_open_existing(file, file_path)) break;
 
-        uint32_t version = 0;
-        if (!flipper_format_read_header(file, temp_str, &version)) break;
+        uint32_t file_version = 0;
+        if (!flipper_format_read_header(file, temp_str, &file_version)) break;
         if (furi_string_cmp_str(temp_str, QRCODE_FILETYPE)
-                || version != QRCODE_FILE_VERSION) {
+                || file_version > QRCODE_FILE_VERSION) {
             FURI_LOG_E(TAG, "Incorrect file format or version");
             break;
         }
 
+        uint32_t desired_mode = 255;
+        uint32_t desired_version = 255;
+        uint32_t desired_ecc = 255;
+        if (file_version > 0) {
+            if (flipper_format_key_exist(file, "QRMode")) {
+                if (flipper_format_read_string(file, "QRMode", temp_str)) {
+                    if (furi_string_size(temp_str) > 0) {
+                        desired_mode = get_mode_value(furi_string_get_char(temp_str, 0));
+                    }
+                } else {
+                    FURI_LOG_E(TAG, "Could not read QRMode");
+                    desired_mode = 255;
+                }
+            }
+
+            if (flipper_format_key_exist(file, "QRVersion")) {
+                if (flipper_format_read_uint32(file, "QRVersion", &desired_version, 1)) {
+                    if (desired_version > MAX_QRCODE_VERSION) {
+                        FURI_LOG_E(TAG, "Invalid QRVersion");
+                        desired_version = 255;
+                    }
+                } else {
+                    FURI_LOG_E(TAG, "Could not read QRVersion");
+                    desired_version = 255;
+                }
+            }
+
+            if (flipper_format_key_exist(file, "QRECC")) {
+                if (flipper_format_read_string(file, "QRECC", temp_str)) {
+                    if (furi_string_size(temp_str) > 0) {
+                        desired_ecc = get_ecc_value(furi_string_get_char(temp_str, 0));
+                    }
+                } else {
+                    FURI_LOG_E(TAG, "Could not read QRECC");
+                    desired_ecc = 255;
+                }
+            }
+        }
+
         if (!flipper_format_read_string(file, "Message", temp_str)) {
             FURI_LOG_E(TAG, "Message is missing");
             break;
         }
+        if (file_version > 0) {
+            FuriString* msg_cont = furi_string_alloc();
+            while (flipper_format_key_exist(file, "Message")) {
+                if (!flipper_format_read_string(file, "Message", msg_cont)) {
+                    FURI_LOG_E(TAG, "Could not read next Message");
+                    break;
+                }
+                furi_string_push_back(temp_str, '\n');
+                furi_string_cat(temp_str, msg_cont);
+            }
+            furi_string_free(msg_cont);
+        }
 
-        if (!qrcode_load_string(instance, temp_str)) {
+        if (!qrcode_load_string(instance, temp_str, (uint8_t)desired_mode, (uint8_t)desired_version, (uint8_t)desired_ecc)) {
             break;
         }
 
@@ -526,9 +714,11 @@ int32_t qrcode_app(void* p) {
                     if (!instance->edit) {
                         instance->selected_idx = MAX(0, instance->selected_idx - 1);
                     } else {
-                        if (instance->selected_idx == 0 && instance->set_version < MAX_QRCODE_VERSION) {
+                        if (instance->selected_idx == 0 && instance->set_mode < instance->max_mode) {
+                            instance->set_mode++;
+                        } else if (instance->selected_idx == 1 && instance->set_version < MAX_QRCODE_VERSION) {
                             instance->set_version++;
-                        } else if (instance->selected_idx == 1) {
+                        } else if (instance->selected_idx == 2) {
                             uint8_t max_ecc = instance->set_version == instance->min_version ? instance->max_ecc_at_min_version : ECC_HIGH;
                             if (instance->set_ecc < max_ecc) {
                                 instance->set_ecc++;
@@ -537,29 +727,58 @@ int32_t qrcode_app(void* p) {
                     }
                 } else if (input.key == InputKeyDown) {
                     if (!instance->edit) {
-                        instance->selected_idx = MIN(1, instance->selected_idx + 1);
+                        instance->selected_idx = MIN(2, instance->selected_idx + 1);
                     } else {
-                        if (instance->selected_idx == 0 && instance->set_version > instance->min_version) {
+                        if (instance->selected_idx == 0 && instance->set_mode > instance->min_mode) {
+                            instance->set_mode--;
+                        } else if (instance->selected_idx == 1 && instance->set_version > instance->min_version) {
                             instance->set_version--;
                             if (instance->set_version == instance->min_version) {
-                                instance->set_ecc = MAX(instance->set_ecc, instance->max_ecc_at_min_version);
+                                instance->set_ecc = MIN(instance->set_ecc, instance->max_ecc_at_min_version);
                             }
-                        } else if (instance->selected_idx == 1 && instance->set_ecc > 0) {
+                        } else if (instance->selected_idx == 2 && instance->set_ecc > 0) {
                             instance->set_ecc--;
                         }
                     }
                 } else if (input.key == InputKeyOk) {
-                    if (instance->edit && (instance->set_version != instance->qrcode->version || instance->set_ecc != instance->qrcode->ecc)) {
+                    if (
+                            instance->edit
+                            && (instance->set_mode != instance->qrcode->mode
+                                || instance->set_version != instance->qrcode->version
+                                || instance->set_ecc != instance->qrcode->ecc)) {
+                        uint8_t orig_min_version = instance->min_version;
+                        uint8_t orig_max_ecc_at_min_version = instance->max_ecc_at_min_version;
+                        if (instance->set_mode != instance->qrcode->mode) {
+                            uint16_t len = (uint16_t)furi_string_size(instance->message);
+                            uint8_t min_version = 0;
+                            uint8_t max_ecc_at_min_version = 0;
+                            if (find_min_version_max_ecc(len, instance->set_mode, &min_version, &max_ecc_at_min_version)) {
+                                if (instance->set_version < min_version) {
+                                    instance->set_version = min_version;
+                                }
+                                if (instance->set_version == min_version && instance->set_ecc > max_ecc_at_min_version) {
+                                    instance->set_ecc = max_ecc_at_min_version;
+                                }
+                                instance->min_version = min_version;
+                                instance->max_ecc_at_min_version = max_ecc_at_min_version;
+                            } else {
+                                instance->set_mode = instance->qrcode->mode;
+                            }
+                        }
+
                         QRCode* qrcode = instance->qrcode;
                         instance->loading = true;
 
-                        if (rebuild_qrcode(instance, instance->set_version, instance->set_ecc)) {
+                        if (rebuild_qrcode(instance, instance->set_mode, instance->set_version, instance->set_ecc)) {
                             qrcode_free(qrcode);
                         } else {
                             FURI_LOG_E(TAG, "Could not rebuild qrcode");
                             instance->qrcode = qrcode;
+                            instance->set_mode = qrcode->mode;
                             instance->set_version = qrcode->version;
                             instance->set_ecc = qrcode->ecc;
+                            instance->min_version = orig_min_version;
+                            instance->max_ecc_at_min_version = orig_max_ecc_at_min_version;
                         }
 
                         instance->loading = false;