Przeglądaj źródła

update qrcode app

MX 2 lat temu
rodzic
commit
084893ae41
5 zmienionych plików z 492 dodań i 168 usunięć
  1. 91 17
      README.md
  2. 1 1
      application.fam
  3. 22 33
      qrcode.c
  4. 2 6
      qrcode.h
  5. 376 111
      qrcode_app.c

+ 91 - 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
@@ -120,6 +173,27 @@ For example, if my ssid was "wifiball" and not broadcast, and the password was
 Message: WIFI:S:wifiball;P:pa$$\:word;T:WPA;H:true;
 ```
 
+## Example: vCard
+Phones can scan [vCard] qrcodes to automatically add a contact to their address
+book. Starting with qrcode v2, multi-line qrcodes can be created, allowing you
+to create vCards!
+
+```
+Filetype: QRCode
+Version: 1
+Message: BEGIN:VCARD
+Message: VERSION:3.0
+Message: N:Smith;John
+Message: FN:John Smith
+Message: ADR;TYPE=dom,home,postal,parcel:;;123 Example St;Exampleton;CA;90210;
+Message: BDAY:1970-01-01
+Message: TEL;TYPE=pref,voice,msg,cell:+18005551212
+Message: END:VCARD
+```
+
+Check the [vCard] specification to learn about all of the fields and their
+values.
+
 ## Building
 First, clone the [flipperzero-firmware] repo and then clone this repo in the
 `applications_user` directory:
@@ -133,7 +207,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 +219,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
@@ -154,3 +227,4 @@ compiler errors.
 [QRCode]: https://github.com/ricmoo/QRCode
 [qFlipper]: https://docs.flipperzero.one/qflipper
 [Releases]: https://github.com/bmatcuk/flipperzero-qrcode/releases/latest
+[vCard]: https://www.evenx.com/vcard-3-0-format-specification

+ 1 - 1
application.fam

@@ -1,7 +1,7 @@
 App(
     appid="qrcode",
     name="QR Code",
-    fap_version=(1, 1),
+    fap_version=(2,0),
     fap_description="Display qrcodes",
     fap_author="Bob Matcuk",
     fap_weburl="https://github.com/bmatcuk/flipperzero-qrcode",

+ 22 - 33
qrcode.c

@@ -175,24 +175,20 @@ 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 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 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; */
+/* } */
 
 // We store the following tightly packed (less 8) in modeInfo
 //               <=9  <=26  <= 40
@@ -703,11 +699,9 @@ static int8_t encodeDataCodewords(
     BitBucket* dataCodewords,
     const uint8_t* text,
     uint16_t length,
+    int8_t mode,
     uint8_t version) {
-    int8_t mode = MODE_BYTE;
-
-    if(isNumeric((char*)text, length)) {
-        mode = MODE_NUMERIC;
+    if(mode == MODE_NUMERIC) {
         bb_appendBits(dataCodewords, 1 << MODE_NUMERIC, 4);
         bb_appendBits(dataCodewords, length, getModeBits(version, MODE_NUMERIC));
 
@@ -728,8 +722,7 @@ static int8_t encodeDataCodewords(
             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));
 
@@ -853,6 +846,7 @@ uint16_t qrcode_getBufferSize(uint8_t version) {
 int8_t qrcode_initBytes(
     QRCode* qrcode,
     uint8_t* modules,
+    int8_t mode,
     uint8_t version,
     uint8_t ecc,
     uint8_t* data,
@@ -880,7 +874,7 @@ int8_t qrcode_initBytes(
     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;
@@ -938,14 +932,9 @@ int8_t qrcode_initBytes(
     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 - 6
qrcode.h

@@ -77,15 +77,11 @@ 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_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,

+ 376 - 111
qrcode_app.c

@@ -11,10 +11,13 @@
 #include "qrcode.h"
 
 #define TAG "qrcode"
-#define QRCODE_FOLDER ANY_PATH("qrcodes")
+#define QRCODE_FOLDER EXT_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;
@@ -86,6 +95,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
@@ -105,6 +137,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
@@ -156,8 +208,8 @@ 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,
@@ -168,7 +220,7 @@ static void render_callback(Canvas* canvas, void* ctx) {
                         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(
@@ -182,7 +234,7 @@ static void render_callback(Canvas* canvas, void* ctx) {
             }
 
             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) {
@@ -195,7 +247,7 @@ static void render_callback(Canvas* canvas, void* ctx) {
                         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,
@@ -213,10 +265,36 @@ static void render_callback(Canvas* canvas, void* ctx) {
                 }
             }
 
-            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);
@@ -308,20 +386,27 @@ 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);
+        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");
 
@@ -333,13 +418,60 @@ 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);
 
@@ -360,7 +492,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) {
@@ -368,51 +500,88 @@ static bool qrcode_load_string(QRCodeApp* instance, FuriString* str) {
             break;
         }
 
-        // figure out the qrcode "mode"
-        uint8_t mode = MODE_BYTE;
+        // figure out the minimum qrcode "mode"
+        int8_t min_mode = MODE_BYTE;
         if(is_numeric(cstr, len))
-            mode = MODE_NUMERIC;
+            min_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++;
+            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);
@@ -439,19 +608,75 @@ 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;
-        if(furi_string_cmp_str(temp_str, QRCODE_FILETYPE) || version != QRCODE_FILE_VERSION) {
+        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) || 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;
         }
 
@@ -546,82 +771,122 @@ int32_t qrcode_app(void* p) {
         }
 
         InputEvent input;
-        while(1) {
-            if(furi_message_queue_get(instance->input_queue, &input, 100) == FuriStatusOk) {
-                furi_check(furi_mutex_acquire(instance->mutex, FuriWaitForever) == FuriStatusOk);
-
-                if(input.key == InputKeyBack) {
-                    if(instance->message) {
-                        furi_string_free(instance->message);
-                        instance->message = NULL;
-                    }
-                    if(instance->qrcode) {
-                        qrcode_free(instance->qrcode);
-                        instance->qrcode = NULL;
-                    }
-                    instance->loading = true;
-                    instance->edit = false;
-                    furi_mutex_release(instance->mutex);
-                    break;
-                } else if(input.key == InputKeyRight) {
-                    instance->show_stats = true;
-                } else if(input.key == InputKeyLeft) {
-                    instance->show_stats = false;
-                } else if(instance->show_stats && !instance->loading && instance->qrcode) {
-                    if(input.key == InputKeyUp) {
-                        if(!instance->edit) {
-                            instance->selected_idx = MAX(0, instance->selected_idx - 1);
-                        } else {
-                            if(instance->selected_idx == 0 &&
-                               instance->set_version < MAX_QRCODE_VERSION) {
-                                instance->set_version++;
-                            } else if(instance->selected_idx == 1) {
-                                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++;
-                                }
+        while(furi_message_queue_get(instance->input_queue, &input, FuriWaitForever) ==
+              FuriStatusOk) {
+            furi_check(furi_mutex_acquire(instance->mutex, FuriWaitForever) == FuriStatusOk);
+
+            if(input.key == InputKeyBack) {
+                if(instance->message) {
+                    furi_string_free(instance->message);
+                    instance->message = NULL;
+                }
+                if(instance->qrcode) {
+                    qrcode_free(instance->qrcode);
+                    instance->qrcode = NULL;
+                }
+                instance->loading = true;
+                instance->edit = false;
+                furi_mutex_release(instance->mutex);
+                break;
+            } else if(input.key == InputKeyRight) {
+                instance->show_stats = true;
+            } else if(input.key == InputKeyLeft) {
+                instance->show_stats = false;
+            } else if(instance->show_stats && !instance->loading && instance->qrcode) {
+                if(input.key == InputKeyUp) {
+                    if(!instance->edit) {
+                        instance->selected_idx = MAX(0, instance->selected_idx - 1);
+                    } else {
+                        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 == 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++;
                             }
                         }
-                    } else if(input.key == InputKeyDown) {
-                        if(!instance->edit) {
-                            instance->selected_idx = MIN(1, instance->selected_idx + 1);
-                        } else {
-                            if(instance->selected_idx == 0 &&
-                               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);
-                                }
-                            } else if(instance->selected_idx == 1 && instance->set_ecc > 0) {
-                                instance->set_ecc--;
+                    }
+                } else if(input.key == InputKeyDown) {
+                    if(!instance->edit) {
+                        instance->selected_idx = MIN(2, instance->selected_idx + 1);
+                    } else {
+                        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 =
+                                    MIN(instance->set_ecc, instance->max_ecc_at_min_version);
                             }
+                        } 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)) {
-                            QRCode* qrcode = instance->qrcode;
-                            instance->loading = true;
-
-                            if(rebuild_qrcode(instance, instance->set_version, instance->set_ecc)) {
-                                qrcode_free(qrcode);
+                    }
+                } else if(input.key == InputKeyOk) {
+                    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 {
-                                FURI_LOG_E(TAG, "Could not rebuild qrcode");
-                                instance->qrcode = qrcode;
-                                instance->set_version = qrcode->version;
-                                instance->set_ecc = qrcode->ecc;
+                                instance->set_mode = instance->qrcode->mode;
                             }
+                        }
+
+                        QRCode* qrcode = instance->qrcode;
+                        instance->loading = true;
 
-                            instance->loading = false;
+                        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->edit = !instance->edit;
+
+                        instance->loading = false;
                     }
+                    instance->edit = !instance->edit;
                 }
-
-                furi_mutex_release(instance->mutex);
             }
+
+            furi_mutex_release(instance->mutex);
             view_port_update(instance->view_port);
         }