From 72d28fa86eaddc66fb09beb60798a7e60607bfa6 Mon Sep 17 00:00:00 2001 From: tomsci Date: Thu, 15 Nov 2018 21:17:43 +0000 Subject: [PATCH] Adding qrcodegen module for generating QR Codes (#2543) * Adding qrcodegen module for generating QR Codes * Added LUA_MODULE_QRCODEGEN KConfig * Changed qrcodegen.encodeText() to use an options table Created common.h with new option table helper fns. * Reworked http.c to use new common.h options table APIs --- .gitmodules | 3 ++ components/modules/Kconfig | 7 +++ components/modules/common.c | 74 +++++++++++++++++++++++++++++++ components/modules/common.h | 37 ++++++++++++++++ components/modules/http.c | 54 +++------------------- components/modules/qrcodegen.c | 71 +++++++++++++++++++++++++++++ components/qrcodegen/component.mk | 3 ++ components/qrcodegen/qrcodegen | 1 + docs/en/modules/qrcodegen.md | 69 ++++++++++++++++++++++++++++ mkdocs.yml | 1 + 10 files changed, 272 insertions(+), 48 deletions(-) create mode 100644 components/modules/common.c create mode 100644 components/modules/common.h create mode 100644 components/modules/qrcodegen.c create mode 100644 components/qrcodegen/component.mk create mode 160000 components/qrcodegen/qrcodegen create mode 100644 docs/en/modules/qrcodegen.md diff --git a/.gitmodules b/.gitmodules index 3bb90acf..6ef2b33e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,3 +11,6 @@ [submodule "components/ucg/ucg"] path = components/ucg/ucg url = https://github.com/olikraus/Ucglib_Arduino.git +[submodule "components/qrcodegen/qrcodegen"] + path = components/qrcodegen/qrcodegen + url = https://github.com/nayuki/QR-Code-generator.git diff --git a/components/modules/Kconfig b/components/modules/Kconfig index f7759e99..26a3aeee 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -153,6 +153,13 @@ config LUA_MODULE_OW help Includes the 1-Wire (ow) module (recommended). +config LUA_MODULE_QRCODEGEN + bool "QR Code Generator module" + default "n" + help + Includes the QR Code Generator from + https://www.nayuki.io/page/qr-code-generator-library + config LUA_MODULE_SDMMC bool "SD-MMC module" default "n" diff --git a/components/modules/common.c b/components/modules/common.c new file mode 100644 index 00000000..f17de19b --- /dev/null +++ b/components/modules/common.c @@ -0,0 +1,74 @@ +#include "lauxlib.h" +#include "common.h" + +// Like luaL_argerror but for options table +int opt_error(lua_State *L, const char* name, const char *extramsg) { + lua_Debug ar; + if (!lua_getstack(L, 0, &ar)) /* no stack frame? */ + return luaL_error(L, "bad option %s (%s)", name, extramsg); + lua_getinfo(L, "n", &ar); + if (ar.name == NULL) + ar.name = "?"; + return luaL_error(L, "bad option %s to " LUA_QS " (%s)", + name, ar.name, extramsg); +} + +// Similar to luaL_argcheck() but using the options table rather than a raw index +bool opt_get(lua_State *L, const char *name, int required_type) +{ + if (!lua_istable(L, -1)) { + return false; + } + + lua_getfield(L, -1, name); + int type = lua_type(L, -1); + if (type == LUA_TNIL) { + // Option not present + lua_pop(L, 1); + return false; + } + if (type != required_type) { + const char* msg = lua_pushfstring(L, "%s expected, got %s", + lua_typename(L, required_type), + lua_typename(L, type)); + opt_error(L, name, msg); + } + return true; +} + +int opt_checkint(lua_State *L, const char *name, int default_val) +{ + if (opt_get(L, name, LUA_TNUMBER)) { + int result = lua_tointeger(L, -1); + lua_pop(L, 1); + return result; + } else { + return default_val; + } +} + +int opt_checkint_range(lua_State *L, const char *name, int default_val, int min_val, int max_val) +{ + if (opt_get(L, name, LUA_TNUMBER)) { + int result = lua_tointeger(L, -1); + lua_pop(L, 1); + if (!(result >= min_val && result <= max_val)) { + const char* msg = lua_pushfstring(L, "must be in range %d-%d", min_val, max_val); + opt_error(L, name, msg); + } + return result; + } else { + return default_val; + } +} + +bool opt_checkbool(lua_State *L, const char *name, bool default_val) +{ + if (opt_get(L, name, LUA_TBOOLEAN)) { + int result = lua_toboolean(L, -1); + lua_pop(L, 1); + return !!result; + } else { + return default_val; + } +} \ No newline at end of file diff --git a/components/modules/common.h b/components/modules/common.h new file mode 100644 index 00000000..15d24888 --- /dev/null +++ b/components/modules/common.h @@ -0,0 +1,37 @@ +#ifndef modules_common_h +#define modules_common_h + +#include "lua.h" + +/* Some common code shared between modules */ + +/* Fetch an optional int value from a table on the top of the stack. If the + option name is not present or the table is nil, returns default_val. Errors + if the key name is present in the table but is not an integer. +*/ +int opt_checkint(lua_State *L, const char *name, int default_val); + +/* Like the above, but also specifies a range which the value must be within */ +int opt_checkint_range(lua_State *L, const char *name, int default_val, int min_val, int max_val); + +/* Fetch an optional bool value from a table on the top of the stack. If the + option name is not present or the table is nil, returns default_val. Errors + if the key name is present in the table but is not a boolean. +*/ +bool opt_checkbool(lua_State *L, const char *name, bool default_val); + +/* Like luaL_argerror() but producing a more suitable error message */ +int opt_error(lua_State *L, const char* name, const char *extramsg); + +/* Returns true and pushes the value onto the stack, if name exists in the + table on the top of the stack and is of type required_type. Errors if name + is present but the wrong type. Returns false and pushes nothing if name is + not present in the table, or the table is nil. +*/ +bool opt_get(lua_State *L, const char *name, int required_type); + +/* Macro like luaL_argcheck() for defining custom additional checks */ +#define opt_check(L, cond, name, extramsg) \ + ((void)((cond) || opt_error(L, (name), (extramsg)))) + +#endif diff --git a/components/modules/http.c b/components/modules/http.c index 4b704998..e0e4b43d 100644 --- a/components/modules/http.c +++ b/components/modules/http.c @@ -1,4 +1,5 @@ #include "module.h" +#include "common.h" #include "lauxlib.h" #include "lmem.h" #include @@ -406,55 +407,12 @@ static void set_headers(lua_State *L, int headers_idx, esp_http_client_handle_t } } -// Similar to luaL_argcheck() but using the options table rather than a raw index -static bool get_option(lua_State *L, const char *name, int required_type) -{ - if (!lua_istable(L, -1)) { - return false; - } - - lua_getfield(L, -1, name); - int type = lua_type(L, -1); - if (type == LUA_TNIL) { - // Option not present - lua_pop(L, 1); - return false; - } - if (type != required_type) { - luaL_error(L, "Bad option '%s' to createConnection (%s expected, got %s)", - name, lua_typename(L, required_type), lua_typename(L, type)); - } - return true; -} - -static int check_optint(lua_State *L, const char *name, int default_val) -{ - if (get_option(L, name, LUA_TNUMBER)) { - int result = lua_tointeger(L, -1); - lua_pop(L, 1); - return result; - } else { - return default_val; - } -} - -static bool check_optbool(lua_State *L, const char *name, bool default_val) -{ - if (get_option(L, name, LUA_TBOOLEAN)) { - int result = lua_toboolean(L, -1); - lua_pop(L, 1); - return !!result; - } else { - return default_val; - } -} - // Options assumed to be on top of stack static void parse_options(lua_State *L, lhttp_context_t *context, esp_http_client_config_t *config) { - config->timeout_ms = check_optint(L, "timeout", 10*1000); // Same default as old http module - config->buffer_size = check_optint(L, "bufsz", DEFAULT_HTTP_BUF_SIZE); - int redirects = check_optint(L, "max_redirects", -1); // -1 means "not specified" here + config->timeout_ms = opt_checkint(L, "timeout", 10*1000); // Same default as old http module + config->buffer_size = opt_checkint(L, "bufsz", DEFAULT_HTTP_BUF_SIZE); + int redirects = opt_checkint(L, "max_redirects", -1); // -1 means "not specified" here if (redirects == 0) { config->disable_auto_redirect = true; } else if (redirects > 0) { @@ -463,9 +421,9 @@ static void parse_options(lua_State *L, lhttp_context_t *context, esp_http_clien // Note, config->is_async is always set to false regardless of what we set // the Async flag to, because of how we configure the tasks we always want // esp_http_client_perform to run in its 'is_async=false' mode. - context_setflagbool(context, Async, check_optbool(L, "async", false)); + context_setflagbool(context, Async, opt_checkbool(L, "async", false)); - if (get_option(L, "cert", LUA_TSTRING)) { + if (opt_get(L, "cert", LUA_TSTRING)) { const char *cert = lua_tostring(L, -1); context_setref(L, context, CertRef); config->cert_pem = cert; diff --git a/components/modules/qrcodegen.c b/components/modules/qrcodegen.c new file mode 100644 index 00000000..8e9e5d93 --- /dev/null +++ b/components/modules/qrcodegen.c @@ -0,0 +1,71 @@ +#include "module.h" +#include "lauxlib.h" +#include "common.h" +#include "lmem.h" + +#include "qrcodegen.h" + +// qrcodegen.encodeText(text, [{minver=int, maxver=int, ecl=int, mask=int, boostecl=bool}]) +// all params except text are optional +static int encodeText(lua_State *L) +{ + lua_settop(L, 2); + const char *text = luaL_checkstring(L, 1); + + int min_ver = opt_checkint_range(L, "minver", qrcodegen_VERSION_MIN, + qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX); + + int max_ver = opt_checkint_range(L, "maxver", qrcodegen_VERSION_MAX, + min_ver, qrcodegen_VERSION_MAX); + + int ecl = opt_checkint_range(L, "ecl", qrcodegen_Ecc_LOW, + qrcodegen_Ecc_LOW, qrcodegen_Ecc_HIGH); + + int mask = opt_checkint_range(L, "mask", qrcodegen_Mask_AUTO, + qrcodegen_Mask_AUTO, qrcodegen_Mask_7); + + bool boost = opt_checkbool(L, "boostecl", false); + + const size_t buf_len = qrcodegen_BUFFER_LEN_FOR_VERSION(max_ver); + uint8_t *result = (uint8_t *)luaM_malloc(L, buf_len * 2); + uint8_t *tempbuf = result + buf_len; + + bool ok = qrcodegen_encodeText(text, tempbuf, result, ecl, min_ver, max_ver, mask, boost); + if (ok) { + lua_pushlstring(L, (const char *)result, buf_len); + } else { + lua_pushnil(L); + } + luaM_freemem(L, result, buf_len * 2); + return 1; +} + +static int getSize(lua_State *L) +{ + const uint8_t* data = (const uint8_t *)luaL_checkstring(L, 1); + lua_pushinteger(L, qrcodegen_getSize(data)); + return 1; +} + +static int getPixel(lua_State *L) +{ + const uint8_t* data = (const uint8_t *)luaL_checkstring(L, 1); + int x = luaL_checkint(L, 2); + int y = luaL_checkint(L, 3); + lua_pushboolean(L, qrcodegen_getModule(data, x, y)); + return 1; +} + +static const LUA_REG_TYPE qrcodegen_map[] = { + { LSTRKEY("encodeText"), LFUNCVAL(encodeText) }, + { LSTRKEY("getSize"), LFUNCVAL(getSize) }, + { LSTRKEY("getPixel"), LFUNCVAL(getPixel) }, + { LSTRKEY("LOW"), LNUMVAL(qrcodegen_Ecc_LOW) }, + { LSTRKEY("MEDIUM"), LNUMVAL(qrcodegen_Ecc_MEDIUM) }, + { LSTRKEY("QUARTILE"), LNUMVAL(qrcodegen_Ecc_QUARTILE) }, + { LSTRKEY("HIGH"), LNUMVAL(qrcodegen_Ecc_HIGH) }, + { LSTRKEY("AUTO"), LNUMVAL(qrcodegen_Mask_AUTO) }, + { LNILKEY, LNILVAL } +}; + +NODEMCU_MODULE(QRCODEGEN, "qrcodegen", qrcodegen_map, NULL); diff --git a/components/qrcodegen/component.mk b/components/qrcodegen/component.mk new file mode 100644 index 00000000..c478fb32 --- /dev/null +++ b/components/qrcodegen/component.mk @@ -0,0 +1,3 @@ +COMPONENT_SRCDIRS:=qrcodegen/c +COMPONENT_OBJS:=qrcodegen/c/qrcodegen.o +COMPONENT_ADD_INCLUDEDIRS:=qrcodegen/c diff --git a/components/qrcodegen/qrcodegen b/components/qrcodegen/qrcodegen new file mode 160000 index 00000000..3da57e5a --- /dev/null +++ b/components/qrcodegen/qrcodegen @@ -0,0 +1 @@ +Subproject commit 3da57e5aa01ebc5c228fd9adfb1f5956c63d1648 diff --git a/docs/en/modules/qrcodegen.md b/docs/en/modules/qrcodegen.md new file mode 100644 index 00000000..7c142656 --- /dev/null +++ b/docs/en/modules/qrcodegen.md @@ -0,0 +1,69 @@ +# QR Code Generator Module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2018-10-20 | https://github.com/nayuki/QR-Code-generator.git integrated by [Tom Sutcliffe](https://github.com/tomsci) | [Tom Sutcliffe](https://github.com/tomsci) | [qrcodegen.c](../../../components/modules/qrcodegen.c)| + +This module wraps the [QR Code](https://en.wikipedia.org/wiki/QR_code) Generator API written in C by https://github.com/nayuki/QR-Code-generator.git for producing a QR Code. + +## qrcodegen.encodeText() +Generates a QR Code from a text string. In the most optimistic case, a QR Code at version 40 with low ECC can hold any UTF-8 string up to 2953 bytes, or any alphanumeric string up to 4296 characters, or any digit string up to 7089 characters. + +#### Syntax +`qrcodegen.encodeText(text, [options])` + +#### Parameters +- `text` The text or URL to encode. Should be UTF-8 or ASCII. +- `options` An optional table, containing any of: + - `minver` the minimum version according to the QR Code Model 2 standard. If not specified, defaults to `1`. + - `maxver` the maximum version according to the QR Code Model 2 standard. If not specified, defaults to `40`. Specifying a lower maximum version reduces the amount of temporary memory the function requires, so it can be worthwhile to specify a smaller value if you know the `text` will fit in a lower-version QR Code. + - `ecl` the error correction level in a QR Code symbol. Higher error correction produces a larger QR Code. One of: + - `qrcodegen.LOW` (default if not specified) + - `qrcodegen.MEDIUM` + - `qrcodegen.QUARTILE` + - `qrcodegen.HIGH` + - `mask` the mask pattern used in a QR Code symbol. An integer 0-7, or `qrcodegen.AUTO` (the default). + - `boostecl` defaults to false. + +#### Returns +The QR Code, encoded as a string. Use `qrcodegen.getSize()` and `qrcodegen.getPixel()` to extract data from the result. If the text cannot be represented within the given version range (for example it is too long) then `nil` is returned. + +#### Example +```lua +qrcode = qrcodegen.encodeText("https://nodemcu.readthedocs.io/", {minver=1, maxver=4}) +print("Size:", qrcodegen.getSize(qrcode)) +``` + +## qrcodegen.getSize() + +#### Syntax +`qrcodegen.getSize(qrcode)` + +#### Parameters +- `qrcode` a QR Code string, as returned by `qrcodegen.encodeText()`. + +#### Returns +Returns the side length in pixels of the given QR Code. The result is in the range [21, 177]. + +## qrcodegen.getPixel(qrcode, x, y) +Get the color of the pixel at the given coordinates of the QR Code. `x` and `y` must be between `0` and the value returned by `qrcodegen.getSize()`. + +#### Parameters +- `qrcode` a QR Code string, as returned by `qrcodegen.encodeText()`. +- `x` +- `y` + +#### Returns +`true` if the given pixel is black, `false` if it is white. + +#### Example +```lua +qrcode = qrcodegen.encodeText("https://nodemcu.readthedocs.io/", {maxver=4}) +size = qrcodegen.getSize(qrcode) +for y = 0, size-1 do + local row = {} + for x = 0, size-1 do + row[x + 1] = qrcodegen.getPixel(x, y) and "*" or " " + end + print(table.concat(row)) +end +``` diff --git a/mkdocs.yml b/mkdocs.yml index 7caa96a4..0795e939 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,6 +48,7 @@ pages: - 'net': 'en/modules/net.md' - 'node': 'en/modules/node.md' - 'ow (1-Wire)': 'en/modules/ow.md' + - 'qrcodegen': 'en/modules/qrcodegen.md' - 'sdmmc': 'en/modules/sdmmc.md' - 'sigma delta': 'en/modules/sigma-delta.md' - 'sodium': 'en/modules/sodium.md'