MX 2 лет назад
Родитель
Сommit
2ab4a63a0a

+ 91 - 17
non_catalog_apps/flipperzero-qrcode/README.md

@@ -23,22 +23,49 @@ Version: 0
 Message: your content here
 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
 ### Message Format
 qrcodes support 4 formats called "modes": numeric, alpha-numeric, binary, and
 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
 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.
 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
 This mode can encode numbers, uppercase letters *only*, spaces, and the
 following symbols: `$%*+-./:`. This format _may_ be appropriate for urls, as
 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
 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.
 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
 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
 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
 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
 utf8. To be standard-compliant, that basically means you can only use Latin
 letters, numbers, and symbols.
 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%
 A qrcode in binary mode can encode ~60% less data than numeric mode, and ~30%
 less than alpha-numeric.
 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
 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,
 underlying qrcode library that I'm using, unfortunately. If there's interest,
 perhaps I'll hack in support sometime.
 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
 ## Using the App
 The app is fairly straightforward. When it first starts, the file browser will
 The app is fairly straightforward. When it first starts, the file browser will
 automatically open to the `qrcodes` directory and display any `.qrcode` files.
 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.
 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
 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
 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;
 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
 ## Building
 First, clone the [flipperzero-firmware] repo and then clone this repo in the
 First, clone the [flipperzero-firmware] repo and then clone this repo in the
 `applications_user` directory:
 `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:
 Next, in the base of the [flipperzero-firmware] directory, run fbt:
 
 
 ```bash
 ```bash
-cd ..
 ./fbt fap_qrcode
 ./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
 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
 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
 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
 [now-removed demo app]: https://github.com/flipperdevices/flipperzero-firmware/pull/160/files
 [flipperzero-firmware]: https://github.com/flipperdevices/flipperzero-firmware
 [flipperzero-firmware]: https://github.com/flipperdevices/flipperzero-firmware
@@ -154,3 +227,4 @@ compiler errors.
 [QRCode]: https://github.com/ricmoo/QRCode
 [QRCode]: https://github.com/ricmoo/QRCode
 [qFlipper]: https://docs.flipperzero.one/qflipper
 [qFlipper]: https://docs.flipperzero.one/qflipper
 [Releases]: https://github.com/bmatcuk/flipperzero-qrcode/releases/latest
 [Releases]: https://github.com/bmatcuk/flipperzero-qrcode/releases/latest
+[vCard]: https://www.evenx.com/vcard-3-0-format-specification

+ 1 - 1
non_catalog_apps/flipperzero-qrcode/application.fam

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

+ 22 - 33
non_catalog_apps/flipperzero-qrcode/qrcode.c

@@ -175,24 +175,20 @@ static int8_t getAlphanumeric(char c) {
     return -1;
     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
 // We store the following tightly packed (less 8) in modeInfo
 //               <=9  <=26  <= 40
 //               <=9  <=26  <= 40
@@ -703,11 +699,9 @@ static int8_t encodeDataCodewords(
     BitBucket* dataCodewords,
     BitBucket* dataCodewords,
     const uint8_t* text,
     const uint8_t* text,
     uint16_t length,
     uint16_t length,
+    int8_t mode,
     uint8_t version) {
     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, 1 << MODE_NUMERIC, 4);
         bb_appendBits(dataCodewords, length, getModeBits(version, MODE_NUMERIC));
         bb_appendBits(dataCodewords, length, getModeBits(version, MODE_NUMERIC));
 
 
@@ -728,8 +722,7 @@ static int8_t encodeDataCodewords(
             bb_appendBits(dataCodewords, accumData, accumCount * 3 + 1);
             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, 1 << MODE_ALPHANUMERIC, 4);
         bb_appendBits(dataCodewords, length, getModeBits(version, MODE_ALPHANUMERIC));
         bb_appendBits(dataCodewords, length, getModeBits(version, MODE_ALPHANUMERIC));
 
 
@@ -853,6 +846,7 @@ uint16_t qrcode_getBufferSize(uint8_t version) {
 int8_t qrcode_initBytes(
 int8_t qrcode_initBytes(
     QRCode* qrcode,
     QRCode* qrcode,
     uint8_t* modules,
     uint8_t* modules,
+    int8_t mode,
     uint8_t version,
     uint8_t version,
     uint8_t ecc,
     uint8_t ecc,
     uint8_t* data,
     uint8_t* data,
@@ -880,7 +874,7 @@ int8_t qrcode_initBytes(
     bb_initBuffer(&codewords, codewordBytes, (int32_t)sizeof(codewordBytes));
     bb_initBuffer(&codewords, codewordBytes, (int32_t)sizeof(codewordBytes));
 
 
     // Place the data code words into the buffer
     // 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) {
     if(mode < 0) {
         return -1;
         return -1;
@@ -938,14 +932,9 @@ int8_t qrcode_initBytes(
     return 0;
     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) {
 bool qrcode_getModule(QRCode* qrcode, uint8_t x, uint8_t y) {
     if(x >= qrcode->size || y >= qrcode->size) {
     if(x >= qrcode->size || y >= qrcode->size) {

+ 2 - 6
non_catalog_apps/flipperzero-qrcode/qrcode.h

@@ -77,15 +77,11 @@ extern "C" {
 
 
 uint16_t qrcode_getBufferSize(uint8_t version);
 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(
 int8_t qrcode_initBytes(
     QRCode* qrcode,
     QRCode* qrcode,
     uint8_t* modules,
     uint8_t* modules,
+    int8_t mode,
     uint8_t version,
     uint8_t version,
     uint8_t ecc,
     uint8_t ecc,
     uint8_t* data,
     uint8_t* data,

+ 376 - 111
non_catalog_apps/flipperzero-qrcode/qrcode_app.c

@@ -11,10 +11,13 @@
 #include "qrcode.h"
 #include "qrcode.h"
 
 
 #define TAG "qrcode"
 #define TAG "qrcode"
-#define QRCODE_FOLDER ANY_PATH("qrcodes")
+#define QRCODE_FOLDER EXT_PATH("qrcodes")
 #define QRCODE_EXTENSION ".qrcode"
 #define QRCODE_EXTENSION ".qrcode"
 #define QRCODE_FILETYPE "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
  * Maximum version is 11 because the f0 screen is only 64 pixels high and
@@ -22,6 +25,9 @@
  */
  */
 #define MAX_QRCODE_VERSION 11
 #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 */
 /** Maximum length by mode, ecc, and version */
 static const uint16_t MAX_LENGTH[3][4][MAX_QRCODE_VERSION] = {
 static const uint16_t MAX_LENGTH[3][4][MAX_QRCODE_VERSION] = {
     {
     {
@@ -56,6 +62,8 @@ typedef struct {
     FuriMutex** mutex;
     FuriMutex** mutex;
     FuriString* message;
     FuriString* message;
     QRCode* qrcode;
     QRCode* qrcode;
+    uint8_t min_mode;
+    uint8_t max_mode;
     uint8_t min_version;
     uint8_t min_version;
     uint8_t max_ecc_at_min_version;
     uint8_t max_ecc_at_min_version;
     bool loading;
     bool loading;
@@ -63,6 +71,7 @@ typedef struct {
     bool show_stats;
     bool show_stats;
     uint8_t selected_idx;
     uint8_t selected_idx;
     bool edit;
     bool edit;
+    uint8_t set_mode;
     uint8_t set_version;
     uint8_t set_version;
     uint8_t set_ecc;
     uint8_t set_ecc;
 } QRCodeApp;
 } 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
  * @param mode qrcode mode
  * @returns a character corresponding to the 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
  * Render
  * @param canvas The canvas to render to
  * @param canvas The canvas to render to
@@ -156,8 +208,8 @@ static void render_callback(Canvas* canvas, void* ctx) {
             FuriString* str = furi_string_alloc();
             FuriString* str = furi_string_alloc();
 
 
             if(!instance->edit || instance->selected_idx == 0) {
             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) {
                 if(instance->selected_idx == 0) {
                     canvas_draw_triangle(
                     canvas_draw_triangle(
                         canvas,
                         canvas,
@@ -168,7 +220,7 @@ static void render_callback(Canvas* canvas, void* ctx) {
                         CanvasDirectionLeftToRight);
                         CanvasDirectionLeftToRight);
                 }
                 }
                 if(instance->edit) {
                 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_draw_triangle(
                         canvas, arrow_left, top, font_height - 4, 4, CanvasDirectionBottomToTop);
                         canvas, arrow_left, top, font_height - 4, 4, CanvasDirectionBottomToTop);
                     canvas_draw_triangle(
                     canvas_draw_triangle(
@@ -182,7 +234,7 @@ static void render_callback(Canvas* canvas, void* ctx) {
             }
             }
 
 
             if(!instance->edit || instance->selected_idx == 1) {
             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_draw_str(
                     canvas, left + 5, 2 * font_height + top + 2, furi_string_get_cstr(str));
                     canvas, left + 5, 2 * font_height + top + 2, furi_string_get_cstr(str));
                 if(instance->selected_idx == 1) {
                 if(instance->selected_idx == 1) {
@@ -195,7 +247,7 @@ static void render_callback(Canvas* canvas, void* ctx) {
                         CanvasDirectionLeftToRight);
                         CanvasDirectionLeftToRight);
                 }
                 }
                 if(instance->edit) {
                 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_draw_triangle(
                         canvas,
                         canvas,
                         arrow_left,
                         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_draw_str(
                     canvas, left + 5, 3 * font_height + top + 4, furi_string_get_cstr(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);
             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
  * sufficiently large enough to encode the full message. It is also assumed
  * that the old qrcode will be free'd by the caller.
  * that the old qrcode will be free'd by the caller.
  * @param instance The qrcode app instance
  * @param instance The qrcode app instance
+ * @param mode The qrcode mode to use
  * @param version The qrcode version to use
  * @param version The qrcode version to use
  * @param ecc The qrcode ECC level to use
  * @param ecc The qrcode ECC level to use
  * @returns true if the qrcode was successfully created
  * @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);
     furi_assert(instance->message);
     furi_assert(instance->message);
 
 
     const char* cstr = furi_string_get_cstr(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);
     instance->qrcode = qrcode_alloc(version);
 
 
     int8_t res = qrcode_initBytes(
     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) {
     if(res != 0) {
         FURI_LOG_E(TAG, "Could not create qrcode");
         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;
     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
  * Load a qrcode from a string
  * @param instance The qrcode app instance
  * @param instance The qrcode app instance
  * @param str The message to encode as a qrcode
  * @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
  * @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(instance);
     furi_assert(str);
     furi_assert(str);
 
 
@@ -360,7 +492,7 @@ static bool qrcode_load_string(QRCodeApp* instance, FuriString* str) {
     bool result = false;
     bool result = false;
     do {
     do {
         const char* cstr = furi_string_get_cstr(str);
         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);
         instance->message = furi_string_alloc_set(str);
         if(!instance->message) {
         if(!instance->message) {
@@ -368,51 +500,88 @@ static bool qrcode_load_string(QRCodeApp* instance, FuriString* str) {
             break;
             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))
         if(is_numeric(cstr, len))
-            mode = MODE_NUMERIC;
+            min_mode = MODE_NUMERIC;
         else if(is_alphanumeric(cstr, len))
         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;
             instance->too_long = true;
             break;
             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
         // 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;
             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;
         result = true;
     } while(false);
     } 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;
     instance->loading = false;
 
 
     furi_mutex_release(instance->mutex);
     furi_mutex_release(instance->mutex);
@@ -439,19 +608,75 @@ static bool qrcode_load_file(QRCodeApp* instance, const char* file_path) {
     do {
     do {
         if(!flipper_format_file_open_existing(file, file_path)) break;
         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");
             FURI_LOG_E(TAG, "Incorrect file format or version");
             break;
             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)) {
         if(!flipper_format_read_string(file, "Message", temp_str)) {
             FURI_LOG_E(TAG, "Message is missing");
             FURI_LOG_E(TAG, "Message is missing");
             break;
             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;
             break;
         }
         }
 
 
@@ -546,82 +771,122 @@ int32_t qrcode_app(void* p) {
         }
         }
 
 
         InputEvent input;
         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 {
                             } 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);
             view_port_update(instance->view_port);
         }
         }