commit 9e141b8de0887c3f12d054912ba8dcf3928eb16d Author: admin Date: Tue Feb 24 18:10:40 2026 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87199a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +build/ + +# ESP-IDF local config artifacts +sdkconfig* +!sdkconfig.defaults + +# Local editor/OS/cache noise +.DS_Store +__pycache__/ +.pytest_cache/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2dc1c60 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "idf.currentSetup": "/Users/moyyang/esp/v5.5.2/esp-idf", + "idf.flashType": "UART", + "idf.port": "/dev/tty.wchusbserial5B0B0251771" +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..159b9dc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,53 @@ +# Repository Guidelines + +## Project Structure & Layering +- `main/platform/{include,internal,src}`: hardware/system abstraction only. +- `main/domain/{include,internal,src}`: business-domain services. +- `main/control_plane/{include,internal,src}`: lifecycle, policy, health, retry/backoff orchestration. +- `main/app_composition/{include,internal,src}`: composition boundary; `app_main` only wires modules. +- `main/dependency_whitelist.md`: approved exceptions for unavoidable lateral dependencies. +- `main/third_party/`, `main/domain/assets/fonts/`, `tools/`: vendored code, embedded assets, utility scripts. +- `build/`: generated artifacts only (ignored). + +## Mandatory Architecture Constraints +1. Dependencies are one-way only: `app_composition -> control_plane -> domain -> platform`. +2. Lateral direct dependencies are forbidden unless listed in `main/dependency_whitelist.md`. +3. Cross-domain communication must use events or Port interfaces; never include another domain's `internal/*.h`. +4. Each component keeps one public header in `include/`; private headers stay in `internal/` via `PRIV_INCLUDE_DIRS`. +5. Runtime state must have a single source of truth (SSOT); other modules subscribe/query instead of copying fields. +6. Blocking APIs must define timeout and idempotent semantics (no destructive background continuation after caller timeout). + +## Build, Flash, and Validation +Activate ESP-IDF v5.5.2 before running any `idf.py` command: +```bash +export PATH=/opt/homebrew/bin:$PATH +export IDF_PATH=/Users/moyyang/esp/v5.5.2/esp-idf +source $IDF_PATH/export.sh +``` +- `idf.py set-target esp32s3`: one-time target setup. +- `idf.py build`: compile and validate. +- `idf.py -p flash monitor`: flash and open serial monitor (`Ctrl+]` to exit). +- `idf.py menuconfig`: adjust project options. +- `idf.py fullclean && idf.py build`: clear stale artifacts. + +## Coding Style & Naming Conventions +- Use 4-space indentation and keep braces/function style consistent with existing `main/*.c`. +- Prefer `lower_snake_case` for functions and variables; use module-prefixed public APIs. +- Use `UPPER_SNAKE_CASE` for macros/constants (`CMD_SEND_DATA`, `WIFI_CONNECTED_BIT`). +- Prefix file-local statics with `s_` (for example `s_server`, `s_jobs`) and keep per-file `TAG` logging constants. + +## Testing Guidelines +There is no dedicated unit-test directory currently. Minimum validation: +- Build check: `idf.py build`. +- Device smoke test after flash: `/v1/health`, printer connect/disconnect, and one print flow (text or image) via `curl`. +- For Control Plane or REST changes, include one request/response or lifecycle scenario in PR notes. + +## Commit & Pull Request Guidelines +Use a clear conventional format: +- Commit style: `feat(rest): add label offset endpoint`, `fix(wifi): handle STA timeout`. +- Keep commits focused and atomic by layer/module (`platform`, `domain`, `control_plane`, `app_composition`). +- PRs include purpose, affected layer(s), whitelist changes (if any), config changes, and validation evidence. + +## Security & Configuration Tips +- Never hardcode credentials or API keys in source files; configure through `menuconfig`. +- Do not commit local `sdkconfig` variants, serial-port-specific commands, or generated `build/` artifacts. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..40bb264 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(lyf_printer_controller) diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa8d7d7 --- /dev/null +++ b/README.md @@ -0,0 +1,277 @@ +# ESP32-S3 LYF Printer Controller (Wi-Fi REST API) + +ESP32-S3 works as a Wi-Fi REST controller and replaces Android App logic: +- REST client -> ESP32-S3 (`esp_http_server`) +- ESP32-S3 -> BLE printer (`lyfPrinter`, `FFF2` write / `FFF1` notify) +- Command compatibility: + - Print path: `0x00~0x07` + - OTA path: `0xA0~0xA4` + +## Features + +- Wi-Fi STA-only mode (configured SSID/password; no SoftAP fallback) +- Built-in web UI at `/` for connect/status, text print, and image upload print +- BLE central client auto-scan/connect to printer name +- Async print queue with jobs (`queued/running/success/failed/canceled`) +- Printer precheck (paper / battery / temperature) +- Built-in UTF-8 text rendering with Chinese support (GB2312 character set, 16x16 bitmap) +- Android-compatible print flow: + - `0x00` power on + - `0x03` set print param + - `0x04` chunked raster send + ACK + - `0x00` power off + - `0x02` feed paper +- REST APIs: + - Health/connection: + - `GET /v1/health` + - `POST /v1/printer/connect` + - `POST /v1/printer/disconnect` + - `GET /v1/printer/status` + - Print: + - `POST /v1/print/raster` + - `POST /v1/print/image` (`base64_msb_1bpp` or `base64_gray8`) + - `POST /v1/print/qr` + - `POST /v1/print/text` + - `POST /v1/print/receipt` + - `POST /v1/print/label` + - Jobs: + - `GET /v1/jobs` + - `DELETE /v1/jobs` + - `GET /v1/jobs/{id}` + - `DELETE /v1/jobs/{id}` + - Label control: + - `POST /v1/label/gap_move` + - `GET /v1/label/offset` + - `POST /v1/label/offset` + - OTA: + - `GET /v1/ota/version` + - `POST /v1/ota/jump_boot` + - `POST /v1/ota/jump_app` + - `POST /v1/ota/erase_page` + - `POST /v1/ota/write_frame` + - `POST /v1/ota/upgrade` + +## Build + +Prerequisites: +- ESP-IDF v5.x +- ESP32-S3 board + +```bash +cd ai_printer +idf.py set-target esp32s3 +idf.py menuconfig +idf.py build +``` + +## Config + +In `menuconfig -> LYF Controller Config`: +- `LYF_WIFI_SSID` +- `LYF_WIFI_PASSWORD` +- `LYF_HTTP_PORT` +- `LYF_API_KEY` (optional) + +When `LYF_API_KEY` is not empty, each request must include: + +```text +X-API-Key: +``` + +STA-only behavior: +- If SSID is empty, startup fails. +- If STA connect fails, startup fails (no AP fallback). + +## Flash + +```bash +cd ai_printer +idf.py -p /dev/tty.usbmodemXXXX flash monitor +``` + +## Web UI + +Open the controller IP in a browser: + +```text +http:/// +``` + +The page provides: +- health/status check +- connect/disconnect printer +- submit a simple text print job +- upload an image (gray8 payload) and let ESP32-S3 handle threshold/scale/raster conversion +- view jobs list + +If API key is enabled, input it in the page before invoking actions. + +## REST Examples + +### Health +```bash +curl http:///v1/health +``` + +### Connect printer +```bash +curl -X POST http:///v1/printer/connect \ + -H 'Content-Type: application/json' \ + -d '{"name":"lyfPrinter","timeout_ms":15000}' +``` + +Auto-match a compatible printer (by BLE service `0xFFF0`): + +```bash +curl -X POST http:///v1/printer/connect \ + -H 'Content-Type: application/json' \ + -d '{"name":"*","timeout_ms":20000}' +``` + +### Raster print +```bash +curl -X POST http:///v1/print/raster \ + -H 'Content-Type: application/json' \ + -d '{ + "width":384, + "height":200, + "density":"中等", + "encoding":"base64_msb_1bpp", + "data":"" + }' +``` + +### Image print (gray8 auto-scale to 384) +```bash +curl -X POST http:///v1/print/image \ + -H 'Content-Type: application/json' \ + -d '{ + "width":800, + "height":600, + "encoding":"base64_gray8", + "data":"", + "scale_to_width":true, + "threshold":160, + "density":"中等" + }' +``` + +### QR print +```bash +curl -X POST http:///v1/print/qr \ + -H 'Content-Type: application/json' \ + -d '{ + "text":"https://example.com/pay/123", + "ecc":"H", + "module_scale":0, + "margin_modules":2, + "density":"中等" + }' +``` + +### Text print +```bash +curl -X POST http:///v1/print/text \ + -H 'Content-Type: application/json' \ + -d '{ + "text":"欢迎使用LYF打印机\\n订单号: A1024\\n谢谢惠顾", + "density":"中等", + "scale":2, + "line_spacing":2, + "max_height":1800 + }' +``` + +### Receipt print +```bash +curl -X POST http:///v1/print/receipt \ + -H 'Content-Type: application/json' \ + -d '{ + "title":"LYF FOOD", + "density":"中等", + "footer":"Thanks!", + "items":[ + {"name":"DishA","qty":2,"price":12.5}, + {"name":"DishB","qty":1,"price":8.0} + ] + }' +``` + +### Label print (with optional gap/offset) +```bash +curl -X POST http:///v1/print/label \ + -H 'Content-Type: application/json' \ + -d '{ + "width":384, + "height":260, + "encoding":"base64_msb_1bpp", + "data":"", + "gap_move_before":true, + "offset_tenths_mm":128, + "density":"中等" + }' +``` + +### Jobs +```bash +curl http:///v1/jobs +curl http:///v1/jobs/1 +curl -X DELETE http:///v1/jobs/1 +curl -X DELETE http:///v1/jobs -d '{"include_success":true,"include_failed":true,"include_canceled":true}' +``` + +### Label control +```bash +curl -X POST http:///v1/label/gap_move +curl http:///v1/label/offset +curl -X POST http:///v1/label/offset \ + -H 'Content-Type: application/json' \ + -d '{"offset_tenths_mm":128}' +``` + +### OTA primitives +```bash +curl http:///v1/ota/version +curl -X POST http:///v1/ota/jump_boot +curl -X POST http:///v1/ota/erase_page -H 'Content-Type: application/json' -d '{"page_num":0}' +curl -X POST http:///v1/ota/write_frame -H 'Content-Type: application/json' -d '{"packet_num":0,"is_last_frame":false,"data":""}' +curl -X POST http:///v1/ota/jump_app +``` + +### OTA full upgrade +```bash +curl -X POST http:///v1/ota/upgrade \ + -H 'Content-Type: application/json' \ + -d '{ + "firmware":"", + "jump_boot":true, + "jump_app":true, + "page_size":1024, + "packet_size":236, + "timeout_ms_per_step":5000 + }' +``` + +## Notes + +- BLE side is central/client role, not printer peripheral role. +- `base64_msb_1bpp` uses Android-compatible bit order (MSB first). +- `/v1/print/text` and `/v1/print/receipt` now support UTF-8 Chinese via embedded 16x16 GB2312 glyphs. +- Characters outside embedded glyph set are rendered as square fallback boxes. +- QR encoding uses embedded `qrcodegen` (Project Nayuki C implementation). + +## Font Assets + +Embedded files: +- `main/domain/assets/fonts/cn16_index.bin` +- `main/domain/assets/fonts/cn16_glyphs.bin` + +Regenerate from your own CJK font: + +```bash +cd ai_printer +python3 -m pip install --user pillow +python3 tools/gen_cn16_font.py \ + --font app/src/main/assets/fonts/msyh.ttc \ + --font-index 0 +``` diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..bb43506 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,42 @@ +idf_component_register( + SRCS + "app_composition/src/app_main.c" + "control_plane/src/controller_lifecycle.c" + "control_plane/src/rest_server.c" + "control_plane/src/rest_server_common.c" + "control_plane/src/rest_server_ops.c" + "control_plane/src/rest_server_print.c" + "control_plane/src/rest_server_jobs.c" + "domain/src/printer_protocol.c" + "domain/src/printer_protocol_commands.c" + "domain/src/raster_tools.c" + "domain/src/raster_tools_image_qr.c" + "domain/src/system_runtime.c" + "platform/src/platform_bootstrap.c" + "platform/src/wifi_manager.c" + "platform/src/ble_printer_client.c" + "third_party/qrcodegen.c" + INCLUDE_DIRS + "app_composition/include" + "control_plane/include" + "domain/include" + "platform/include" + "third_party" + PRIV_INCLUDE_DIRS + "app_composition/internal" + "control_plane/internal" + "domain/internal" + "platform/internal" + REQUIRES + bt + esp_wifi + esp_netif + esp_event + esp_http_server + nvs_flash + json + mbedtls + EMBED_FILES + "domain/assets/fonts/cn16_index.bin" + "domain/assets/fonts/cn16_glyphs.bin" +) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..ed55dea --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,23 @@ +menu "LYF Controller Config" + +config LYF_WIFI_SSID + string "Wi-Fi SSID" + default "" + +config LYF_WIFI_PASSWORD + string "Wi-Fi Password" + default "" + +config LYF_WIFI_MAXIMUM_RETRY + int "Wi-Fi Maximum Retry" + default 10 + +config LYF_HTTP_PORT + int "REST HTTP Port" + default 80 + +config LYF_API_KEY + string "REST API Key (optional, empty means disabled)" + default "" + +endmenu diff --git a/main/app_composition/README.md b/main/app_composition/README.md new file mode 100644 index 0000000..13f019e --- /dev/null +++ b/main/app_composition/README.md @@ -0,0 +1,8 @@ +# App Composition Layer + +Purpose: top-level assembly only. + +Rules: +- `app_main` wires components, startup order, and shutdown hooks. +- No feature/business logic in composition code. +- No direct hardware operations outside platform abstractions. diff --git a/main/app_composition/include/.gitkeep b/main/app_composition/include/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/app_composition/internal/.gitkeep b/main/app_composition/internal/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/app_composition/src/.gitkeep b/main/app_composition/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/app_composition/src/app_main.c b/main/app_composition/src/app_main.c new file mode 100644 index 0000000..d5c7feb --- /dev/null +++ b/main/app_composition/src/app_main.c @@ -0,0 +1,12 @@ +#include "controller_lifecycle.h" + +#include "esp_err.h" +#include "esp_log.h" + +static const char *TAG = "app_main"; + +void app_main(void) { + ESP_ERROR_CHECK(controller_lifecycle_start()); + + ESP_LOGI(TAG, "LYF controller started"); +} diff --git a/main/control_plane/README.md b/main/control_plane/README.md new file mode 100644 index 0000000..00acd56 --- /dev/null +++ b/main/control_plane/README.md @@ -0,0 +1,8 @@ +# Control Plane Layer + +Purpose: lifecycle, policy, health checks, retry/backoff, and orchestration decisions. + +Rules: +- Contains "decision + orchestration" only. +- Calls domain APIs; does not absorb hardware details. +- Maintains dependency whitelist decisions for exceptional lateral coupling. diff --git a/main/control_plane/include/.gitkeep b/main/control_plane/include/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/control_plane/include/controller_lifecycle.h b/main/control_plane/include/controller_lifecycle.h new file mode 100644 index 0000000..64061da --- /dev/null +++ b/main/control_plane/include/controller_lifecycle.h @@ -0,0 +1,6 @@ +#pragma once + +#include "esp_err.h" + +esp_err_t controller_lifecycle_start(void); +void controller_lifecycle_stop(void); diff --git a/main/control_plane/include/rest_server.h b/main/control_plane/include/rest_server.h new file mode 100644 index 0000000..0e82bb1 --- /dev/null +++ b/main/control_plane/include/rest_server.h @@ -0,0 +1,6 @@ +#pragma once + +#include "esp_err.h" + +esp_err_t rest_server_start(void); +void rest_server_stop(void); diff --git a/main/control_plane/internal/.gitkeep b/main/control_plane/internal/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/control_plane/internal/rest_server_internal.h b/main/control_plane/internal/rest_server_internal.h new file mode 100644 index 0000000..2051b2b --- /dev/null +++ b/main/control_plane/internal/rest_server_internal.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include "cJSON.h" +#include "esp_err.h" +#include "esp_http_server.h" + +bool rest_server_auth_ok(httpd_req_t *req); +esp_err_t rest_server_send_json(httpd_req_t *req, const char *status, cJSON *root); +esp_err_t rest_server_send_error(httpd_req_t *req, const char *status, const char *message); +esp_err_t rest_server_read_body(httpd_req_t *req, char **out_body); +bool rest_server_parse_uri_u32_tail(const char *uri, uint32_t *out_value); +esp_err_t rest_server_base64_decode_alloc(const char *b64, uint8_t **out_raw, size_t *out_len); +void rest_server_appendf(char *dst, size_t cap, size_t *offset, const char *fmt, ...); +bool rest_server_json_bool_with_default(cJSON *item, bool default_value); + +esp_err_t rest_server_health_get(httpd_req_t *req); +esp_err_t rest_server_connect_post(httpd_req_t *req); +esp_err_t rest_server_disconnect_post(httpd_req_t *req); +esp_err_t rest_server_status_get(httpd_req_t *req); + +esp_err_t rest_server_print_raster_post(httpd_req_t *req); +esp_err_t rest_server_print_image_post(httpd_req_t *req); +esp_err_t rest_server_print_qr_post(httpd_req_t *req); +esp_err_t rest_server_print_label_post(httpd_req_t *req); +esp_err_t rest_server_print_text_post(httpd_req_t *req); +esp_err_t rest_server_print_receipt_post(httpd_req_t *req); + +esp_err_t rest_server_job_get(httpd_req_t *req); +esp_err_t rest_server_jobs_get(httpd_req_t *req); +esp_err_t rest_server_job_delete(httpd_req_t *req); +esp_err_t rest_server_jobs_delete(httpd_req_t *req); + +esp_err_t rest_server_label_gap_move_post(httpd_req_t *req); +esp_err_t rest_server_label_offset_get(httpd_req_t *req); +esp_err_t rest_server_label_offset_post(httpd_req_t *req); + +esp_err_t rest_server_ota_version_get(httpd_req_t *req); +esp_err_t rest_server_ota_jump_boot_post(httpd_req_t *req); +esp_err_t rest_server_ota_jump_app_post(httpd_req_t *req); +esp_err_t rest_server_ota_erase_page_post(httpd_req_t *req); +esp_err_t rest_server_ota_write_frame_post(httpd_req_t *req); +esp_err_t rest_server_ota_upgrade_post(httpd_req_t *req); diff --git a/main/control_plane/src/.gitkeep b/main/control_plane/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/control_plane/src/controller_lifecycle.c b/main/control_plane/src/controller_lifecycle.c new file mode 100644 index 0000000..c9106f7 --- /dev/null +++ b/main/control_plane/src/controller_lifecycle.c @@ -0,0 +1,27 @@ +#include "controller_lifecycle.h" + +#include "esp_log.h" +#include "printer_protocol.h" +#include "rest_server.h" +#include "system_runtime.h" + +static const char *TAG = "controller_lifecycle"; + +esp_err_t controller_lifecycle_start(void) { + ESP_LOGI(TAG, "Starting system runtime"); + ESP_ERROR_CHECK(system_runtime_bootstrap()); + + ESP_LOGI(TAG, "Starting printer protocol"); + ESP_ERROR_CHECK(printer_protocol_init()); + + ESP_LOGI(TAG, "Starting REST server"); + ESP_ERROR_CHECK(rest_server_start()); + + return ESP_OK; +} + +void controller_lifecycle_stop(void) { + rest_server_stop(); + printer_protocol_disconnect(); + ESP_LOGI(TAG, "Controller lifecycle stopped"); +} diff --git a/main/control_plane/src/rest_server.c b/main/control_plane/src/rest_server.c new file mode 100644 index 0000000..bdd92f0 --- /dev/null +++ b/main/control_plane/src/rest_server.c @@ -0,0 +1,487 @@ +#include "rest_server.h" + +#include + +#include "rest_server_internal.h" +#include "esp_http_server.h" +#include "esp_log.h" + +static const char *TAG = "rest_server"; + +static httpd_handle_t s_server; + +static const char *s_web_index = + "\n" + "\n" + "\n" + " \n" + " \n" + " LYF Printer Controller\n" + " \n" + "\n" + "\n" + "
\n" + "

LYF Printer Controller

\n" + "

ESP32-S3 local control page. This page calls the onboard REST API directly.

\n" + "\n" + " \n" + " \n" + "\n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + "\n" + " \n" + " \n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + "\n" + " \n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + " \n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + "
Image bytes are uploaded as gray8; ESP32-S3 handles threshold/scale/raster processing.
\n" + "\n" + "
Tip: connect printer first, then submit print jobs.
\n" + "
Ready.
\n" + "
\n" + "\n" + " \n" + "\n" + "\n"; + +static esp_err_t index_get(httpd_req_t *req) { + httpd_resp_set_type(req, "text/html; charset=utf-8"); + httpd_resp_set_hdr(req, "Cache-Control", "no-store"); + return httpd_resp_send(req, s_web_index, HTTPD_RESP_USE_STRLEN); +} + +static esp_err_t favicon_get(httpd_req_t *req) { + httpd_resp_set_status(req, "204 No Content"); + return httpd_resp_send(req, NULL, 0); +} + +esp_err_t rest_server_start(void) { + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = CONFIG_LYF_HTTP_PORT; + config.uri_match_fn = httpd_uri_match_wildcard; + config.max_uri_handlers = 32; + + esp_err_t err = httpd_start(&s_server, &config); + if (err != ESP_OK) { + ESP_LOGE(TAG, "httpd_start failed: %s", esp_err_to_name(err)); + return err; + } + + httpd_uri_t index = { + .uri = "/", + .method = HTTP_GET, + .handler = index_get, + }; + httpd_register_uri_handler(s_server, &index); + + httpd_uri_t favicon = { + .uri = "/favicon.ico", + .method = HTTP_GET, + .handler = favicon_get, + }; + httpd_register_uri_handler(s_server, &favicon); + + httpd_uri_t health = { + .uri = "/v1/health", + .method = HTTP_GET, + .handler = rest_server_health_get, + }; + httpd_register_uri_handler(s_server, &health); + + httpd_uri_t connect = { + .uri = "/v1/printer/connect", + .method = HTTP_POST, + .handler = rest_server_connect_post, + }; + httpd_register_uri_handler(s_server, &connect); + + httpd_uri_t disconnect = { + .uri = "/v1/printer/disconnect", + .method = HTTP_POST, + .handler = rest_server_disconnect_post, + }; + httpd_register_uri_handler(s_server, &disconnect); + + httpd_uri_t status = { + .uri = "/v1/printer/status", + .method = HTTP_GET, + .handler = rest_server_status_get, + }; + httpd_register_uri_handler(s_server, &status); + + httpd_uri_t print_raster = { + .uri = "/v1/print/raster", + .method = HTTP_POST, + .handler = rest_server_print_raster_post, + }; + httpd_register_uri_handler(s_server, &print_raster); + + httpd_uri_t print_image = { + .uri = "/v1/print/image", + .method = HTTP_POST, + .handler = rest_server_print_image_post, + }; + httpd_register_uri_handler(s_server, &print_image); + + httpd_uri_t print_qr = { + .uri = "/v1/print/qr", + .method = HTTP_POST, + .handler = rest_server_print_qr_post, + }; + httpd_register_uri_handler(s_server, &print_qr); + + httpd_uri_t print_text = { + .uri = "/v1/print/text", + .method = HTTP_POST, + .handler = rest_server_print_text_post, + }; + httpd_register_uri_handler(s_server, &print_text); + + httpd_uri_t print_label = { + .uri = "/v1/print/label", + .method = HTTP_POST, + .handler = rest_server_print_label_post, + }; + httpd_register_uri_handler(s_server, &print_label); + + httpd_uri_t print_receipt = { + .uri = "/v1/print/receipt", + .method = HTTP_POST, + .handler = rest_server_print_receipt_post, + }; + httpd_register_uri_handler(s_server, &print_receipt); + + httpd_uri_t jobs = { + .uri = "/v1/jobs", + .method = HTTP_GET, + .handler = rest_server_jobs_get, + }; + httpd_register_uri_handler(s_server, &jobs); + + httpd_uri_t jobs_delete_uri = { + .uri = "/v1/jobs", + .method = HTTP_DELETE, + .handler = rest_server_jobs_delete, + }; + httpd_register_uri_handler(s_server, &jobs_delete_uri); + + httpd_uri_t job = { + .uri = "/v1/jobs/*", + .method = HTTP_GET, + .handler = rest_server_job_get, + }; + httpd_register_uri_handler(s_server, &job); + + httpd_uri_t job_delete_uri = { + .uri = "/v1/jobs/*", + .method = HTTP_DELETE, + .handler = rest_server_job_delete, + }; + httpd_register_uri_handler(s_server, &job_delete_uri); + + httpd_uri_t label_gap = { + .uri = "/v1/label/gap_move", + .method = HTTP_POST, + .handler = rest_server_label_gap_move_post, + }; + httpd_register_uri_handler(s_server, &label_gap); + + httpd_uri_t label_offset_get_uri = { + .uri = "/v1/label/offset", + .method = HTTP_GET, + .handler = rest_server_label_offset_get, + }; + httpd_register_uri_handler(s_server, &label_offset_get_uri); + + httpd_uri_t label_offset_post_uri = { + .uri = "/v1/label/offset", + .method = HTTP_POST, + .handler = rest_server_label_offset_post, + }; + httpd_register_uri_handler(s_server, &label_offset_post_uri); + + httpd_uri_t ota_version = { + .uri = "/v1/ota/version", + .method = HTTP_GET, + .handler = rest_server_ota_version_get, + }; + httpd_register_uri_handler(s_server, &ota_version); + + httpd_uri_t ota_jump_boot = { + .uri = "/v1/ota/jump_boot", + .method = HTTP_POST, + .handler = rest_server_ota_jump_boot_post, + }; + httpd_register_uri_handler(s_server, &ota_jump_boot); + + httpd_uri_t ota_jump_app = { + .uri = "/v1/ota/jump_app", + .method = HTTP_POST, + .handler = rest_server_ota_jump_app_post, + }; + httpd_register_uri_handler(s_server, &ota_jump_app); + + httpd_uri_t ota_erase_page = { + .uri = "/v1/ota/erase_page", + .method = HTTP_POST, + .handler = rest_server_ota_erase_page_post, + }; + httpd_register_uri_handler(s_server, &ota_erase_page); + + httpd_uri_t ota_write_frame = { + .uri = "/v1/ota/write_frame", + .method = HTTP_POST, + .handler = rest_server_ota_write_frame_post, + }; + httpd_register_uri_handler(s_server, &ota_write_frame); + + httpd_uri_t ota_upgrade = { + .uri = "/v1/ota/upgrade", + .method = HTTP_POST, + .handler = rest_server_ota_upgrade_post, + }; + httpd_register_uri_handler(s_server, &ota_upgrade); + + ESP_LOGI(TAG, "REST server started on port %d", CONFIG_LYF_HTTP_PORT); + return ESP_OK; +} + +void rest_server_stop(void) { + if (s_server != NULL) { + httpd_stop(s_server); + s_server = NULL; + } +} diff --git a/main/control_plane/src/rest_server_common.c b/main/control_plane/src/rest_server_common.c new file mode 100644 index 0000000..cbc34ba --- /dev/null +++ b/main/control_plane/src/rest_server_common.c @@ -0,0 +1,164 @@ +#include "rest_server_internal.h" + +#include +#include +#include +#include +#include + +#include "mbedtls/base64.h" +bool rest_server_auth_ok(httpd_req_t *req) { + if (strlen(CONFIG_LYF_API_KEY) == 0) { + return true; + } + + char buf[96] = {0}; + if (httpd_req_get_hdr_value_str(req, "X-API-Key", buf, sizeof(buf)) != ESP_OK) { + return false; + } + + return strcmp(buf, CONFIG_LYF_API_KEY) == 0; +} + +esp_err_t rest_server_send_json(httpd_req_t *req, const char *status, cJSON *root) { + char *text = cJSON_PrintUnformatted(root); + if (text == NULL) { + return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "json encode failed"); + } + + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_status(req, status); + esp_err_t err = httpd_resp_sendstr(req, text); + cJSON_free(text); + return err; +} + +esp_err_t rest_server_send_error(httpd_req_t *req, const char *status, const char *message) { + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", false); + cJSON_AddStringToObject(root, "error", message != NULL ? message : "unknown"); + esp_err_t err = rest_server_send_json(req, status, root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_read_body(httpd_req_t *req, char **out_body) { + if (out_body == NULL) { + return ESP_ERR_INVALID_ARG; + } + + *out_body = NULL; + + if (req->content_len <= 0 || req->content_len > (1024 * 1024)) { + return ESP_ERR_INVALID_SIZE; + } + + char *buf = (char *)calloc(1, req->content_len + 1); + if (buf == NULL) { + return ESP_ERR_NO_MEM; + } + + int received = 0; + while (received < req->content_len) { + int ret = httpd_req_recv(req, buf + received, req->content_len - received); + if (ret <= 0) { + free(buf); + return ESP_FAIL; + } + received += ret; + } + + *out_body = buf; + return ESP_OK; +} + +bool rest_server_parse_uri_u32_tail(const char *uri, uint32_t *out_value) { + if (uri == NULL || out_value == NULL) { + return false; + } + + const char *slash = strrchr(uri, '/'); + if (slash == NULL || *(slash + 1) == '\0') { + return false; + } + + errno = 0; + char *endptr = NULL; + unsigned long val = strtoul(slash + 1, &endptr, 10); + if (errno != 0 || endptr == slash + 1 || *endptr != '\0' || val > UINT32_MAX) { + return false; + } + + *out_value = (uint32_t)val; + return true; +} + +esp_err_t rest_server_base64_decode_alloc(const char *b64, uint8_t **out_raw, size_t *out_len) { + if (b64 == NULL || out_raw == NULL || out_len == NULL) { + return ESP_ERR_INVALID_ARG; + } + + *out_raw = NULL; + *out_len = 0; + + size_t decoded_len = 0; + int rc = mbedtls_base64_decode(NULL, + 0, + &decoded_len, + (const unsigned char *)b64, + strlen(b64)); + if (!(rc == 0 || rc == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL)) { + return ESP_ERR_INVALID_ARG; + } + + uint8_t *buf = (uint8_t *)malloc(decoded_len); + if (buf == NULL) { + return ESP_ERR_NO_MEM; + } + + rc = mbedtls_base64_decode(buf, + decoded_len, + &decoded_len, + (const unsigned char *)b64, + strlen(b64)); + if (rc != 0) { + free(buf); + return ESP_ERR_INVALID_ARG; + } + + *out_raw = buf; + *out_len = decoded_len; + return ESP_OK; +} + +void rest_server_appendf(char *dst, size_t cap, size_t *offset, const char *fmt, ...) { + if (dst == NULL || cap == 0 || offset == NULL || fmt == NULL || *offset >= cap) { + return; + } + + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(dst + *offset, cap - *offset, fmt, ap); + va_end(ap); + + if (n <= 0) { + return; + } + + size_t written = (size_t)n; + if (written >= cap - *offset) { + *offset = cap - 1; + return; + } + *offset += written; +} + +bool rest_server_json_bool_with_default(cJSON *item, bool default_value) { + if (item == NULL) { + return default_value; + } + if (cJSON_IsBool(item)) { + return cJSON_IsTrue(item); + } + return default_value; +} diff --git a/main/control_plane/src/rest_server_jobs.c b/main/control_plane/src/rest_server_jobs.c new file mode 100644 index 0000000..8b3f41b --- /dev/null +++ b/main/control_plane/src/rest_server_jobs.c @@ -0,0 +1,173 @@ +#include "rest_server_internal.h" + +#include + +#include "printer_protocol.h" +esp_err_t rest_server_job_get(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + uint32_t job_id = 0; + if (!rest_server_parse_uri_u32_tail(req->uri, &job_id)) { + return rest_server_send_error(req, "400 Bad Request", "invalid job id"); + } + + print_job_info_t info; + if (!printer_protocol_get_job(job_id, &info)) { + return rest_server_send_error(req, "404 Not Found", "job not found"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "job_id", info.id); + cJSON_AddStringToObject(root, "state", printer_protocol_job_state_str(info.state)); + cJSON_AddNumberToObject(root, "progress", info.progress); + cJSON_AddStringToObject(root, "density", info.density); + cJSON_AddNumberToObject(root, "width", info.width); + cJSON_AddNumberToObject(root, "height", info.height); + cJSON_AddNumberToObject(root, "data_len", (double)info.data_len); + cJSON_AddStringToObject(root, "error", info.error); + cJSON_AddNumberToObject(root, "created_ms", (double)info.created_ms); + cJSON_AddNumberToObject(root, "started_ms", (double)info.started_ms); + cJSON_AddNumberToObject(root, "finished_ms", (double)info.finished_ms); + + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_jobs_get(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + print_job_info_t jobs[16]; + size_t total_count = 0; + esp_err_t list_rc = printer_protocol_list_jobs(jobs, 16, &total_count); + if (list_rc != ESP_OK) { + return rest_server_send_error(req, "500 Internal Server Error", "list jobs failed"); + } + + size_t emit_count = total_count; + if (emit_count > 16) { + emit_count = 16; + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "total", (double)total_count); + cJSON_AddNumberToObject(root, "returned", (double)emit_count); + + cJSON *arr = cJSON_AddArrayToObject(root, "jobs"); + for (size_t i = 0; i < emit_count; ++i) { + cJSON *item = cJSON_CreateObject(); + cJSON_AddNumberToObject(item, "job_id", jobs[i].id); + cJSON_AddStringToObject(item, "state", printer_protocol_job_state_str(jobs[i].state)); + cJSON_AddNumberToObject(item, "progress", jobs[i].progress); + cJSON_AddStringToObject(item, "density", jobs[i].density); + cJSON_AddNumberToObject(item, "width", jobs[i].width); + cJSON_AddNumberToObject(item, "height", jobs[i].height); + cJSON_AddNumberToObject(item, "data_len", (double)jobs[i].data_len); + cJSON_AddStringToObject(item, "error", jobs[i].error); + cJSON_AddNumberToObject(item, "created_ms", (double)jobs[i].created_ms); + cJSON_AddNumberToObject(item, "started_ms", (double)jobs[i].started_ms); + cJSON_AddNumberToObject(item, "finished_ms", (double)jobs[i].finished_ms); + cJSON_AddItemToArray(arr, item); + } + + if (total_count > emit_count) { + cJSON_AddStringToObject(root, "warning", "job list truncated"); + } + + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_job_delete(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + uint32_t job_id = 0; + if (!rest_server_parse_uri_u32_tail(req->uri, &job_id)) { + return rest_server_send_error(req, "400 Bad Request", "invalid job id"); + } + + char cancel_err[128] = {0}; + esp_err_t cancel_rc = printer_protocol_cancel_job(job_id, cancel_err, sizeof(cancel_err)); + if (cancel_rc == ESP_ERR_NOT_FOUND) { + return rest_server_send_error(req, "404 Not Found", "job not found"); + } + if (cancel_rc == ESP_ERR_INVALID_STATE) { + return rest_server_send_error(req, + "409 Conflict", + cancel_err[0] != '\0' ? cancel_err : "job already finished"); + } + if (cancel_rc != ESP_OK) { + return rest_server_send_error(req, + "409 Conflict", + cancel_err[0] != '\0' ? cancel_err : "cancel failed"); + } + + print_job_info_t info; + bool ok = printer_protocol_get_job(job_id, &info); + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "job_id", job_id); + if (ok) { + cJSON_AddStringToObject(root, "state", printer_protocol_job_state_str(info.state)); + cJSON_AddStringToObject(root, "error", info.error); + } else { + cJSON_AddStringToObject(root, "state", "canceled"); + } + + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_jobs_delete(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + bool include_success = true; + bool include_failed = true; + bool include_canceled = true; + + if (req->content_len > 0) { + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + include_success = rest_server_json_bool_with_default(cJSON_GetObjectItemCaseSensitive(json, "include_success"), true); + include_failed = rest_server_json_bool_with_default(cJSON_GetObjectItemCaseSensitive(json, "include_failed"), true); + include_canceled = rest_server_json_bool_with_default(cJSON_GetObjectItemCaseSensitive(json, "include_canceled"), true); + cJSON_Delete(json); + } + + size_t removed = printer_protocol_cleanup_jobs(include_success, include_failed, include_canceled); + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "removed", (double)removed); + cJSON_AddBoolToObject(root, "include_success", include_success); + cJSON_AddBoolToObject(root, "include_failed", include_failed); + cJSON_AddBoolToObject(root, "include_canceled", include_canceled); + + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + diff --git a/main/control_plane/src/rest_server_ops.c b/main/control_plane/src/rest_server_ops.c new file mode 100644 index 0000000..ed5a4fe --- /dev/null +++ b/main/control_plane/src/rest_server_ops.c @@ -0,0 +1,535 @@ +#include "rest_server_internal.h" + +#include +#include +#include + +#include "printer_protocol.h" +#include "system_runtime.h" +static void fill_runtime_json(cJSON *root) { + char ip[32] = {0}; + system_runtime_get_ip(ip, sizeof(ip)); + + printer_runtime_status_t st = {0}; + printer_protocol_get_runtime_status(&st); + + cJSON_AddBoolToObject(root, "wifi_ready", system_runtime_wifi_ready()); + cJSON_AddStringToObject(root, "wifi_mode", "sta"); + cJSON_AddStringToObject(root, "ip", ip); + + cJSON_AddBoolToObject(root, "ble_connected", st.connected); + cJSON_AddBoolToObject(root, "ble_notify_ready", st.notify_ready); + cJSON_AddBoolToObject(root, "printer_busy", st.busy); + cJSON_AddBoolToObject(root, "has_paper", st.has_paper); + cJSON_AddNumberToObject(root, "battery_percent", st.battery_percent); + cJSON_AddNumberToObject(root, "temperature", st.temperature); + cJSON_AddNumberToObject(root, "queue_depth", st.queue_depth); + cJSON_AddNumberToObject(root, "mtu", st.mtu); + cJSON_AddNumberToObject(root, "last_status_ms", (double)st.last_status_ms); +} + +esp_err_t rest_server_health_get(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + fill_runtime_json(root); + + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_connect_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + char name[32] = "lyfPrinter"; + uint32_t timeout_ms = 15000; + + if (req->content_len > 0) { + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + if (json == NULL) { + free(body); + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jname = cJSON_GetObjectItemCaseSensitive(json, "name"); + if (cJSON_IsString(jname) && jname->valuestring != NULL) { + strlcpy(name, jname->valuestring, sizeof(name)); + } + + cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms"); + if (cJSON_IsNumber(jtimeout) && jtimeout->valuedouble > 0) { + timeout_ms = (uint32_t)jtimeout->valuedouble; + } + + cJSON_Delete(json); + free(body); + } + + esp_err_t err = printer_protocol_connect(name, timeout_ms); + if (err != ESP_OK) { + if (err == ESP_ERR_TIMEOUT) { + return rest_server_send_error(req, "504 Gateway Timeout", "connect timeout"); + } + return rest_server_send_error(req, "500 Internal Server Error", "connect failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddStringToObject(root, "message", "connected"); + fill_runtime_json(root); + + err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_disconnect_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + printer_protocol_disconnect(); + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddStringToObject(root, "message", "disconnected"); + + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_status_get(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + fill_runtime_json(root); + + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_label_gap_move_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + uint32_t timeout_ms = 5000; + if (req->content_len > 0) { + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms"); + if (cJSON_IsNumber(jtimeout) && jtimeout->valuedouble > 0 && jtimeout->valuedouble <= 30000) { + timeout_ms = (uint32_t)jtimeout->valuedouble; + } + cJSON_Delete(json); + } + + char cmd_err[128] = {0}; + esp_err_t rc = printer_protocol_gap_move(timeout_ms, cmd_err, sizeof(cmd_err)); + if (rc != ESP_OK) { + return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "gap move failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddStringToObject(root, "message", "gap move success"); + cJSON_AddNumberToObject(root, "timeout_ms", timeout_ms); + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_label_offset_get(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + uint8_t offset = 0; + char cmd_err[128] = {0}; + esp_err_t rc = printer_protocol_get_label_offset(&offset, 3000, cmd_err, sizeof(cmd_err)); + if (rc != ESP_OK) { + return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "get offset failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "offset_tenths_mm", offset); + cJSON_AddNumberToObject(root, "offset_mm", ((double)offset) / 10.0); + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_label_offset_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jvalue = cJSON_GetObjectItemCaseSensitive(json, "offset_tenths_mm"); + if (!cJSON_IsNumber(jvalue) || jvalue->valuedouble < 0 || jvalue->valuedouble > 254) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "offset_tenths_mm must be 0..254"); + } + + uint8_t value = (uint8_t)jvalue->valuedouble; + cJSON_Delete(json); + + char cmd_err[128] = {0}; + esp_err_t rc = printer_protocol_set_label_offset(value, 3000, cmd_err, sizeof(cmd_err)); + if (rc != ESP_OK) { + return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "set offset failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "offset_tenths_mm", value); + cJSON_AddNumberToObject(root, "offset_mm", ((double)value) / 10.0); + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_ota_version_get(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + printer_ota_version_t version = {0}; + char cmd_err[128] = {0}; + esp_err_t rc = printer_protocol_ota_get_version(&version, 4000, cmd_err, sizeof(cmd_err)); + if (rc != ESP_OK) { + return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "get version failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "major", version.major); + cJSON_AddNumberToObject(root, "minor", version.minor); + cJSON_AddNumberToObject(root, "patch", version.patch); + char version_text[32]; + snprintf(version_text, sizeof(version_text), "V%u.%u.%u", version.major, version.minor, version.patch); + cJSON_AddStringToObject(root, "version", version_text); + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_ota_jump_boot_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char cmd_err[128] = {0}; + esp_err_t rc = printer_protocol_ota_jump_boot(5000, cmd_err, sizeof(cmd_err)); + if (rc != ESP_OK) { + return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "jump boot failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddStringToObject(root, "message", "jumped to boot"); + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_ota_jump_app_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char cmd_err[128] = {0}; + esp_err_t rc = printer_protocol_ota_jump_app(5000, cmd_err, sizeof(cmd_err)); + if (rc != ESP_OK) { + return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "jump app failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddStringToObject(root, "message", "jumped to app"); + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_ota_erase_page_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jpage = cJSON_GetObjectItemCaseSensitive(json, "page_num"); + cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms"); + if (!cJSON_IsNumber(jpage) || jpage->valuedouble < 0 || jpage->valuedouble > 65535) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "page_num must be 0..65535"); + } + + uint32_t timeout_ms = 5000; + if (cJSON_IsNumber(jtimeout) && jtimeout->valuedouble > 0 && jtimeout->valuedouble <= 30000) { + timeout_ms = (uint32_t)jtimeout->valuedouble; + } + + uint16_t page_num = (uint16_t)jpage->valuedouble; + cJSON_Delete(json); + + char cmd_err[128] = {0}; + esp_err_t rc = printer_protocol_ota_erase_page(page_num, timeout_ms, cmd_err, sizeof(cmd_err)); + if (rc != ESP_OK) { + return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "erase page failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "page_num", page_num); + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_ota_write_frame_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jpack = cJSON_GetObjectItemCaseSensitive(json, "packet_num"); + cJSON *jlast = cJSON_GetObjectItemCaseSensitive(json, "is_last_frame"); + cJSON *jdata = cJSON_GetObjectItemCaseSensitive(json, "data"); + cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms"); + + if (!cJSON_IsNumber(jpack) || jpack->valuedouble < 0 || jpack->valuedouble > 65535 || + !cJSON_IsString(jdata) || jdata->valuestring == NULL) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "packet_num and data are required"); + } + + uint32_t timeout_ms = 4000; + if (cJSON_IsNumber(jtimeout) && jtimeout->valuedouble > 0 && jtimeout->valuedouble <= 30000) { + timeout_ms = (uint32_t)jtimeout->valuedouble; + } + bool is_last = rest_server_json_bool_with_default(jlast, false); + uint16_t packet_num = (uint16_t)jpack->valuedouble; + + uint8_t *frame_data = NULL; + size_t frame_len = 0; + esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &frame_data, &frame_len); + cJSON_Delete(json); + if (b64_rc != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid base64 data"); + } + + char cmd_err[128] = {0}; + esp_err_t rc = printer_protocol_ota_write_frame(packet_num, + is_last, + frame_data, + frame_len, + timeout_ms, + cmd_err, + sizeof(cmd_err)); + free(frame_data); + if (rc != ESP_OK) { + return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "write frame failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "packet_num", packet_num); + cJSON_AddBoolToObject(root, "is_last_frame", is_last); + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_ota_upgrade_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jfirmware = cJSON_GetObjectItemCaseSensitive(json, "firmware"); + cJSON *jjumpboot = cJSON_GetObjectItemCaseSensitive(json, "jump_boot"); + cJSON *jjumpapp = cJSON_GetObjectItemCaseSensitive(json, "jump_app"); + cJSON *jpagesize = cJSON_GetObjectItemCaseSensitive(json, "page_size"); + cJSON *jpacketsize = cJSON_GetObjectItemCaseSensitive(json, "packet_size"); + cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms_per_step"); + cJSON *jreadversion = cJSON_GetObjectItemCaseSensitive(json, "read_version_after"); + + if (!cJSON_IsString(jfirmware) || jfirmware->valuestring == NULL) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "firmware(base64) is required"); + } + + bool jump_boot = rest_server_json_bool_with_default(jjumpboot, true); + bool jump_app = rest_server_json_bool_with_default(jjumpapp, true); + bool read_version_after = rest_server_json_bool_with_default(jreadversion, true); + + uint16_t page_size = 1024; + uint16_t packet_size = 236; + uint32_t timeout_ms = 5000; + if (cJSON_IsNumber(jpagesize) && jpagesize->valuedouble >= 256 && jpagesize->valuedouble <= 4096) { + page_size = (uint16_t)jpagesize->valuedouble; + } + if (cJSON_IsNumber(jpacketsize) && jpacketsize->valuedouble >= 16 && jpacketsize->valuedouble <= 236) { + packet_size = (uint16_t)jpacketsize->valuedouble; + } + if (cJSON_IsNumber(jtimeout) && jtimeout->valuedouble >= 500 && jtimeout->valuedouble <= 60000) { + timeout_ms = (uint32_t)jtimeout->valuedouble; + } + + uint8_t *firmware = NULL; + size_t firmware_len = 0; + esp_err_t b64_rc = rest_server_base64_decode_alloc(jfirmware->valuestring, &firmware, &firmware_len); + cJSON_Delete(json); + if (b64_rc != ESP_OK || firmware_len == 0) { + free(firmware); + return rest_server_send_error(req, "400 Bad Request", "invalid firmware base64"); + } + + char cmd_err[128] = {0}; + if (jump_boot) { + esp_err_t rc = printer_protocol_ota_jump_boot(timeout_ms, cmd_err, sizeof(cmd_err)); + if (rc != ESP_OK) { + free(firmware); + return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "jump boot failed"); + } + } + + uint32_t total_pages = (uint32_t)((firmware_len + page_size - 1) / page_size); + for (uint32_t page = 0; page < total_pages; ++page) { + esp_err_t rc = printer_protocol_ota_erase_page((uint16_t)page, timeout_ms, cmd_err, sizeof(cmd_err)); + if (rc != ESP_OK) { + free(firmware); + return rest_server_send_error(req, + "409 Conflict", + cmd_err[0] != '\0' ? cmd_err : "erase page failed"); + } + } + + uint32_t total_packets = (uint32_t)((firmware_len + packet_size - 1) / packet_size); + for (uint32_t packet = 0; packet < total_packets; ++packet) { + size_t start = (size_t)packet * packet_size; + size_t remain = firmware_len - start; + size_t len = remain > packet_size ? packet_size : remain; + bool is_last = (packet + 1) == total_packets; + + esp_err_t rc = printer_protocol_ota_write_frame((uint16_t)packet, + is_last, + firmware + start, + len, + timeout_ms, + cmd_err, + sizeof(cmd_err)); + if (rc != ESP_OK) { + free(firmware); + return rest_server_send_error(req, + "409 Conflict", + cmd_err[0] != '\0' ? cmd_err : "write frame failed"); + } + } + + if (jump_app) { + esp_err_t rc = printer_protocol_ota_jump_app(timeout_ms, cmd_err, sizeof(cmd_err)); + if (rc != ESP_OK) { + free(firmware); + return rest_server_send_error(req, "409 Conflict", cmd_err[0] != '\0' ? cmd_err : "jump app failed"); + } + } + + printer_ota_version_t version = {0}; + bool version_ok = false; + if (read_version_after) { + if (printer_protocol_ota_get_version(&version, timeout_ms, cmd_err, sizeof(cmd_err)) == ESP_OK) { + version_ok = true; + } + } + + free(firmware); + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "firmware_len", (double)firmware_len); + cJSON_AddNumberToObject(root, "page_size", page_size); + cJSON_AddNumberToObject(root, "packet_size", packet_size); + cJSON_AddNumberToObject(root, "total_pages", total_pages); + cJSON_AddNumberToObject(root, "total_packets", total_packets); + cJSON_AddBoolToObject(root, "jump_boot", jump_boot); + cJSON_AddBoolToObject(root, "jump_app", jump_app); + if (version_ok) { + cJSON *v = cJSON_AddObjectToObject(root, "version"); + cJSON_AddNumberToObject(v, "major", version.major); + cJSON_AddNumberToObject(v, "minor", version.minor); + cJSON_AddNumberToObject(v, "patch", version.patch); + } + + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} diff --git a/main/control_plane/src/rest_server_print.c b/main/control_plane/src/rest_server_print.c new file mode 100644 index 0000000..f860d4c --- /dev/null +++ b/main/control_plane/src/rest_server_print.c @@ -0,0 +1,659 @@ +#include "rest_server_internal.h" + +#include +#include +#include +#include + +#include "printer_protocol.h" +#include "raster_tools.h" +static esp_err_t submit_raster_job_and_reply(httpd_req_t *req, + const uint8_t *raw, + size_t raw_len, + uint16_t width, + uint16_t height, + const char *density, + const char *warning) { + char submit_err[128] = {0}; + uint32_t job_id = 0; + esp_err_t submit_rc = printer_protocol_submit_raster_job(raw, + raw_len, + width, + height, + density, + &job_id, + submit_err, + sizeof(submit_err)); + if (submit_rc != ESP_OK) { + return rest_server_send_error(req, + "400 Bad Request", + submit_err[0] != '\0' ? submit_err : "submit failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddNumberToObject(root, "job_id", job_id); + cJSON_AddStringToObject(root, "state", "queued"); + if (warning != NULL && warning[0] != '\0') { + cJSON_AddStringToObject(root, "warning", warning); + } + + esp_err_t err = rest_server_send_json(req, "202 Accepted", root); + cJSON_Delete(root); + return err; +} + +static esp_err_t decode_image_json_to_raster(cJSON *json, + uint16_t *out_width, + uint16_t *out_height, + uint8_t **out_raster, + size_t *out_len, + char *err, + size_t err_len) { + if (json == NULL || out_width == NULL || out_height == NULL || out_raster == NULL || out_len == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid args"); + } + return ESP_ERR_INVALID_ARG; + } + + *out_width = 0; + *out_height = 0; + *out_raster = NULL; + *out_len = 0; + + cJSON *jwidth = cJSON_GetObjectItemCaseSensitive(json, "width"); + cJSON *jheight = cJSON_GetObjectItemCaseSensitive(json, "height"); + cJSON *jencoding = cJSON_GetObjectItemCaseSensitive(json, "encoding"); + cJSON *jdata = cJSON_GetObjectItemCaseSensitive(json, "data"); + + if (!cJSON_IsNumber(jwidth) || !cJSON_IsNumber(jheight) || + !cJSON_IsString(jencoding) || !cJSON_IsString(jdata)) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "missing required image fields"); + } + return ESP_ERR_INVALID_ARG; + } + + uint16_t width = (uint16_t)jwidth->valuedouble; + uint16_t height = (uint16_t)jheight->valuedouble; + if (width == 0 || height == 0) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid image size"); + } + return ESP_ERR_INVALID_ARG; + } + + if (strcmp(jencoding->valuestring, "base64_msb_1bpp") == 0) { + uint8_t *raw = NULL; + size_t raw_len = 0; + esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &raw, &raw_len); + if (b64_rc != ESP_OK) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid base64"); + } + return ESP_ERR_INVALID_ARG; + } + *out_width = width; + *out_height = height; + *out_raster = raw; + *out_len = raw_len; + return ESP_OK; + } + + if (strcmp(jencoding->valuestring, "base64_gray8") == 0) { + uint8_t *gray = NULL; + size_t gray_len = 0; + esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &gray, &gray_len); + if (b64_rc != ESP_OK) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid base64"); + } + return ESP_ERR_INVALID_ARG; + } + + size_t expected = (size_t)width * height; + if (gray_len != expected) { + free(gray); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "gray8 size mismatch"); + } + return ESP_ERR_INVALID_SIZE; + } + + cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale_to_width"); + cJSON *jthreshold = cJSON_GetObjectItemCaseSensitive(json, "threshold"); + cJSON *jinvert = cJSON_GetObjectItemCaseSensitive(json, "invert"); + cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); + + bool scale_to_width = rest_server_json_bool_with_default(jscale, true); + bool invert = rest_server_json_bool_with_default(jinvert, false); + uint8_t threshold = 160; + uint16_t max_height = 2200; + if (cJSON_IsNumber(jthreshold) && jthreshold->valuedouble >= 0 && jthreshold->valuedouble <= 255) { + threshold = (uint8_t)jthreshold->valuedouble; + } + if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { + max_height = (uint16_t)jmaxh->valuedouble; + } + + uint16_t out_w = 0; + uint16_t out_h = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + esp_err_t conv_rc = raster_tools_convert_gray8_to_raster_384(gray, + width, + height, + scale_to_width, + threshold, + invert, + max_height, + &out_w, + &out_h, + &raster, + &raster_len, + err, + err_len); + free(gray); + if (conv_rc != ESP_OK) { + return conv_rc; + } + + *out_width = out_w; + *out_height = out_h; + *out_raster = raster; + *out_len = raster_len; + return ESP_OK; + } + + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "unsupported encoding"); + } + return ESP_ERR_NOT_SUPPORTED; +} + +esp_err_t rest_server_print_raster_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jwidth = cJSON_GetObjectItemCaseSensitive(json, "width"); + cJSON *jheight = cJSON_GetObjectItemCaseSensitive(json, "height"); + cJSON *jencoding = cJSON_GetObjectItemCaseSensitive(json, "encoding"); + cJSON *jdata = cJSON_GetObjectItemCaseSensitive(json, "data"); + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + + if (!cJSON_IsNumber(jwidth) || !cJSON_IsNumber(jheight) || + !cJSON_IsString(jencoding) || !cJSON_IsString(jdata)) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "missing required fields"); + } + + if (strcmp(jencoding->valuestring, "base64_msb_1bpp") != 0) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "unsupported encoding"); + } + + const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) + ? jdensity->valuestring + : "中等"; + + uint8_t *raw = NULL; + size_t raw_len = 0; + esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &raw, &raw_len); + if (b64_rc != ESP_OK) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "invalid base64"); + } + + esp_err_t submit_rc = submit_raster_job_and_reply(req, + raw, + raw_len, + (uint16_t)jwidth->valuedouble, + (uint16_t)jheight->valuedouble, + density, + NULL); + free(raw); + cJSON_Delete(json); + return submit_rc; +} + +esp_err_t rest_server_print_image_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) + ? jdensity->valuestring + : "中等"; + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char decode_err[128] = {0}; + esp_err_t decode_rc = decode_image_json_to_raster(json, + &width, + &height, + &raster, + &raster_len, + decode_err, + sizeof(decode_err)); + cJSON_Delete(json); + if (decode_rc != ESP_OK) { + return rest_server_send_error(req, + "400 Bad Request", + decode_err[0] != '\0' ? decode_err : "decode image failed"); + } + + esp_err_t submit_rc = submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + density, + NULL); + free(raster); + return submit_rc; +} + +esp_err_t rest_server_print_qr_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jtext = cJSON_GetObjectItemCaseSensitive(json, "text"); + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + cJSON *jecc = cJSON_GetObjectItemCaseSensitive(json, "ecc"); + cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "module_scale"); + cJSON *jmargin = cJSON_GetObjectItemCaseSensitive(json, "margin_modules"); + cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); + + if (!cJSON_IsString(jtext) || jtext->valuestring == NULL || jtext->valuestring[0] == '\0') { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "text is required"); + } + + const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) + ? jdensity->valuestring + : "中等"; + + uint8_t ecc_level = 3; /* default H */ + if (cJSON_IsString(jecc) && jecc->valuestring != NULL) { + if (strcasecmp(jecc->valuestring, "L") == 0) { + ecc_level = 0; + } else if (strcasecmp(jecc->valuestring, "M") == 0) { + ecc_level = 1; + } else if (strcasecmp(jecc->valuestring, "Q") == 0) { + ecc_level = 2; + } else if (strcasecmp(jecc->valuestring, "H") == 0) { + ecc_level = 3; + } + } + + uint8_t module_scale = 0; + uint8_t margin_modules = 2; + uint16_t max_height = 2200; + if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 0 && jscale->valuedouble <= 16) { + module_scale = (uint8_t)jscale->valuedouble; + } + if (cJSON_IsNumber(jmargin) && jmargin->valuedouble >= 1 && jmargin->valuedouble <= 12) { + margin_modules = (uint8_t)jmargin->valuedouble; + } + if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { + max_height = (uint16_t)jmaxh->valuedouble; + } + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char render_msg[128] = {0}; + esp_err_t qr_rc = raster_tools_render_qr_384(jtext->valuestring, + module_scale, + margin_modules, + ecc_level, + max_height, + &width, + &height, + &raster, + &raster_len, + render_msg, + sizeof(render_msg)); + cJSON_Delete(json); + if (qr_rc != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", render_msg[0] != '\0' ? render_msg : "qr render failed"); + } + + esp_err_t submit_rc = submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + density, + NULL); + free(raster); + return submit_rc; +} + +esp_err_t rest_server_print_label_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + cJSON *jgap = cJSON_GetObjectItemCaseSensitive(json, "gap_move_before"); + cJSON *joffset = cJSON_GetObjectItemCaseSensitive(json, "offset_tenths_mm"); + + bool gap_move_before = rest_server_json_bool_with_default(jgap, true); + bool has_offset = cJSON_IsNumber(joffset); + uint8_t offset_value = 0; + if (has_offset) { + if (joffset->valuedouble < 0 || joffset->valuedouble > 254) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "offset_tenths_mm must be 0..254"); + } + offset_value = (uint8_t)joffset->valuedouble; + } + + if (has_offset) { + char cmd_err[128] = {0}; + esp_err_t offset_rc = printer_protocol_set_label_offset(offset_value, 3000, cmd_err, sizeof(cmd_err)); + if (offset_rc != ESP_OK) { + cJSON_Delete(json); + return rest_server_send_error(req, + "409 Conflict", + cmd_err[0] != '\0' ? cmd_err : "set label offset failed"); + } + } + + if (gap_move_before) { + char cmd_err[128] = {0}; + esp_err_t gap_rc = printer_protocol_gap_move(5000, cmd_err, sizeof(cmd_err)); + if (gap_rc != ESP_OK) { + cJSON_Delete(json); + return rest_server_send_error(req, + "409 Conflict", + cmd_err[0] != '\0' ? cmd_err : "gap move failed"); + } + } + + const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) + ? jdensity->valuestring + : "中等"; + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char decode_err[128] = {0}; + esp_err_t decode_rc = decode_image_json_to_raster(json, + &width, + &height, + &raster, + &raster_len, + decode_err, + sizeof(decode_err)); + cJSON_Delete(json); + if (decode_rc != ESP_OK) { + return rest_server_send_error(req, + "400 Bad Request", + decode_err[0] != '\0' ? decode_err : "decode image failed"); + } + + esp_err_t submit_rc = submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + density, + NULL); + free(raster); + return submit_rc; +} + +esp_err_t rest_server_print_text_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jtext = cJSON_GetObjectItemCaseSensitive(json, "text"); + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale"); + cJSON *jline = cJSON_GetObjectItemCaseSensitive(json, "line_spacing"); + cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); + + if (!cJSON_IsString(jtext) || jtext->valuestring == NULL) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "text is required"); + } + + const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) + ? jdensity->valuestring + : "中等"; + uint8_t scale = 2; + uint8_t line_spacing = 2; + uint16_t max_height = 2000; + + if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 1 && jscale->valuedouble <= 6) { + scale = (uint8_t)jscale->valuedouble; + } + if (cJSON_IsNumber(jline) && jline->valuedouble >= 0 && jline->valuedouble <= 12) { + line_spacing = (uint8_t)jline->valuedouble; + } + if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { + max_height = (uint16_t)jmaxh->valuedouble; + } + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char render_msg[128] = {0}; + + esp_err_t render_rc = raster_tools_render_text_384(jtext->valuestring, + scale, + line_spacing, + max_height, + &width, + &height, + &raster, + &raster_len, + render_msg, + sizeof(render_msg)); + cJSON_Delete(json); + + if (render_rc != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", render_msg[0] != '\0' ? render_msg : "text render failed"); + } + + esp_err_t submit_rc = submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + density, + render_msg); + free(raster); + return submit_rc; +} + +esp_err_t rest_server_print_receipt_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jtitle = cJSON_GetObjectItemCaseSensitive(json, "title"); + cJSON *jitems = cJSON_GetObjectItemCaseSensitive(json, "items"); + cJSON *jfooter = cJSON_GetObjectItemCaseSensitive(json, "footer"); + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale"); + + const char *title = (cJSON_IsString(jtitle) && jtitle->valuestring != NULL) + ? jtitle->valuestring + : "LYF RECEIPT"; + const char *footer = (cJSON_IsString(jfooter) && jfooter->valuestring != NULL) + ? jfooter->valuestring + : "Thank you"; + const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) + ? jdensity->valuestring + : "中等"; + uint8_t scale = 2; + if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 1 && jscale->valuedouble <= 4) { + scale = (uint8_t)jscale->valuedouble; + } + + size_t text_cap = 8192; + char *text = (char *)calloc(1, text_cap); + if (text == NULL) { + cJSON_Delete(json); + return rest_server_send_error(req, "500 Internal Server Error", "no memory"); + } + + size_t off = 0; + rest_server_appendf(text, text_cap, &off, "%s\n", title); + rest_server_appendf(text, text_cap, &off, "--------------------------------\n"); + + double total = 0.0; + if (cJSON_IsArray(jitems)) { + int n = cJSON_GetArraySize(jitems); + for (int i = 0; i < n; ++i) { + cJSON *it = cJSON_GetArrayItem(jitems, i); + if (!cJSON_IsObject(it)) { + continue; + } + + cJSON *jname = cJSON_GetObjectItemCaseSensitive(it, "name"); + cJSON *jqty = cJSON_GetObjectItemCaseSensitive(it, "qty"); + cJSON *jprice = cJSON_GetObjectItemCaseSensitive(it, "price"); + + const char *name = (cJSON_IsString(jname) && jname->valuestring != NULL) + ? jname->valuestring + : "item"; + int qty = cJSON_IsNumber(jqty) ? (int)jqty->valuedouble : 1; + double price = cJSON_IsNumber(jprice) ? jprice->valuedouble : 0.0; + double line_total = qty * price; + total += line_total; + + rest_server_appendf(text, + text_cap, + &off, + "%-16.16s x%-3d %8.2f\n", + name, + qty, + line_total); + } + } else { + rest_server_appendf(text, text_cap, &off, "(no items)\n"); + } + + rest_server_appendf(text, text_cap, &off, "--------------------------------\n"); + rest_server_appendf(text, text_cap, &off, "TOTAL: %.2f\n", total); + rest_server_appendf(text, text_cap, &off, "%s\n", footer); + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char render_msg[128] = {0}; + + esp_err_t render_rc = raster_tools_render_text_384(text, + scale, + 2, + 2200, + &width, + &height, + &raster, + &raster_len, + render_msg, + sizeof(render_msg)); + free(text); + cJSON_Delete(json); + + if (render_rc != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", render_msg[0] != '\0' ? render_msg : "receipt render failed"); + } + + esp_err_t submit_rc = submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + density, + render_msg); + free(raster); + return submit_rc; +} diff --git a/main/dependency_whitelist.md b/main/dependency_whitelist.md new file mode 100644 index 0000000..a77af50 --- /dev/null +++ b/main/dependency_whitelist.md @@ -0,0 +1,13 @@ +# Dependency Whitelist + +Use this file only when a same-level or lateral dependency is unavoidable. + +Entry format: +- requester: `/` +- target: `/` +- reason: short technical justification +- owner: person/team +- expiry: YYYY-MM-DD (must be reviewed before expiry) + +Current entries: +- none diff --git a/main/domain/README.md b/main/domain/README.md new file mode 100644 index 0000000..e16ed16 --- /dev/null +++ b/main/domain/README.md @@ -0,0 +1,9 @@ +# Domain Layer + +Purpose: business capabilities with single responsibility per domain service. + +Rules: +- Expose stable domain APIs in `include/`. +- Keep implementation details in `internal/` and `src/`. +- No direct cross-domain internal state access. +- Cross-domain interactions must use events or Port interfaces. diff --git a/main/domain/assets/fonts/cn16_glyphs.bin b/main/domain/assets/fonts/cn16_glyphs.bin new file mode 100644 index 0000000..631b943 Binary files /dev/null and b/main/domain/assets/fonts/cn16_glyphs.bin differ diff --git a/main/domain/assets/fonts/cn16_index.bin b/main/domain/assets/fonts/cn16_index.bin new file mode 100644 index 0000000..2638f0d Binary files /dev/null and b/main/domain/assets/fonts/cn16_index.bin differ diff --git a/main/domain/include/.gitkeep b/main/domain/include/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/domain/include/printer_protocol.h b/main/domain/include/printer_protocol.h new file mode 100644 index 0000000..6e82d8a --- /dev/null +++ b/main/domain/include/printer_protocol.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include + +#include "esp_err.h" + +typedef enum { + PRINT_JOB_STATE_NONE = 0, + PRINT_JOB_STATE_QUEUED, + PRINT_JOB_STATE_RUNNING, + PRINT_JOB_STATE_SUCCESS, + PRINT_JOB_STATE_FAILED, + PRINT_JOB_STATE_CANCELED, +} print_job_state_t; + +typedef struct { + uint32_t id; + print_job_state_t state; + uint8_t progress; + uint16_t width; + uint16_t height; + size_t data_len; + char density[16]; + char error[96]; + int64_t created_ms; + int64_t started_ms; + int64_t finished_ms; +} print_job_info_t; + +typedef struct { + uint8_t major; + uint8_t minor; + uint8_t patch; +} printer_ota_version_t; + +typedef struct { + bool connected; + bool notify_ready; + bool busy; + bool has_paper; + uint8_t battery_percent; + float temperature; + uint16_t mtu; + uint32_t queue_depth; + int64_t last_status_ms; +} printer_runtime_status_t; + +esp_err_t printer_protocol_init(void); + +esp_err_t printer_protocol_connect(const char *target_name, uint32_t timeout_ms); +void printer_protocol_disconnect(void); + +void printer_protocol_get_runtime_status(printer_runtime_status_t *out_status); + +esp_err_t printer_protocol_submit_raster_job(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + const char *density, + uint32_t *out_job_id, + char *err, + size_t err_len); + +bool printer_protocol_get_job(uint32_t job_id, print_job_info_t *out_info); +esp_err_t printer_protocol_list_jobs(print_job_info_t *out_jobs, size_t max_jobs, size_t *out_count); +esp_err_t printer_protocol_cancel_job(uint32_t job_id, char *err, size_t err_len); +size_t printer_protocol_cleanup_jobs(bool include_success, bool include_failed, bool include_canceled); + +esp_err_t printer_protocol_gap_move(uint32_t timeout_ms, char *err, size_t err_len); +esp_err_t printer_protocol_get_label_offset(uint8_t *out_offset, uint32_t timeout_ms, char *err, size_t err_len); +esp_err_t printer_protocol_set_label_offset(uint8_t offset, uint32_t timeout_ms, char *err, size_t err_len); + +esp_err_t printer_protocol_ota_jump_boot(uint32_t timeout_ms, char *err, size_t err_len); +esp_err_t printer_protocol_ota_jump_app(uint32_t timeout_ms, char *err, size_t err_len); +esp_err_t printer_protocol_ota_erase_page(uint16_t page_num, uint32_t timeout_ms, char *err, size_t err_len); +esp_err_t printer_protocol_ota_write_frame(uint16_t packet_num, + bool is_last_frame, + const uint8_t *data, + size_t data_len, + uint32_t timeout_ms, + char *err, + size_t err_len); +esp_err_t printer_protocol_ota_get_version(printer_ota_version_t *out_version, + uint32_t timeout_ms, + char *err, + size_t err_len); + +const char *printer_protocol_job_state_str(print_job_state_t state); diff --git a/main/domain/include/raster_tools.h b/main/domain/include/raster_tools.h new file mode 100644 index 0000000..93347e5 --- /dev/null +++ b/main/domain/include/raster_tools.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include "esp_err.h" + +esp_err_t raster_tools_render_text_384(const char *text, + uint8_t scale, + uint8_t line_spacing, + uint16_t max_height, + uint16_t *out_width, + uint16_t *out_height, + uint8_t **out_raster, + size_t *out_len, + char *err, + size_t err_len); + +esp_err_t raster_tools_convert_gray8_to_raster_384(const uint8_t *gray, + uint16_t src_width, + uint16_t src_height, + bool scale_to_width, + uint8_t threshold, + bool invert, + uint16_t max_height, + uint16_t *out_width, + uint16_t *out_height, + uint8_t **out_raster, + size_t *out_len, + char *err, + size_t err_len); + +esp_err_t raster_tools_render_qr_384(const char *text, + uint8_t module_scale, + uint8_t margin_modules, + uint8_t ecc_level, + uint16_t max_height, + uint16_t *out_width, + uint16_t *out_height, + uint8_t **out_raster, + size_t *out_len, + char *err, + size_t err_len); diff --git a/main/domain/include/system_runtime.h b/main/domain/include/system_runtime.h new file mode 100644 index 0000000..675e188 --- /dev/null +++ b/main/domain/include/system_runtime.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +#include "esp_err.h" + +esp_err_t system_runtime_bootstrap(void); + +bool system_runtime_wifi_ready(void); +void system_runtime_get_ip(char *buf, size_t buf_len); diff --git a/main/domain/internal/.gitkeep b/main/domain/internal/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/domain/internal/printer_protocol_internal.h b/main/domain/internal/printer_protocol_internal.h new file mode 100644 index 0000000..8bc78e0 --- /dev/null +++ b/main/domain/internal/printer_protocol_internal.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#include "esp_err.h" + +#define CMD_GAP_MOVE 0x05 +#define CMD_GET_OFFSET 0x06 +#define CMD_SET_OFFSET 0x07 +#define CMD_BOOT_JUMP_BOOT 0xA0 +#define CMD_BOOT_ERASE_PAGE 0xA1 +#define CMD_BOOT_WRITE_DATA 0xA2 +#define CMD_BOOT_JUMP_APP 0xA3 +#define CMD_BOOT_GET_VERSION 0xA4 + +#define OTA_MAX_DATA_PER_FRAME 236u + +bool printer_protocol_acquire_control_lane(uint32_t timeout_ms, char *err, size_t err_len); +void printer_protocol_release_control_lane(void); +esp_err_t printer_protocol_send_cmd_wait_response(uint8_t cmd, + const uint8_t *payload, + uint16_t payload_len, + bool with_checksum, + uint32_t timeout_ms, + bool expect_ack, + uint8_t *out_payload, + size_t out_payload_cap, + uint16_t *out_payload_len, + char *err, + size_t err_len); diff --git a/main/domain/src/.gitkeep b/main/domain/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/domain/src/printer_protocol.c b/main/domain/src/printer_protocol.c new file mode 100644 index 0000000..cf3599c --- /dev/null +++ b/main/domain/src/printer_protocol.c @@ -0,0 +1,931 @@ +#include "printer_protocol.h" +#include "printer_protocol_internal.h" + +#include +#include +#include +#include + +#include "ble_printer_client.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#include "freertos/task.h" + +#define PROTO_ADDR 0x01 + +#define CMD_POWER 0x00 +#define CMD_GET_STATUS 0x01 +#define CMD_SET_DISTANCE 0x02 +#define CMD_SET_PARAM 0x03 +#define CMD_SEND_DATA 0x04 + +#define EVT_ACK BIT0 + +#define JOB_QUEUE_LEN 8 +#define JOB_SLOT_MAX 16 +#define MAX_RASTER_BYTES (384 * 3000 / 8) + +typedef struct { + bool used; + bool cancel_requested; + uint32_t id; + print_job_state_t state; + uint8_t progress; + uint16_t width; + uint16_t height; + size_t data_len; + char density[16]; + char error[96]; + int64_t created_ms; + int64_t started_ms; + int64_t finished_ms; + uint8_t *data; +} job_slot_t; + +typedef struct { + bool has_paper; + uint8_t battery; + float temperature; + int64_t updated_ms; +} parsed_status_t; + +static SemaphoreHandle_t s_mutex; +static QueueHandle_t s_job_queue; +static EventGroupHandle_t s_evt; + +static uint32_t s_busy_refcnt; + +static parsed_status_t s_status = { + .has_paper = true, + .battery = 100, + .temperature = 25.0f, + .updated_ms = 0, +}; + +static uint8_t s_last_rsp_cmd; +static uint16_t s_last_rsp_payload_len; +static uint8_t s_last_rsp_payload[252]; + +static uint32_t s_next_job_id = 1; +static job_slot_t s_jobs[JOB_SLOT_MAX]; + +static int find_job_idx_locked(uint32_t id) { + for (int i = 0; i < JOB_SLOT_MAX; ++i) { + if (s_jobs[i].used && s_jobs[i].id == id) { + return i; + } + } + return -1; +} + +static int alloc_job_slot_locked(void) { + for (int i = 0; i < JOB_SLOT_MAX; ++i) { + if (!s_jobs[i].used) { + return i; + } + } + + /* Reuse completed slot when table is full. */ + for (int i = 0; i < JOB_SLOT_MAX; ++i) { + if (s_jobs[i].state == PRINT_JOB_STATE_SUCCESS || + s_jobs[i].state == PRINT_JOB_STATE_FAILED || + s_jobs[i].state == PRINT_JOB_STATE_CANCELED) { + free(s_jobs[i].data); + memset(&s_jobs[i], 0, sizeof(s_jobs[i])); + return i; + } + } + + return -1; +} + +static bool is_terminal_state(print_job_state_t state) { + return state == PRINT_JOB_STATE_SUCCESS || + state == PRINT_JOB_STATE_FAILED || + state == PRINT_JOB_STATE_CANCELED; +} + +bool printer_protocol_acquire_control_lane(uint32_t timeout_ms, char *err, size_t err_len) { + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "lock timeout"); + } + return false; + } + + if (s_busy_refcnt != 0) { + xSemaphoreGive(s_mutex); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer busy"); + } + return false; + } + + s_busy_refcnt = 1; + xSemaphoreGive(s_mutex); + return true; +} + +void printer_protocol_release_control_lane(void) { + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + return; + } + + if (s_busy_refcnt > 0) { + --s_busy_refcnt; + } + xSemaphoreGive(s_mutex); +} + +static uint8_t checksum8(const uint8_t *data, size_t len) { + uint32_t sum = 0; + for (size_t i = 0; i < len; ++i) { + sum += data[i]; + } + return (uint8_t)(sum & 0xFF); +} + +static esp_err_t send_frame(uint8_t cmd, const uint8_t *payload, uint16_t payload_len, bool with_checksum) { + uint8_t frame[260]; + size_t total = 4 + payload_len + (with_checksum ? 1 : 0); + if (total > sizeof(frame)) { + return ESP_ERR_INVALID_SIZE; + } + + frame[0] = PROTO_ADDR; + frame[1] = cmd; + frame[2] = (uint8_t)((payload_len >> 8) & 0xFF); + frame[3] = (uint8_t)(payload_len & 0xFF); + + if (payload_len > 0 && payload != NULL) { + memcpy(&frame[4], payload, payload_len); + } + + if (with_checksum) { + frame[4 + payload_len] = checksum8(frame, 4 + payload_len); + } + + return ble_printer_client_write(frame, total); +} + +static void clear_ack_signal(void) { + xEventGroupClearBits(s_evt, EVT_ACK); +} + +static bool wait_response(uint8_t cmd, + uint8_t *out_payload, + size_t out_payload_cap, + uint16_t *out_payload_len, + uint32_t timeout_ms) { + int64_t deadline = esp_timer_get_time() / 1000 + timeout_ms; + + while (true) { + int64_t now = esp_timer_get_time() / 1000; + if (now >= deadline) { + return false; + } + + uint32_t wait_ms = (uint32_t)(deadline - now); + EventBits_t bits = xEventGroupWaitBits(s_evt, + EVT_ACK, + pdTRUE, + pdFALSE, + pdMS_TO_TICKS(wait_ms)); + if (!(bits & EVT_ACK)) { + return false; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { + continue; + } + + uint8_t rsp_cmd = s_last_rsp_cmd; + uint16_t rsp_len = s_last_rsp_payload_len; + uint8_t rsp_copy[sizeof(s_last_rsp_payload)]; + if (rsp_len > sizeof(rsp_copy)) { + rsp_len = sizeof(rsp_copy); + } + if (rsp_len > 0) { + memcpy(rsp_copy, s_last_rsp_payload, rsp_len); + } + xSemaphoreGive(s_mutex); + + if (rsp_cmd == cmd) { + if (out_payload != NULL && out_payload_cap > 0 && rsp_len > 0) { + size_t copy_len = rsp_len; + if (copy_len > out_payload_cap) { + copy_len = out_payload_cap; + } + memcpy(out_payload, rsp_copy, copy_len); + } + if (out_payload_len != NULL) { + *out_payload_len = rsp_len; + } + return true; + } + } +} + +static bool wait_ack(uint8_t cmd, uint32_t timeout_ms) { + uint8_t payload[252]; + uint16_t payload_len = 0; + if (!wait_response(cmd, payload, sizeof(payload), &payload_len, timeout_ms)) { + return false; + } + return payload_len >= 1 && payload[0] == 0x01; +} + +static bool send_cmd_with_ack(uint8_t cmd, + const uint8_t *payload, + uint16_t payload_len, + bool with_checksum, + uint32_t timeout_ms) { + clear_ack_signal(); + if (send_frame(cmd, payload, payload_len, with_checksum) != ESP_OK) { + return false; + } + return wait_ack(cmd, timeout_ms); +} + +esp_err_t printer_protocol_send_cmd_wait_response(uint8_t cmd, + const uint8_t *payload, + uint16_t payload_len, + bool with_checksum, + uint32_t timeout_ms, + bool expect_ack, + uint8_t *out_payload, + size_t out_payload_cap, + uint16_t *out_payload_len, + char *err, + size_t err_len) { + clear_ack_signal(); + if (send_frame(cmd, payload, payload_len, with_checksum) != ESP_OK) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "send frame failed"); + } + return ESP_FAIL; + } + + uint8_t rsp[252]; + uint16_t rsp_len = 0; + if (!wait_response(cmd, rsp, sizeof(rsp), &rsp_len, timeout_ms)) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "response timeout"); + } + return ESP_ERR_TIMEOUT; + } + + if (expect_ack && (rsp_len < 1 || rsp[0] != 0x01)) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "cmd 0x%02X rejected", cmd); + } + return ESP_FAIL; + } + + if (out_payload != NULL && out_payload_cap > 0 && rsp_len > 0) { + size_t copy_len = rsp_len; + if (copy_len > out_payload_cap) { + copy_len = out_payload_cap; + } + memcpy(out_payload, rsp, copy_len); + } + if (out_payload_len != NULL) { + *out_payload_len = rsp_len; + } + + return ESP_OK; +} + +static bool request_status_sync(uint32_t timeout_ms) { + int64_t old_ms; + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { + return false; + } + old_ms = s_status.updated_ms; + xSemaphoreGive(s_mutex); + + uint8_t payload = 0x00; + if (send_frame(CMD_GET_STATUS, &payload, 0, true) != ESP_OK) { + return false; + } + + int64_t deadline = esp_timer_get_time() / 1000 + timeout_ms; + while ((esp_timer_get_time() / 1000) < deadline) { + vTaskDelay(pdMS_TO_TICKS(20)); + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { + continue; + } + + bool updated = s_status.updated_ms > old_ms; + xSemaphoreGive(s_mutex); + + if (updated) { + return true; + } + } + + return false; +} + +static uint16_t density_to_hot_time(const char *density) { + if (density == NULL) { + return 2000; + } + if (strcmp(density, "较淡") == 0) { + return 1000; + } + if (strcmp(density, "中等") == 0) { + return 1500; + } + if (strcmp(density, "较浓") == 0) { + return 2000; + } + if (strcmp(density, "最深") == 0) { + return 3000; + } + return 2000; +} + +static bool precheck_printer_ready(char *err, size_t err_len) { + if (!request_status_sync(1200)) { + snprintf(err, err_len, "status timeout"); + return false; + } + + parsed_status_t status; + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { + snprintf(err, err_len, "status lock failed"); + return false; + } + + status = s_status; + xSemaphoreGive(s_mutex); + + if (!status.has_paper) { + snprintf(err, err_len, "printer out of paper"); + return false; + } + + if (status.battery <= 40) { + snprintf(err, err_len, "battery too low"); + return false; + } + + if (status.temperature >= 60.0f) { + snprintf(err, err_len, "temperature too high"); + return false; + } + + return true; +} + +static bool run_print_job(job_slot_t *job) { + char err[96]; + err[0] = '\0'; + + if (!ble_printer_client_is_connected()) { + snprintf(job->error, sizeof(job->error), "printer not connected"); + return false; + } + + if (!precheck_printer_ready(err, sizeof(err))) { + snprintf(job->error, sizeof(job->error), "%s", err); + return false; + } + + if (job->cancel_requested) { + snprintf(job->error, sizeof(job->error), "job canceled"); + return false; + } + + uint8_t power_on = 0x01; + if (!send_cmd_with_ack(CMD_POWER, &power_on, 1, true, 1500)) { + snprintf(job->error, sizeof(job->error), "power on failed"); + return false; + } + + uint16_t hot_time = density_to_hot_time(job->density); + uint8_t param[4] = { + 0x01, + 0x02, + (uint8_t)((hot_time >> 8) & 0xFF), + (uint8_t)(hot_time & 0xFF), + }; + + if (!send_cmd_with_ack(CMD_SET_PARAM, param, sizeof(param), true, 1500)) { + snprintf(job->error, sizeof(job->error), "set print param failed"); + return false; + } + + const size_t chunk_max = 240; + size_t total_chunks = (size_t)ceil((double)job->data_len / (double)chunk_max); + + for (size_t i = 0; i < total_chunks; ++i) { + if (job->cancel_requested) { + snprintf(job->error, sizeof(job->error), "job canceled"); + return false; + } + + size_t start = i * chunk_max; + size_t remain = job->data_len - start; + size_t chunk_len = remain > chunk_max ? chunk_max : remain; + + if (!send_cmd_with_ack(CMD_SEND_DATA, + &job->data[start], + (uint16_t)chunk_len, + false, + 2500)) { + snprintf(job->error, sizeof(job->error), "send chunk timeout at %u", (unsigned)i); + return false; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { + job->progress = (uint8_t)(((i + 1) * 90) / total_chunks); + if (job->progress < 5) { + job->progress = 5; + } + xSemaphoreGive(s_mutex); + } + } + + uint8_t power_off = 0x00; + (void)send_cmd_with_ack(CMD_POWER, &power_off, 1, true, 1200); + + uint8_t feed_payload[3] = {0x2B, 0x00, 0x0C}; + (void)send_cmd_with_ack(CMD_SET_DISTANCE, feed_payload, sizeof(feed_payload), true, 1200); + + return true; +} + +static void worker_task(void *arg) { + (void)arg; + + while (true) { + uint32_t job_id; + if (xQueueReceive(s_job_queue, &job_id, portMAX_DELAY) != pdTRUE) { + continue; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + continue; + } + + int idx = find_job_idx_locked(job_id); + if (idx < 0) { + xSemaphoreGive(s_mutex); + continue; + } + + if (s_jobs[idx].state == PRINT_JOB_STATE_CANCELED) { + free(s_jobs[idx].data); + s_jobs[idx].data = NULL; + if (s_jobs[idx].finished_ms == 0) { + s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; + } + xSemaphoreGive(s_mutex); + continue; + } + + if (s_busy_refcnt != 0) { + xSemaphoreGive(s_mutex); + xQueueSendToFront(s_job_queue, &job_id, 0); + vTaskDelay(pdMS_TO_TICKS(20)); + continue; + } + + s_jobs[idx].state = PRINT_JOB_STATE_RUNNING; + s_jobs[idx].started_ms = esp_timer_get_time() / 1000; + s_jobs[idx].progress = 1; + s_busy_refcnt = 1; + xSemaphoreGive(s_mutex); + + bool ok = run_print_job(&s_jobs[idx]); + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + continue; + } + + if (s_busy_refcnt > 0) { + --s_busy_refcnt; + } + s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; + if (s_jobs[idx].state != PRINT_JOB_STATE_CANCELED) { + s_jobs[idx].progress = 100; + } + + if (ok) { + s_jobs[idx].state = PRINT_JOB_STATE_SUCCESS; + s_jobs[idx].error[0] = '\0'; + } else if (s_jobs[idx].cancel_requested) { + s_jobs[idx].state = PRINT_JOB_STATE_CANCELED; + if (s_jobs[idx].error[0] == '\0') { + strlcpy(s_jobs[idx].error, "job canceled", sizeof(s_jobs[idx].error)); + } + } else { + s_jobs[idx].state = PRINT_JOB_STATE_FAILED; + } + + free(s_jobs[idx].data); + s_jobs[idx].data = NULL; + xSemaphoreGive(s_mutex); + } +} + +static void status_poll_task(void *arg) { + (void)arg; + + while (true) { + bool can_poll = false; + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { + can_poll = (s_busy_refcnt == 0); + xSemaphoreGive(s_mutex); + } + + if (ble_printer_client_is_connected() && can_poll) { + uint8_t dummy = 0x00; + (void)send_frame(CMD_GET_STATUS, &dummy, 0, true); + } + vTaskDelay(pdMS_TO_TICKS(5000)); + } +} + +static void on_rx_frame(const uint8_t *data, size_t len) { + if (data == NULL || len < 4) { + return; + } + + if (data[0] != PROTO_ADDR) { + return; + } + + uint8_t cmd = data[1]; + uint16_t payload_len = ((uint16_t)data[2] << 8) | data[3]; + size_t required = (size_t)payload_len + 4; + if (len < required) { + return; + } + + const uint8_t *payload = &data[4]; + + if (cmd == CMD_GET_STATUS && payload_len >= 5) { + bool has_paper = payload[0] != 0; + uint8_t battery = payload[1]; + int sign = (payload[2] == 0x2D) ? -1 : 1; + int temp_x10 = ((int)payload[3] << 8) | payload[4]; + float temperature = (float)(sign * temp_x10) / 10.0f; + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + s_status.has_paper = has_paper; + s_status.battery = battery; + s_status.temperature = temperature; + s_status.updated_ms = esp_timer_get_time() / 1000; + xSemaphoreGive(s_mutex); + } + return; + } + + if (payload_len >= 1) { + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + s_last_rsp_cmd = cmd; + s_last_rsp_payload_len = payload_len; + if (s_last_rsp_payload_len > sizeof(s_last_rsp_payload)) { + s_last_rsp_payload_len = sizeof(s_last_rsp_payload); + } + memcpy(s_last_rsp_payload, payload, s_last_rsp_payload_len); + xSemaphoreGive(s_mutex); + } + xEventGroupSetBits(s_evt, EVT_ACK); + } +} + +esp_err_t printer_protocol_init(void) { + s_mutex = xSemaphoreCreateMutex(); + if (s_mutex == NULL) { + return ESP_ERR_NO_MEM; + } + + s_evt = xEventGroupCreate(); + if (s_evt == NULL) { + return ESP_ERR_NO_MEM; + } + + s_job_queue = xQueueCreate(JOB_QUEUE_LEN, sizeof(uint32_t)); + if (s_job_queue == NULL) { + return ESP_ERR_NO_MEM; + } + + esp_err_t err = ble_printer_client_init(on_rx_frame); + if (err != ESP_OK) { + return err; + } + + BaseType_t ok = xTaskCreate(worker_task, "print_worker", 6144, NULL, 6, NULL); + if (ok != pdPASS) { + return ESP_ERR_NO_MEM; + } + + ok = xTaskCreate(status_poll_task, "status_poll", 4096, NULL, 4, NULL); + if (ok != pdPASS) { + return ESP_ERR_NO_MEM; + } + + return ESP_OK; +} + +esp_err_t printer_protocol_connect(const char *target_name, uint32_t timeout_ms) { + return ble_printer_client_connect(target_name, timeout_ms); +} + +void printer_protocol_disconnect(void) { + ble_printer_client_disconnect(); +} + +void printer_protocol_get_runtime_status(printer_runtime_status_t *out_status) { + if (out_status == NULL) { + return; + } + + memset(out_status, 0, sizeof(*out_status)); + + ble_link_state_t link = {0}; + ble_printer_client_get_link_state(&link); + + out_status->connected = link.connected; + out_status->notify_ready = link.notify_ready; + out_status->mtu = link.mtu; + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { + out_status->busy = s_busy_refcnt > 0; + out_status->has_paper = s_status.has_paper; + out_status->battery_percent = s_status.battery; + out_status->temperature = s_status.temperature; + out_status->last_status_ms = s_status.updated_ms; + out_status->queue_depth = (uint32_t)uxQueueMessagesWaiting(s_job_queue); + xSemaphoreGive(s_mutex); + } +} + +esp_err_t printer_protocol_submit_raster_job(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + const char *density, + uint32_t *out_job_id, + char *err, + size_t err_len) { + if (raster == NULL || raster_len == 0 || width == 0 || height == 0) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid args"); + } + return ESP_ERR_INVALID_ARG; + } + + if (width != 384) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "width must be 384"); + } + return ESP_ERR_INVALID_ARG; + } + + if (raster_len > MAX_RASTER_BYTES) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "raster too large"); + } + return ESP_ERR_INVALID_SIZE; + } + + size_t expected_len = ((size_t)width / 8u) * (size_t)height; + if (expected_len != raster_len) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "size mismatch exp=%u got=%u", + (unsigned)expected_len, + (unsigned)raster_len); + } + return ESP_ERR_INVALID_SIZE; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "lock timeout"); + } + return ESP_ERR_TIMEOUT; + } + + int idx = alloc_job_slot_locked(); + if (idx < 0) { + xSemaphoreGive(s_mutex); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "job table full"); + } + return ESP_ERR_NO_MEM; + } + + uint8_t *copy = (uint8_t *)malloc(raster_len); + if (copy == NULL) { + xSemaphoreGive(s_mutex); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "malloc failed"); + } + return ESP_ERR_NO_MEM; + } + + memcpy(copy, raster, raster_len); + + uint32_t id = s_next_job_id++; + if (s_next_job_id == 0) { + s_next_job_id = 1; + } + + memset(&s_jobs[idx], 0, sizeof(s_jobs[idx])); + s_jobs[idx].used = true; + s_jobs[idx].id = id; + s_jobs[idx].state = PRINT_JOB_STATE_QUEUED; + s_jobs[idx].progress = 0; + s_jobs[idx].width = width; + s_jobs[idx].height = height; + s_jobs[idx].data_len = raster_len; + s_jobs[idx].created_ms = esp_timer_get_time() / 1000; + s_jobs[idx].data = copy; + strlcpy(s_jobs[idx].density, density != NULL ? density : "中等", sizeof(s_jobs[idx].density)); + + xSemaphoreGive(s_mutex); + + if (xQueueSend(s_job_queue, &id, pdMS_TO_TICKS(500)) != pdTRUE) { + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) == pdTRUE) { + int rollback_idx = find_job_idx_locked(id); + if (rollback_idx >= 0) { + free(s_jobs[rollback_idx].data); + memset(&s_jobs[rollback_idx], 0, sizeof(s_jobs[rollback_idx])); + } + xSemaphoreGive(s_mutex); + } + + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "job queue full"); + } + return ESP_ERR_TIMEOUT; + } + + if (out_job_id != NULL) { + *out_job_id = id; + } + + return ESP_OK; +} + +bool printer_protocol_get_job(uint32_t job_id, print_job_info_t *out_info) { + if (out_info == NULL || job_id == 0) { + return false; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + return false; + } + + int idx = find_job_idx_locked(job_id); + if (idx < 0) { + xSemaphoreGive(s_mutex); + return false; + } + + memset(out_info, 0, sizeof(*out_info)); + + out_info->id = s_jobs[idx].id; + out_info->state = s_jobs[idx].state; + out_info->progress = s_jobs[idx].progress; + out_info->width = s_jobs[idx].width; + out_info->height = s_jobs[idx].height; + out_info->data_len = s_jobs[idx].data_len; + out_info->created_ms = s_jobs[idx].created_ms; + out_info->started_ms = s_jobs[idx].started_ms; + out_info->finished_ms = s_jobs[idx].finished_ms; + strlcpy(out_info->density, s_jobs[idx].density, sizeof(out_info->density)); + strlcpy(out_info->error, s_jobs[idx].error, sizeof(out_info->error)); + + xSemaphoreGive(s_mutex); + return true; +} + +esp_err_t printer_protocol_list_jobs(print_job_info_t *out_jobs, size_t max_jobs, size_t *out_count) { + if (out_count == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + return ESP_ERR_TIMEOUT; + } + + size_t count = 0; + for (int i = 0; i < JOB_SLOT_MAX; ++i) { + if (!s_jobs[i].used || s_jobs[i].state == PRINT_JOB_STATE_NONE) { + continue; + } + if (out_jobs != NULL && count < max_jobs) { + memset(&out_jobs[count], 0, sizeof(out_jobs[count])); + out_jobs[count].id = s_jobs[i].id; + out_jobs[count].state = s_jobs[i].state; + out_jobs[count].progress = s_jobs[i].progress; + out_jobs[count].width = s_jobs[i].width; + out_jobs[count].height = s_jobs[i].height; + out_jobs[count].data_len = s_jobs[i].data_len; + out_jobs[count].created_ms = s_jobs[i].created_ms; + out_jobs[count].started_ms = s_jobs[i].started_ms; + out_jobs[count].finished_ms = s_jobs[i].finished_ms; + strlcpy(out_jobs[count].density, s_jobs[i].density, sizeof(out_jobs[count].density)); + strlcpy(out_jobs[count].error, s_jobs[i].error, sizeof(out_jobs[count].error)); + } + ++count; + } + + xSemaphoreGive(s_mutex); + *out_count = count; + return ESP_OK; +} + +esp_err_t printer_protocol_cancel_job(uint32_t job_id, char *err, size_t err_len) { + if (job_id == 0) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid job id"); + } + return ESP_ERR_INVALID_ARG; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "lock timeout"); + } + return ESP_ERR_TIMEOUT; + } + + int idx = find_job_idx_locked(job_id); + if (idx < 0) { + xSemaphoreGive(s_mutex); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "job not found"); + } + return ESP_ERR_NOT_FOUND; + } + + if (is_terminal_state(s_jobs[idx].state)) { + xSemaphoreGive(s_mutex); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "job already finished"); + } + return ESP_ERR_INVALID_STATE; + } + + s_jobs[idx].cancel_requested = true; + if (s_jobs[idx].state == PRINT_JOB_STATE_QUEUED) { + s_jobs[idx].state = PRINT_JOB_STATE_CANCELED; + s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; + s_jobs[idx].progress = 0; + strlcpy(s_jobs[idx].error, "job canceled", sizeof(s_jobs[idx].error)); + free(s_jobs[idx].data); + s_jobs[idx].data = NULL; + } else if (s_jobs[idx].state == PRINT_JOB_STATE_RUNNING) { + strlcpy(s_jobs[idx].error, "cancel requested", sizeof(s_jobs[idx].error)); + } + + xSemaphoreGive(s_mutex); + return ESP_OK; +} + +size_t printer_protocol_cleanup_jobs(bool include_success, bool include_failed, bool include_canceled) { + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + return 0; + } + + size_t removed = 0; + for (int i = 0; i < JOB_SLOT_MAX; ++i) { + if (!s_jobs[i].used) { + continue; + } + + bool match = false; + if (include_success && s_jobs[i].state == PRINT_JOB_STATE_SUCCESS) { + match = true; + } + if (include_failed && s_jobs[i].state == PRINT_JOB_STATE_FAILED) { + match = true; + } + if (include_canceled && s_jobs[i].state == PRINT_JOB_STATE_CANCELED) { + match = true; + } + + if (!match) { + continue; + } + + free(s_jobs[i].data); + memset(&s_jobs[i], 0, sizeof(s_jobs[i])); + ++removed; + } + + xSemaphoreGive(s_mutex); + return removed; +} diff --git a/main/domain/src/printer_protocol_commands.c b/main/domain/src/printer_protocol_commands.c new file mode 100644 index 0000000..734f8e0 --- /dev/null +++ b/main/domain/src/printer_protocol_commands.c @@ -0,0 +1,322 @@ +#include "printer_protocol.h" +#include "printer_protocol_internal.h" + +#include +#include + +#include "ble_printer_client.h" + +esp_err_t printer_protocol_gap_move(uint32_t timeout_ms, char *err, size_t err_len) { + if (!ble_printer_client_is_connected()) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer not connected"); + } + return ESP_ERR_INVALID_STATE; + } + + if (!printer_protocol_acquire_control_lane(1000, err, err_len)) { + return ESP_ERR_INVALID_STATE; + } + + uint8_t payload = 0x01; + esp_err_t rc = printer_protocol_send_cmd_wait_response(CMD_GAP_MOVE, + &payload, + 1, + true, + timeout_ms, + true, + NULL, + 0, + NULL, + err, + err_len); + printer_protocol_release_control_lane(); + return rc; +} + +esp_err_t printer_protocol_get_label_offset(uint8_t *out_offset, uint32_t timeout_ms, char *err, size_t err_len) { + if (out_offset == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid args"); + } + return ESP_ERR_INVALID_ARG; + } + + if (!ble_printer_client_is_connected()) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer not connected"); + } + return ESP_ERR_INVALID_STATE; + } + + if (!printer_protocol_acquire_control_lane(1000, err, err_len)) { + return ESP_ERR_INVALID_STATE; + } + + uint8_t req = 0x01; + uint8_t rsp[16]; + uint16_t rsp_len = 0; + esp_err_t rc = printer_protocol_send_cmd_wait_response(CMD_GET_OFFSET, + &req, + 1, + true, + timeout_ms, + false, + rsp, + sizeof(rsp), + &rsp_len, + err, + err_len); + printer_protocol_release_control_lane(); + if (rc != ESP_OK) { + return rc; + } + if (rsp_len < 1) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid offset response"); + } + return ESP_FAIL; + } + if (rsp[0] == 0xFF) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "offset unavailable"); + } + return ESP_FAIL; + } + + *out_offset = rsp[0]; + return ESP_OK; +} + +esp_err_t printer_protocol_set_label_offset(uint8_t offset, uint32_t timeout_ms, char *err, size_t err_len) { + if (!ble_printer_client_is_connected()) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer not connected"); + } + return ESP_ERR_INVALID_STATE; + } + + if (!printer_protocol_acquire_control_lane(1000, err, err_len)) { + return ESP_ERR_INVALID_STATE; + } + + esp_err_t rc = printer_protocol_send_cmd_wait_response(CMD_SET_OFFSET, + &offset, + 1, + true, + timeout_ms, + true, + NULL, + 0, + NULL, + err, + err_len); + printer_protocol_release_control_lane(); + return rc; +} + +esp_err_t printer_protocol_ota_jump_boot(uint32_t timeout_ms, char *err, size_t err_len) { + if (!ble_printer_client_is_connected()) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer not connected"); + } + return ESP_ERR_INVALID_STATE; + } + + if (!printer_protocol_acquire_control_lane(1000, err, err_len)) { + return ESP_ERR_INVALID_STATE; + } + + esp_err_t rc = printer_protocol_send_cmd_wait_response(CMD_BOOT_JUMP_BOOT, + NULL, + 0, + true, + timeout_ms, + true, + NULL, + 0, + NULL, + err, + err_len); + printer_protocol_release_control_lane(); + return rc; +} + +esp_err_t printer_protocol_ota_jump_app(uint32_t timeout_ms, char *err, size_t err_len) { + if (!ble_printer_client_is_connected()) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer not connected"); + } + return ESP_ERR_INVALID_STATE; + } + + if (!printer_protocol_acquire_control_lane(1000, err, err_len)) { + return ESP_ERR_INVALID_STATE; + } + + esp_err_t rc = printer_protocol_send_cmd_wait_response(CMD_BOOT_JUMP_APP, + NULL, + 0, + true, + timeout_ms, + true, + NULL, + 0, + NULL, + err, + err_len); + printer_protocol_release_control_lane(); + return rc; +} + +esp_err_t printer_protocol_ota_erase_page(uint16_t page_num, uint32_t timeout_ms, char *err, size_t err_len) { + if (!ble_printer_client_is_connected()) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer not connected"); + } + return ESP_ERR_INVALID_STATE; + } + + if (!printer_protocol_acquire_control_lane(1000, err, err_len)) { + return ESP_ERR_INVALID_STATE; + } + + uint8_t payload[2] = { + (uint8_t)((page_num >> 8) & 0xFF), + (uint8_t)(page_num & 0xFF), + }; + + esp_err_t rc = printer_protocol_send_cmd_wait_response(CMD_BOOT_ERASE_PAGE, + payload, + sizeof(payload), + true, + timeout_ms, + true, + NULL, + 0, + NULL, + err, + err_len); + printer_protocol_release_control_lane(); + return rc; +} + +esp_err_t printer_protocol_ota_write_frame(uint16_t packet_num, + bool is_last_frame, + const uint8_t *data, + size_t data_len, + uint32_t timeout_ms, + char *err, + size_t err_len) { + if ((data_len > 0 && data == NULL) || data_len > OTA_MAX_DATA_PER_FRAME) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid frame length"); + } + return ESP_ERR_INVALID_ARG; + } + + if (!ble_printer_client_is_connected()) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer not connected"); + } + return ESP_ERR_INVALID_STATE; + } + + if (!printer_protocol_acquire_control_lane(1000, err, err_len)) { + return ESP_ERR_INVALID_STATE; + } + + uint8_t payload[3 + OTA_MAX_DATA_PER_FRAME]; + payload[0] = (uint8_t)((packet_num >> 8) & 0xFF); + payload[1] = (uint8_t)(packet_num & 0xFF); + payload[2] = is_last_frame ? 0x01 : 0x00; + if (data_len > 0) { + memcpy(&payload[3], data, data_len); + } + + esp_err_t rc = printer_protocol_send_cmd_wait_response(CMD_BOOT_WRITE_DATA, + payload, + (uint16_t)(3 + data_len), + true, + timeout_ms, + true, + NULL, + 0, + NULL, + err, + err_len); + printer_protocol_release_control_lane(); + return rc; +} + +esp_err_t printer_protocol_ota_get_version(printer_ota_version_t *out_version, + uint32_t timeout_ms, + char *err, + size_t err_len) { + if (out_version == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid args"); + } + return ESP_ERR_INVALID_ARG; + } + + if (!ble_printer_client_is_connected()) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer not connected"); + } + return ESP_ERR_INVALID_STATE; + } + + if (!printer_protocol_acquire_control_lane(1000, err, err_len)) { + return ESP_ERR_INVALID_STATE; + } + + uint8_t rsp[16]; + uint16_t rsp_len = 0; + esp_err_t rc = printer_protocol_send_cmd_wait_response(CMD_BOOT_GET_VERSION, + NULL, + 0, + true, + timeout_ms, + false, + rsp, + sizeof(rsp), + &rsp_len, + err, + err_len); + printer_protocol_release_control_lane(); + if (rc != ESP_OK) { + return rc; + } + + if (rsp_len < 3) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid version response"); + } + return ESP_FAIL; + } + + out_version->major = rsp[0]; + out_version->minor = rsp[1]; + out_version->patch = rsp[2]; + return ESP_OK; +} + +const char *printer_protocol_job_state_str(print_job_state_t state) { + switch (state) { + case PRINT_JOB_STATE_NONE: + return "none"; + case PRINT_JOB_STATE_QUEUED: + return "queued"; + case PRINT_JOB_STATE_RUNNING: + return "running"; + case PRINT_JOB_STATE_SUCCESS: + return "success"; + case PRINT_JOB_STATE_FAILED: + return "failed"; + case PRINT_JOB_STATE_CANCELED: + return "canceled"; + default: + return "unknown"; + } +} diff --git a/main/domain/src/raster_tools.c b/main/domain/src/raster_tools.c new file mode 100644 index 0000000..0b19dc4 --- /dev/null +++ b/main/domain/src/raster_tools.c @@ -0,0 +1,467 @@ +#include "raster_tools.h" + +#include +#include +#include +#include + +#define RASTER_WIDTH 384 +#define RASTER_BYTES_PER_ROW (RASTER_WIDTH / 8) +#define CN16_GLYPH_BYTES 32u +#define CN16_INDEX_ENTRY_BYTES 6u +#define UTF8_REPLACEMENT_CODEPOINT 0x003Fu + +extern const uint8_t _binary_cn16_index_bin_start[] asm("_binary_cn16_index_bin_start"); +extern const uint8_t _binary_cn16_index_bin_end[] asm("_binary_cn16_index_bin_end"); +extern const uint8_t _binary_cn16_glyphs_bin_start[] asm("_binary_cn16_glyphs_bin_start"); +extern const uint8_t _binary_cn16_glyphs_bin_end[] asm("_binary_cn16_glyphs_bin_end"); + +/* 5x7 ASCII font table for characters 0x20..0x7E. */ +static const uint8_t font5x7[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, /* space */ + 0x00, 0x00, 0x5F, 0x00, 0x00, /* ! */ + 0x00, 0x07, 0x00, 0x07, 0x00, /* " */ + 0x14, 0x7F, 0x14, 0x7F, 0x14, /* # */ + 0x24, 0x2A, 0x7F, 0x2A, 0x12, /* $ */ + 0x23, 0x13, 0x08, 0x64, 0x62, /* % */ + 0x36, 0x49, 0x55, 0x22, 0x50, /* & */ + 0x00, 0x05, 0x03, 0x00, 0x00, /* ' */ + 0x00, 0x1C, 0x22, 0x41, 0x00, /* ( */ + 0x00, 0x41, 0x22, 0x1C, 0x00, /* ) */ + 0x14, 0x08, 0x3E, 0x08, 0x14, /* * */ + 0x08, 0x08, 0x3E, 0x08, 0x08, /* + */ + 0x00, 0x50, 0x30, 0x00, 0x00, /* , */ + 0x08, 0x08, 0x08, 0x08, 0x08, /* - */ + 0x00, 0x60, 0x60, 0x00, 0x00, /* . */ + 0x20, 0x10, 0x08, 0x04, 0x02, /* / */ + 0x3E, 0x51, 0x49, 0x45, 0x3E, /* 0 */ + 0x00, 0x42, 0x7F, 0x40, 0x00, /* 1 */ + 0x42, 0x61, 0x51, 0x49, 0x46, /* 2 */ + 0x21, 0x41, 0x45, 0x4B, 0x31, /* 3 */ + 0x18, 0x14, 0x12, 0x7F, 0x10, /* 4 */ + 0x27, 0x45, 0x45, 0x45, 0x39, /* 5 */ + 0x3C, 0x4A, 0x49, 0x49, 0x30, /* 6 */ + 0x01, 0x71, 0x09, 0x05, 0x03, /* 7 */ + 0x36, 0x49, 0x49, 0x49, 0x36, /* 8 */ + 0x06, 0x49, 0x49, 0x29, 0x1E, /* 9 */ + 0x00, 0x36, 0x36, 0x00, 0x00, /* : */ + 0x00, 0x56, 0x36, 0x00, 0x00, /* ; */ + 0x08, 0x14, 0x22, 0x41, 0x00, /* < */ + 0x14, 0x14, 0x14, 0x14, 0x14, /* = */ + 0x00, 0x41, 0x22, 0x14, 0x08, /* > */ + 0x02, 0x01, 0x51, 0x09, 0x06, /* ? */ + 0x32, 0x49, 0x79, 0x41, 0x3E, /* @ */ + 0x7E, 0x11, 0x11, 0x11, 0x7E, /* A */ + 0x7F, 0x49, 0x49, 0x49, 0x36, /* B */ + 0x3E, 0x41, 0x41, 0x41, 0x22, /* C */ + 0x7F, 0x41, 0x41, 0x22, 0x1C, /* D */ + 0x7F, 0x49, 0x49, 0x49, 0x41, /* E */ + 0x7F, 0x09, 0x09, 0x09, 0x01, /* F */ + 0x3E, 0x41, 0x49, 0x49, 0x7A, /* G */ + 0x7F, 0x08, 0x08, 0x08, 0x7F, /* H */ + 0x00, 0x41, 0x7F, 0x41, 0x00, /* I */ + 0x20, 0x40, 0x41, 0x3F, 0x01, /* J */ + 0x7F, 0x08, 0x14, 0x22, 0x41, /* K */ + 0x7F, 0x40, 0x40, 0x40, 0x40, /* L */ + 0x7F, 0x02, 0x0C, 0x02, 0x7F, /* M */ + 0x7F, 0x04, 0x08, 0x10, 0x7F, /* N */ + 0x3E, 0x41, 0x41, 0x41, 0x3E, /* O */ + 0x7F, 0x09, 0x09, 0x09, 0x06, /* P */ + 0x3E, 0x41, 0x51, 0x21, 0x5E, /* Q */ + 0x7F, 0x09, 0x19, 0x29, 0x46, /* R */ + 0x46, 0x49, 0x49, 0x49, 0x31, /* S */ + 0x01, 0x01, 0x7F, 0x01, 0x01, /* T */ + 0x3F, 0x40, 0x40, 0x40, 0x3F, /* U */ + 0x1F, 0x20, 0x40, 0x20, 0x1F, /* V */ + 0x7F, 0x20, 0x18, 0x20, 0x7F, /* W */ + 0x63, 0x14, 0x08, 0x14, 0x63, /* X */ + 0x03, 0x04, 0x78, 0x04, 0x03, /* Y */ + 0x61, 0x51, 0x49, 0x45, 0x43, /* Z */ + 0x00, 0x00, 0x7F, 0x41, 0x41, /* [ */ + 0x02, 0x04, 0x08, 0x10, 0x20, /* \\ */ + 0x41, 0x41, 0x7F, 0x00, 0x00, /* ] */ + 0x04, 0x02, 0x01, 0x02, 0x04, /* ^ */ + 0x80, 0x80, 0x80, 0x80, 0x80, /* _ */ + 0x00, 0x03, 0x05, 0x00, 0x00, /* ` */ + 0x20, 0x54, 0x54, 0x54, 0x78, /* a */ + 0x7F, 0x48, 0x44, 0x44, 0x38, /* b */ + 0x38, 0x44, 0x44, 0x44, 0x20, /* c */ + 0x38, 0x44, 0x44, 0x48, 0x7F, /* d */ + 0x38, 0x54, 0x54, 0x54, 0x18, /* e */ + 0x08, 0x7E, 0x09, 0x01, 0x02, /* f */ + 0x0C, 0x52, 0x52, 0x52, 0x3E, /* g */ + 0x7F, 0x08, 0x04, 0x04, 0x78, /* h */ + 0x00, 0x44, 0x7D, 0x40, 0x00, /* i */ + 0x20, 0x40, 0x44, 0x3D, 0x00, /* j */ + 0x7F, 0x10, 0x28, 0x44, 0x00, /* k */ + 0x00, 0x41, 0x7F, 0x40, 0x00, /* l */ + 0x7C, 0x04, 0x18, 0x04, 0x78, /* m */ + 0x7C, 0x08, 0x04, 0x04, 0x78, /* n */ + 0x38, 0x44, 0x44, 0x44, 0x38, /* o */ + 0x7C, 0x14, 0x14, 0x14, 0x08, /* p */ + 0x08, 0x14, 0x14, 0x18, 0x7C, /* q */ + 0x7C, 0x08, 0x04, 0x04, 0x08, /* r */ + 0x48, 0x54, 0x54, 0x54, 0x20, /* s */ + 0x04, 0x3F, 0x44, 0x40, 0x20, /* t */ + 0x3C, 0x40, 0x40, 0x20, 0x7C, /* u */ + 0x1C, 0x20, 0x40, 0x20, 0x1C, /* v */ + 0x3C, 0x40, 0x30, 0x40, 0x3C, /* w */ + 0x44, 0x28, 0x10, 0x28, 0x44, /* x */ + 0x0C, 0x50, 0x50, 0x50, 0x3C, /* y */ + 0x44, 0x64, 0x54, 0x4C, 0x44, /* z */ + 0x00, 0x08, 0x36, 0x41, 0x00, /* { */ + 0x00, 0x00, 0x7F, 0x00, 0x00, /* | */ + 0x00, 0x41, 0x36, 0x08, 0x00, /* } */ + 0x08, 0x08, 0x2A, 0x1C, 0x08 /* ~ */ +}; + +static void set_black(uint8_t *buf, uint16_t width, uint16_t x, uint16_t y) { + if (x >= width) { + return; + } + uint16_t bpr = width / 8; + size_t idx = (size_t)y * bpr + (x / 8); + uint8_t bit = (uint8_t)(7 - (x % 8)); + buf[idx] |= (uint8_t)(1u << bit); +} + +static size_t cn16_index_count(void) { + size_t bytes = (size_t)(_binary_cn16_index_bin_end - _binary_cn16_index_bin_start); + return bytes / CN16_INDEX_ENTRY_BYTES; +} + +static bool cn16_read_index_entry(size_t idx, uint32_t *out_cp, uint16_t *out_gid) { + if (out_cp == NULL || out_gid == NULL) { + return false; + } + size_t count = cn16_index_count(); + if (idx >= count) { + return false; + } + + const uint8_t *p = _binary_cn16_index_bin_start + idx * CN16_INDEX_ENTRY_BYTES; + *out_cp = ((uint32_t)p[0]) | + ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); + *out_gid = (uint16_t)(p[4] | ((uint16_t)p[5] << 8)); + return true; +} + +static bool cn16_lookup_glyph(uint32_t codepoint, const uint8_t **out_glyph) { + if (out_glyph == NULL) { + return false; + } + + size_t count = cn16_index_count(); + if (count == 0) { + return false; + } + + size_t lo = 0; + size_t hi = count; + while (lo < hi) { + size_t mid = lo + ((hi - lo) / 2); + uint32_t cp = 0; + uint16_t gid = 0; + if (!cn16_read_index_entry(mid, &cp, &gid)) { + return false; + } + + if (cp == codepoint) { + size_t glyph_bytes = (size_t)(_binary_cn16_glyphs_bin_end - + _binary_cn16_glyphs_bin_start); + size_t off = (size_t)gid * CN16_GLYPH_BYTES; + if ((off + CN16_GLYPH_BYTES) > glyph_bytes) { + return false; + } + *out_glyph = _binary_cn16_glyphs_bin_start + off; + return true; + } + if (cp < codepoint) { + lo = mid + 1; + } else { + hi = mid; + } + } + + return false; +} + +static void draw_char(uint8_t *buf, + uint16_t width, + uint16_t height, + uint16_t x, + uint16_t y, + char ch, + uint8_t scale) { + if (ch < 32 || ch > 126) { + ch = '?'; + } + + const uint8_t *glyph = &font5x7[(ch - 32) * 5]; + for (uint8_t col = 0; col < 5; ++col) { + uint8_t bits = glyph[col]; + for (uint8_t row = 0; row < 7; ++row) { + if (((bits >> row) & 0x01u) == 0) { + continue; + } + for (uint8_t sx = 0; sx < scale; ++sx) { + for (uint8_t sy = 0; sy < scale; ++sy) { + uint16_t px = (uint16_t)(x + col * scale + sx); + uint16_t py = (uint16_t)(y + row * scale + sy); + if (py < height) { + set_black(buf, width, px, py); + } + } + } + } + } +} + +static void draw_cn16_glyph(uint8_t *buf, + uint16_t width, + uint16_t height, + uint16_t x, + uint16_t y, + const uint8_t *glyph, + uint8_t scale) { + if (glyph == NULL || scale == 0) { + return; + } + + for (uint8_t row = 0; row < 16; ++row) { + uint16_t bits = ((uint16_t)glyph[row * 2] << 8) | glyph[row * 2 + 1]; + for (uint8_t col = 0; col < 16; ++col) { + if ((bits & (uint16_t)(1u << (15 - col))) == 0) { + continue; + } + for (uint8_t sx = 0; sx < scale; ++sx) { + for (uint8_t sy = 0; sy < scale; ++sy) { + uint16_t px = (uint16_t)(x + col * scale + sx); + uint16_t py = (uint16_t)(y + row * scale + sy); + if (py < height && px < width) { + set_black(buf, width, px, py); + } + } + } + } + } +} + +static void draw_missing_box(uint8_t *buf, + uint16_t width, + uint16_t height, + uint16_t x, + uint16_t y, + uint8_t scale) { + uint16_t box = (uint16_t)(16 * scale); + for (uint16_t r = 0; r < box; ++r) { + for (uint16_t c = 0; c < box; ++c) { + bool edge = (r < scale) || (c < scale) || (r >= box - scale) || (c >= box - scale); + if (!edge) { + continue; + } + uint16_t px = (uint16_t)(x + c); + uint16_t py = (uint16_t)(y + r); + if (px < width && py < height) { + set_black(buf, width, px, py); + } + } + } +} + +static uint32_t utf8_decode_one(const unsigned char *s, size_t len, size_t *out_consumed) { + if (out_consumed == NULL || s == NULL || len == 0) { + return UTF8_REPLACEMENT_CODEPOINT; + } + + uint8_t b0 = s[0]; + if (b0 < 0x80) { + *out_consumed = 1; + return b0; + } + + if ((b0 & 0xE0u) == 0xC0u && len >= 2) { + uint8_t b1 = s[1]; + if ((b1 & 0xC0u) == 0x80u) { + uint32_t cp = ((uint32_t)(b0 & 0x1Fu) << 6) | + (uint32_t)(b1 & 0x3Fu); + if (cp >= 0x80u) { + *out_consumed = 2; + return cp; + } + } + } else if ((b0 & 0xF0u) == 0xE0u && len >= 3) { + uint8_t b1 = s[1]; + uint8_t b2 = s[2]; + if ((b1 & 0xC0u) == 0x80u && (b2 & 0xC0u) == 0x80u) { + uint32_t cp = ((uint32_t)(b0 & 0x0Fu) << 12) | + ((uint32_t)(b1 & 0x3Fu) << 6) | + (uint32_t)(b2 & 0x3Fu); + if (cp >= 0x800u && !(cp >= 0xD800u && cp <= 0xDFFFu)) { + *out_consumed = 3; + return cp; + } + } + } else if ((b0 & 0xF8u) == 0xF0u && len >= 4) { + uint8_t b1 = s[1]; + uint8_t b2 = s[2]; + uint8_t b3 = s[3]; + if ((b1 & 0xC0u) == 0x80u && (b2 & 0xC0u) == 0x80u && (b3 & 0xC0u) == 0x80u) { + uint32_t cp = ((uint32_t)(b0 & 0x07u) << 18) | + ((uint32_t)(b1 & 0x3Fu) << 12) | + ((uint32_t)(b2 & 0x3Fu) << 6) | + (uint32_t)(b3 & 0x3Fu); + if (cp >= 0x10000u && cp <= 0x10FFFFu) { + *out_consumed = 4; + return cp; + } + } + } + + *out_consumed = 1; + return UTF8_REPLACEMENT_CODEPOINT; +} + +esp_err_t raster_tools_render_text_384(const char *text, + uint8_t scale, + uint8_t line_spacing, + uint16_t max_height, + uint16_t *out_width, + uint16_t *out_height, + uint8_t **out_raster, + size_t *out_len, + char *err, + size_t err_len) { + if (text == NULL || out_width == NULL || out_height == NULL || + out_raster == NULL || out_len == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid args"); + } + return ESP_ERR_INVALID_ARG; + } + + if (scale == 0) { + scale = 1; + } + + if (max_height == 0 || max_height > 3000) { + max_height = 3000; + } + + size_t full_len = (size_t)RASTER_BYTES_PER_ROW * max_height; + uint8_t *buf = (uint8_t *)calloc(1, full_len); + if (buf == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "no memory"); + } + return ESP_ERR_NO_MEM; + } + + uint16_t cursor_x = 0; + uint16_t cursor_y = 0; + uint16_t ascii_w = (uint16_t)(6 * scale); + uint16_t ascii_h = (uint16_t)(7 * scale); + uint16_t cjk_w = (uint16_t)(16 * scale); + uint16_t cjk_h = (uint16_t)(16 * scale); + uint16_t line_h = (uint16_t)(cjk_h + line_spacing * scale); + if (line_h == 0) { + line_h = cjk_h; + } + + uint16_t used_bottom = 0; + bool truncated = false; + + const unsigned char *p = (const unsigned char *)text; + size_t remain = strlen(text); + while (remain > 0) { + size_t consumed = 0; + uint32_t cp = utf8_decode_one(p, remain, &consumed); + if (consumed == 0) { + break; + } + p += consumed; + remain -= consumed; + + if (cp == '\r') { + continue; + } + if (cp == '\n') { + cursor_x = 0; + cursor_y = (uint16_t)(cursor_y + line_h); + continue; + } + + bool is_ascii = cp >= 0x20u && cp <= 0x7Eu; + uint16_t draw_w = is_ascii ? ascii_w : cjk_w; + uint16_t draw_h = is_ascii ? ascii_h : cjk_h; + uint16_t draw_y = cursor_y; + if (is_ascii && cjk_h > ascii_h) { + draw_y = (uint16_t)(cursor_y + (cjk_h - ascii_h) / 2); + } + + if ((uint16_t)(cursor_x + draw_w) > RASTER_WIDTH) { + cursor_x = 0; + cursor_y = (uint16_t)(cursor_y + line_h); + draw_y = cursor_y; + if (is_ascii && cjk_h > ascii_h) { + draw_y = (uint16_t)(cursor_y + (cjk_h - ascii_h) / 2); + } + } + + if ((uint16_t)(draw_y + draw_h) > max_height) { + truncated = true; + break; + } + + if (is_ascii) { + draw_char(buf, RASTER_WIDTH, max_height, cursor_x, draw_y, (char)cp, scale); + } else { + const uint8_t *glyph = NULL; + if (cn16_lookup_glyph(cp, &glyph)) { + draw_cn16_glyph(buf, RASTER_WIDTH, max_height, cursor_x, draw_y, glyph, scale); + } else { + draw_missing_box(buf, RASTER_WIDTH, max_height, cursor_x, draw_y, scale); + } + } + cursor_x = (uint16_t)(cursor_x + draw_w); + + uint16_t bottom = (uint16_t)(draw_y + draw_h); + if (bottom > used_bottom) { + used_bottom = bottom; + } + } + + if (used_bottom == 0) { + used_bottom = cjk_h; + } + + if (used_bottom > max_height) { + used_bottom = max_height; + } + + size_t final_len = (size_t)RASTER_BYTES_PER_ROW * used_bottom; + uint8_t *final_buf = (uint8_t *)malloc(final_len); + if (final_buf == NULL) { + free(buf); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "no memory"); + } + return ESP_ERR_NO_MEM; + } + + memcpy(final_buf, buf, final_len); + free(buf); + + *out_width = RASTER_WIDTH; + *out_height = used_bottom; + *out_raster = final_buf; + *out_len = final_len; + + if (truncated && err != NULL && err_len > 0) { + snprintf(err, err_len, "text truncated due to max height"); + } + + return ESP_OK; +} diff --git a/main/domain/src/raster_tools_image_qr.c b/main/domain/src/raster_tools_image_qr.c new file mode 100644 index 0000000..a209645 --- /dev/null +++ b/main/domain/src/raster_tools_image_qr.c @@ -0,0 +1,258 @@ +#include "raster_tools.h" + +#include +#include +#include +#include + +#include "qrcodegen.h" + +#define RASTER_WIDTH 384 +#define RASTER_BYTES_PER_ROW (RASTER_WIDTH / 8) + +static void set_black(uint8_t *buf, uint16_t width, uint16_t x, uint16_t y) { + if (x >= width) { + return; + } + uint16_t bpr = width / 8; + size_t idx = (size_t)y * bpr + (x / 8); + uint8_t bit = (uint8_t)(7 - (x % 8)); + buf[idx] |= (uint8_t)(1u << bit); +} + +esp_err_t raster_tools_convert_gray8_to_raster_384(const uint8_t *gray, + uint16_t src_width, + uint16_t src_height, + bool scale_to_width, + uint8_t threshold, + bool invert, + uint16_t max_height, + uint16_t *out_width, + uint16_t *out_height, + uint8_t **out_raster, + size_t *out_len, + char *err, + size_t err_len) { + if (gray == NULL || src_width == 0 || src_height == 0 || + out_width == NULL || out_height == NULL || out_raster == NULL || out_len == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid args"); + } + return ESP_ERR_INVALID_ARG; + } + + if (max_height == 0 || max_height > 3000) { + max_height = 3000; + } + + uint16_t dst_width = RASTER_WIDTH; + uint16_t dst_height = src_height; + if (scale_to_width) { + if (src_width == 0) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid source width"); + } + return ESP_ERR_INVALID_ARG; + } + uint32_t scaled = ((uint32_t)src_height * RASTER_WIDTH + (src_width / 2u)) / src_width; + if (scaled == 0) { + scaled = 1; + } + if (scaled > max_height) { + scaled = max_height; + } + dst_height = (uint16_t)scaled; + } else if (src_width != RASTER_WIDTH) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "width must be 384 when scale_to_width=false"); + } + return ESP_ERR_INVALID_ARG; + } + + if (dst_height == 0 || dst_height > max_height) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid output height"); + } + return ESP_ERR_INVALID_SIZE; + } + + size_t len = (size_t)RASTER_BYTES_PER_ROW * dst_height; + uint8_t *raster = (uint8_t *)calloc(1, len); + if (raster == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "no memory"); + } + return ESP_ERR_NO_MEM; + } + + for (uint16_t y = 0; y < dst_height; ++y) { + uint16_t src_y = scale_to_width + ? (uint16_t)(((uint32_t)y * src_height) / dst_height) + : y; + if (src_y >= src_height) { + src_y = (uint16_t)(src_height - 1); + } + + for (uint16_t x = 0; x < RASTER_WIDTH; ++x) { + uint16_t src_x = scale_to_width + ? (uint16_t)(((uint32_t)x * src_width) / RASTER_WIDTH) + : x; + if (src_x >= src_width) { + src_x = (uint16_t)(src_width - 1); + } + + uint8_t v = gray[(size_t)src_y * src_width + src_x]; + bool black = invert ? (v > threshold) : (v < threshold); + if (black) { + set_black(raster, RASTER_WIDTH, x, y); + } + } + } + + *out_width = dst_width; + *out_height = dst_height; + *out_raster = raster; + *out_len = len; + return ESP_OK; +} + +static enum qrcodegen_Ecc map_qr_ecc(uint8_t ecc_level) { + switch (ecc_level) { + case 0: + return qrcodegen_Ecc_LOW; + case 1: + return qrcodegen_Ecc_MEDIUM; + case 2: + return qrcodegen_Ecc_QUARTILE; + case 3: + default: + return qrcodegen_Ecc_HIGH; + } +} + +esp_err_t raster_tools_render_qr_384(const char *text, + uint8_t module_scale, + uint8_t margin_modules, + uint8_t ecc_level, + uint16_t max_height, + uint16_t *out_width, + uint16_t *out_height, + uint8_t **out_raster, + size_t *out_len, + char *err, + size_t err_len) { + if (text == NULL || text[0] == '\0' || + out_width == NULL || out_height == NULL || out_raster == NULL || out_len == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid args"); + } + return ESP_ERR_INVALID_ARG; + } + + if (max_height == 0 || max_height > 3000) { + max_height = 3000; + } + + if (margin_modules == 0) { + margin_modules = 2; + } + + size_t qr_buf_len = qrcodegen_BUFFER_LEN_MAX; + uint8_t *temp = (uint8_t *)malloc(qr_buf_len); + uint8_t *qr = (uint8_t *)malloc(qr_buf_len); + if (temp == NULL || qr == NULL) { + free(temp); + free(qr); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "no memory"); + } + return ESP_ERR_NO_MEM; + } + + bool ok = qrcodegen_encodeText(text, + temp, + qr, + map_qr_ecc(ecc_level), + qrcodegen_VERSION_MIN, + qrcodegen_VERSION_MAX, + qrcodegen_Mask_AUTO, + true); + if (!ok) { + free(temp); + free(qr); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "qr encode failed"); + } + return ESP_FAIL; + } + + int qr_size = qrcodegen_getSize(qr); + int full_modules = qr_size + 2 * margin_modules; + + if (module_scale == 0) { + module_scale = (uint8_t)(RASTER_WIDTH / full_modules); + if (module_scale == 0) { + module_scale = 1; + } + } + + uint32_t image_size = (uint32_t)full_modules * module_scale; + if (image_size > RASTER_WIDTH || image_size > max_height) { + free(temp); + free(qr); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "qr too large for 384 width"); + } + return ESP_ERR_INVALID_SIZE; + } + + uint16_t out_h = (uint16_t)image_size; + size_t len = (size_t)RASTER_BYTES_PER_ROW * out_h; + uint8_t *raster = (uint8_t *)calloc(1, len); + if (raster == NULL) { + free(temp); + free(qr); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "no memory"); + } + return ESP_ERR_NO_MEM; + } + + uint16_t x_origin = (uint16_t)((RASTER_WIDTH - image_size) / 2); + for (int my = 0; my < full_modules; ++my) { + for (int mx = 0; mx < full_modules; ++mx) { + int qx = mx - margin_modules; + int qy = my - margin_modules; + bool black = (qx >= 0 && qx < qr_size && qy >= 0 && qy < qr_size) + ? qrcodegen_getModule(qr, qx, qy) + : false; + if (!black) { + continue; + } + + uint16_t px0 = (uint16_t)(x_origin + mx * module_scale); + uint16_t py0 = (uint16_t)(my * module_scale); + for (uint8_t sy = 0; sy < module_scale; ++sy) { + uint16_t py = (uint16_t)(py0 + sy); + if (py >= out_h) { + continue; + } + for (uint8_t sx = 0; sx < module_scale; ++sx) { + uint16_t px = (uint16_t)(px0 + sx); + if (px < RASTER_WIDTH) { + set_black(raster, RASTER_WIDTH, px, py); + } + } + } + } + } + + free(temp); + free(qr); + + *out_width = RASTER_WIDTH; + *out_height = out_h; + *out_raster = raster; + *out_len = len; + return ESP_OK; +} diff --git a/main/domain/src/system_runtime.c b/main/domain/src/system_runtime.c new file mode 100644 index 0000000..3529afa --- /dev/null +++ b/main/domain/src/system_runtime.c @@ -0,0 +1,20 @@ +#include "system_runtime.h" + +#include "platform_bootstrap.h" +#include "wifi_manager.h" + +esp_err_t system_runtime_bootstrap(void) { + esp_err_t err = platform_bootstrap_init(); + if (err != ESP_OK) { + return err; + } + return wifi_manager_start(); +} + +bool system_runtime_wifi_ready(void) { + return wifi_manager_is_ready(); +} + +void system_runtime_get_ip(char *buf, size_t buf_len) { + wifi_manager_get_ip(buf, buf_len); +} diff --git a/main/platform/README.md b/main/platform/README.md new file mode 100644 index 0000000..b571305 --- /dev/null +++ b/main/platform/README.md @@ -0,0 +1,13 @@ +# Platform Layer + +Purpose: hardware and system resource abstraction only. + +Allowed examples: +- Wi-Fi/BLE driver wrappers +- NVS/time/RTOS/system adapters +- board-specific IO and transport bindings + +Rules: +- Expose stable APIs in `include/`. +- Keep private headers in `internal/`. +- Do not depend on `domain`, `control_plane`, or `app_composition`. diff --git a/main/platform/include/.gitkeep b/main/platform/include/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/platform/include/ble_printer_client.h b/main/platform/include/ble_printer_client.h new file mode 100644 index 0000000..15a0f42 --- /dev/null +++ b/main/platform/include/ble_printer_client.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +#include "esp_err.h" + +typedef void (*ble_frame_rx_cb_t)(const uint8_t *data, size_t len); + +typedef struct { + bool connected; + bool notify_ready; + uint16_t mtu; +} ble_link_state_t; + +esp_err_t ble_printer_client_init(ble_frame_rx_cb_t rx_cb); + +esp_err_t ble_printer_client_connect(const char *target_name, uint32_t timeout_ms); +void ble_printer_client_disconnect(void); + +bool ble_printer_client_is_connected(void); +void ble_printer_client_get_link_state(ble_link_state_t *out_state); + +esp_err_t ble_printer_client_write(const uint8_t *data, size_t len); diff --git a/main/platform/include/platform_bootstrap.h b/main/platform/include/platform_bootstrap.h new file mode 100644 index 0000000..6587a10 --- /dev/null +++ b/main/platform/include/platform_bootstrap.h @@ -0,0 +1,5 @@ +#pragma once + +#include "esp_err.h" + +esp_err_t platform_bootstrap_init(void); diff --git a/main/platform/include/wifi_manager.h b/main/platform/include/wifi_manager.h new file mode 100644 index 0000000..09bd29c --- /dev/null +++ b/main/platform/include/wifi_manager.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include "esp_err.h" + +esp_err_t wifi_manager_start(void); + +bool wifi_manager_is_ready(void); + +void wifi_manager_get_ip(char *buf, size_t buf_len); diff --git a/main/platform/internal/.gitkeep b/main/platform/internal/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/platform/src/.gitkeep b/main/platform/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/platform/src/ble_printer_client.c b/main/platform/src/ble_printer_client.c new file mode 100644 index 0000000..970c8f7 --- /dev/null +++ b/main/platform/src/ble_printer_client.c @@ -0,0 +1,655 @@ +#include "ble_printer_client.h" + +#include +#include + +#include "esp_err.h" +#include "esp_log.h" +#include "esp_nimble_hci.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/semphr.h" +#include "host/ble_gatt.h" +#include "host/ble_hs.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "os/os_mbuf.h" +#include "services/gap/ble_svc_gap.h" +#include "services/gatt/ble_svc_gatt.h" + +#define PRINTER_SERVICE_UUID16 0xFFF0 +#define PRINTER_NOTIFY_UUID16 0xFFF1 +#define PRINTER_WRITE_UUID16 0xFFF2 +#define CCCD_UUID16 0x2902 + +#define EVT_CONNECTED BIT0 +#define EVT_READY BIT1 +#define EVT_FAILED BIT2 + +static const char *TAG = "ble_client"; + +static EventGroupHandle_t s_evt_group; +static SemaphoreHandle_t s_lock; + +static ble_frame_rx_cb_t s_rx_cb; + +static uint8_t s_addr_type; +static ble_addr_t s_target_addr; +static char s_target_name[32] = "lyfPrinter"; +static bool s_match_any_compatible; + +static uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE; +static uint16_t s_service_start; +static uint16_t s_service_end; +static uint16_t s_notify_val_handle; +static uint16_t s_write_val_handle; +static uint16_t s_cccd_handle; +static uint16_t s_mtu = 23; + +static bool s_scanning; +static bool s_notify_ready; +static bool s_host_synced; + +static uint16_t uuid16(const ble_uuid_t *uuid) { + if (uuid == NULL || uuid->type != BLE_UUID_TYPE_16) { + return 0; + } + return BLE_UUID16(uuid)->value; +} + +static bool adv_has_uuid16(const struct ble_hs_adv_fields *fields, uint16_t target_uuid) { + if (fields == NULL || fields->uuids16 == NULL || fields->num_uuids16 == 0) { + return false; + } + for (uint8_t i = 0; i < fields->num_uuids16; ++i) { + if (fields->uuids16[i].value == target_uuid) { + return true; + } + } + return false; +} + +static size_t normalize_name(const char *in, char *out, size_t out_cap) { + if (in == NULL || out == NULL || out_cap == 0) { + return 0; + } + + const char *start = in; + while (*start != '\0' && isspace((unsigned char)*start)) { + ++start; + } + if (*start == '"' || *start == '\'') { + ++start; + } + + size_t len = strlen(start); + while (len > 0 && isspace((unsigned char)start[len - 1])) { + --len; + } + if (len > 0 && (start[len - 1] == '"' || start[len - 1] == '\'')) { + --len; + } + + size_t out_len = 0; + for (size_t i = 0; i < len && out_len + 1 < out_cap; ++i) { + unsigned char c = (unsigned char)start[i]; + if (isupper(c)) { + c = (unsigned char)tolower(c); + } + out[out_len++] = (char)c; + } + out[out_len] = '\0'; + return out_len; +} + +static bool normalized_name_match(const char *adv_name, const char *target_name) { + char adv[40] = {0}; + char target[40] = {0}; + size_t adv_len = normalize_name(adv_name, adv, sizeof(adv)); + size_t target_len = normalize_name(target_name, target, sizeof(target)); + if (adv_len == 0 || target_len == 0) { + return false; + } + if (strcmp(adv, target) == 0) { + return true; + } + // Be tolerant of shortened or prefixed names. + return (strstr(adv, target) != NULL) || (strstr(target, adv) != NULL); +} + +static void reset_discovery_state(void) { + s_service_start = 0; + s_service_end = 0; + s_notify_val_handle = 0; + s_write_val_handle = 0; + s_cccd_handle = 0; + s_notify_ready = false; + s_mtu = 23; +} + +static void signal_failure(void) { + xEventGroupSetBits(s_evt_group, EVT_FAILED); +} + +static void signal_ready(void) { + xEventGroupSetBits(s_evt_group, EVT_READY); +} + +static int dsc_disc_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + uint16_t chr_val_handle, + const struct ble_gatt_dsc *dsc, + void *arg); + +static int chr_disc_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + const struct ble_gatt_chr *chr, + void *arg); + +static int svc_disc_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + const struct ble_gatt_svc *service, + void *arg); + +static int cccd_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) { + (void)conn_handle; + (void)attr; + (void)arg; + + if (error->status != 0) { + ESP_LOGE(TAG, "CCCD write failed status=%d", error->status); + signal_failure(); + return 0; + } + + s_notify_ready = true; + ESP_LOGI(TAG, "Notify subscription enabled"); + signal_ready(); + return 0; +} + +static int dsc_disc_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + uint16_t chr_val_handle, + const struct ble_gatt_dsc *dsc, + void *arg) { + (void)chr_val_handle; + (void)arg; + + if (error->status == 0 && dsc != NULL) { + if (uuid16(&dsc->uuid.u) == CCCD_UUID16) { + s_cccd_handle = dsc->handle; + ESP_LOGI(TAG, "Found CCCD handle=%u", s_cccd_handle); + } + return 0; + } + + if (error->status == BLE_HS_EDONE) { + if (s_cccd_handle == 0) { + ESP_LOGW(TAG, "CCCD not found, fallback to val_handle+1"); + s_cccd_handle = s_notify_val_handle + 1; + } + + uint8_t cccd[2] = {0x01, 0x00}; + int rc = ble_gattc_write_flat(conn_handle, s_cccd_handle, cccd, sizeof(cccd), cccd_write_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "ble_gattc_write_flat CCCD failed rc=%d", rc); + signal_failure(); + } + return 0; + } + + ESP_LOGE(TAG, "Descriptor discovery failed status=%d", error->status); + signal_failure(); + return 0; +} + +static int chr_disc_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + const struct ble_gatt_chr *chr, + void *arg) { + (void)arg; + + if (error->status == 0 && chr != NULL) { + uint16_t id = uuid16(&chr->uuid.u); + if (id == PRINTER_NOTIFY_UUID16) { + s_notify_val_handle = chr->val_handle; + ESP_LOGI(TAG, "Found notify char handle=%u", s_notify_val_handle); + } else if (id == PRINTER_WRITE_UUID16) { + s_write_val_handle = chr->val_handle; + ESP_LOGI(TAG, "Found write char handle=%u", s_write_val_handle); + } + return 0; + } + + if (error->status == BLE_HS_EDONE) { + if (s_notify_val_handle == 0 || s_write_val_handle == 0) { + ESP_LOGE(TAG, + "Characteristic discovery incomplete notify=%u write=%u", + s_notify_val_handle, + s_write_val_handle); + signal_failure(); + return 0; + } + + uint16_t end_handle = (s_write_val_handle > s_notify_val_handle) + ? (uint16_t)(s_write_val_handle - 1) + : s_service_end; + + if (end_handle <= s_notify_val_handle) { + end_handle = s_service_end; + } + + int rc = ble_gattc_disc_all_dscs(conn_handle, + s_notify_val_handle, + end_handle, + dsc_disc_cb, + NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Descriptor discovery start failed rc=%d", rc); + signal_failure(); + } + return 0; + } + + ESP_LOGE(TAG, "Characteristic discovery failed status=%d", error->status); + signal_failure(); + return 0; +} + +static int svc_disc_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + const struct ble_gatt_svc *service, + void *arg) { + (void)arg; + + if (error->status == 0 && service != NULL) { + if (uuid16(&service->uuid.u) == PRINTER_SERVICE_UUID16) { + s_service_start = service->start_handle; + s_service_end = service->end_handle; + ESP_LOGI(TAG, "Found printer service [%u, %u]", s_service_start, s_service_end); + } + return 0; + } + + if (error->status == BLE_HS_EDONE) { + uint16_t start = s_service_start; + uint16_t end = s_service_end; + if (start == 0 || end == 0) { + // Android app searches all services/chars for FFF1/FFF2. + // Use a global fallback range for broader compatibility. + start = 1; + end = 0xFFFF; + ESP_LOGW(TAG, "Printer service 0x%04x not found; fallback char discovery in full handle range", PRINTER_SERVICE_UUID16); + } + + int rc = ble_gattc_disc_all_chrs(conn_handle, + start, + end, + chr_disc_cb, + NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Start characteristic discovery failed rc=%d", rc); + signal_failure(); + } + return 0; + } + + ESP_LOGE(TAG, "Service discovery failed status=%d", error->status); + signal_failure(); + return 0; +} + +static void start_service_discovery(void) { + int rc = ble_gattc_disc_all_svcs(s_conn_handle, svc_disc_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "ble_gattc_disc_all_svcs failed rc=%d", rc); + signal_failure(); + } +} + +static void start_scan(void); + +static void stop_scan_if_running(void) { + if (s_scanning) { + ble_gap_disc_cancel(); + s_scanning = false; + } +} + +static int gap_event_cb(struct ble_gap_event *event, void *arg) { + (void)arg; + + switch (event->type) { + case BLE_GAP_EVENT_DISC: { + if (!s_scanning) { + return 0; + } + struct ble_hs_adv_fields fields; + memset(&fields, 0, sizeof(fields)); + + int rc = ble_hs_adv_parse_fields(&fields, + event->disc.data, + event->disc.length_data); + if (rc != 0) { + return 0; + } + + char name[32] = {0}; + bool has_name = (fields.name_len > 0 && fields.name != NULL); + if (has_name) { + size_t n = fields.name_len < sizeof(name) - 1 ? fields.name_len : sizeof(name) - 1; + memcpy(name, fields.name, n); + } + + if (s_match_any_compatible) { + bool uuid_match = adv_has_uuid16(&fields, PRINTER_SERVICE_UUID16); + bool name_match = has_name && normalized_name_match(name, "lyfPrinter"); + if (!uuid_match && !name_match) { + return 0; + } + ESP_LOGI(TAG, + "Found compatible device '%s' RSSI=%d (uuid_match=%d)", + has_name ? name : "", + event->disc.rssi, + uuid_match ? 1 : 0); + } else { + if (!has_name || !normalized_name_match(name, s_target_name)) { + return 0; + } + ESP_LOGI(TAG, "Found target device '%s' RSSI=%d", name, event->disc.rssi); + } + + s_target_addr = event->disc.addr; + stop_scan_if_running(); + + struct ble_gap_conn_params conn_params; + memset(&conn_params, 0, sizeof(conn_params)); + conn_params.scan_itvl = 0x0010; + conn_params.scan_window = 0x0010; + conn_params.itvl_min = 0x0018; + conn_params.itvl_max = 0x0028; + conn_params.latency = 0; + conn_params.supervision_timeout = 0x0100; + conn_params.min_ce_len = 0x0010; + conn_params.max_ce_len = 0x0300; + + rc = ble_gap_connect(s_addr_type, + &s_target_addr, + 30000, + &conn_params, + gap_event_cb, + NULL); + if (rc != 0) { + ESP_LOGE(TAG, "ble_gap_connect failed rc=%d", rc); + signal_failure(); + } else { + ESP_LOGI(TAG, "Connecting..."); + } + + return 0; + } + + case BLE_GAP_EVENT_DISC_COMPLETE: + s_scanning = false; + return 0; + + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + s_conn_handle = event->connect.conn_handle; + xEventGroupSetBits(s_evt_group, EVT_CONNECTED); + ESP_LOGI(TAG, "Connected handle=%u", s_conn_handle); + + ble_gattc_exchange_mtu(s_conn_handle, NULL, NULL); + start_service_discovery(); + } else { + ESP_LOGE(TAG, "Connect failed status=%d", event->connect.status); + signal_failure(); + } + return 0; + + case BLE_GAP_EVENT_DISCONNECT: + ESP_LOGW(TAG, "Disconnected reason=%d", event->disconnect.reason); + s_conn_handle = BLE_HS_CONN_HANDLE_NONE; + s_notify_ready = false; + reset_discovery_state(); + return 0; + + case BLE_GAP_EVENT_MTU: + s_mtu = event->mtu.value; + ESP_LOGI(TAG, "MTU updated to %u", s_mtu); + return 0; + + case BLE_GAP_EVENT_NOTIFY_RX: { + if (event->notify_rx.om == NULL || s_rx_cb == NULL) { + return 0; + } + + uint16_t data_len = OS_MBUF_PKTLEN(event->notify_rx.om); + if (data_len == 0) { + return 0; + } + + uint8_t buf[256]; + uint16_t out_len = 0; + if (data_len > sizeof(buf)) { + ESP_LOGW(TAG, "Notify packet too large len=%u", data_len); + return 0; + } + + int rc = ble_hs_mbuf_to_flat(event->notify_rx.om, buf, sizeof(buf), &out_len); + if (rc != 0) { + ESP_LOGW(TAG, "ble_hs_mbuf_to_flat notify failed rc=%d", rc); + return 0; + } + + s_rx_cb(buf, out_len); + return 0; + } + + default: + return 0; + } +} + +static void start_scan(void) { + struct ble_gap_disc_params params; + memset(¶ms, 0, sizeof(params)); + // Keep duplicates so we can receive scan response updates carrying full local name. + params.filter_duplicates = 0; + params.passive = 0; + params.itvl = 0x0010; + params.window = 0x0010; + + stop_scan_if_running(); + + int rc = ble_gap_disc(s_addr_type, BLE_HS_FOREVER, ¶ms, gap_event_cb, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "ble_gap_disc failed rc=%d", rc); + signal_failure(); + return; + } + + s_scanning = true; + if (s_match_any_compatible) { + ESP_LOGI(TAG, "Scanning for compatible printer (service 0x%04x)", PRINTER_SERVICE_UUID16); + } else { + ESP_LOGI(TAG, "Scanning for %s", s_target_name); + } +} + +static void ble_on_reset(int reason) { + ESP_LOGE(TAG, "BLE reset reason=%d", reason); +} + +static void ble_on_sync(void) { + int rc = ble_hs_id_infer_auto(0, &s_addr_type); + if (rc != 0) { + ESP_LOGE(TAG, "ble_hs_id_infer_auto failed rc=%d", rc); + } + s_host_synced = true; +} + +static void nimble_host_task(void *param) { + (void)param; + nimble_port_run(); + nimble_port_freertos_deinit(); +} + +esp_err_t ble_printer_client_init(ble_frame_rx_cb_t rx_cb) { + s_rx_cb = rx_cb; + + s_evt_group = xEventGroupCreate(); + if (s_evt_group == NULL) { + return ESP_ERR_NO_MEM; + } + + s_lock = xSemaphoreCreateMutex(); + if (s_lock == NULL) { + return ESP_ERR_NO_MEM; + } + + esp_err_t err = nimble_port_init(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + return err; + } + ble_hs_cfg.reset_cb = ble_on_reset; + ble_hs_cfg.sync_cb = ble_on_sync; + + ble_svc_gap_init(); + ble_svc_gatt_init(); + + nimble_port_freertos_init(nimble_host_task); + + reset_discovery_state(); + return ESP_OK; +} + +esp_err_t ble_printer_client_connect(const char *target_name, uint32_t timeout_ms) { + s_match_any_compatible = false; + if (target_name != NULL && target_name[0] != '\0') { + if (strcmp(target_name, "*") == 0) { + s_match_any_compatible = true; + } else { + strlcpy(s_target_name, target_name, sizeof(s_target_name)); + } + } + + if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(3000)) != pdTRUE) { + return ESP_ERR_TIMEOUT; + } + + if (s_conn_handle != BLE_HS_CONN_HANDLE_NONE && s_notify_ready) { + xSemaphoreGive(s_lock); + return ESP_OK; + } + + xEventGroupClearBits(s_evt_group, EVT_CONNECTED | EVT_READY | EVT_FAILED); + reset_discovery_state(); + + int wait_sync_ms = 3000; + while (!s_host_synced && wait_sync_ms > 0) { + vTaskDelay(pdMS_TO_TICKS(20)); + wait_sync_ms -= 20; + } + if (!s_host_synced) { + xSemaphoreGive(s_lock); + return ESP_ERR_INVALID_STATE; + } + + start_scan(); + xSemaphoreGive(s_lock); + + EventBits_t bits = xEventGroupWaitBits(s_evt_group, + EVT_READY | EVT_FAILED, + pdFALSE, + pdFALSE, + pdMS_TO_TICKS(timeout_ms)); + + if (bits & EVT_READY) { + return ESP_OK; + } + if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(500)) == pdTRUE) { + stop_scan_if_running(); + xSemaphoreGive(s_lock); + } + + // Similar to Android app behavior: quick retry when initial attempt fails or times out. + if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(1000)) == pdTRUE) { + xEventGroupClearBits(s_evt_group, EVT_CONNECTED | EVT_READY | EVT_FAILED); + reset_discovery_state(); + start_scan(); + xSemaphoreGive(s_lock); + } + + bits = xEventGroupWaitBits(s_evt_group, + EVT_READY | EVT_FAILED, + pdFALSE, + pdFALSE, + pdMS_TO_TICKS(timeout_ms)); + if (bits & EVT_READY) { + return ESP_OK; + } + if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(500)) == pdTRUE) { + stop_scan_if_running(); + xSemaphoreGive(s_lock); + } + if (bits & EVT_FAILED) { + return ESP_FAIL; + } + return ESP_ERR_TIMEOUT; +} + +void ble_printer_client_disconnect(void) { + if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(2000)) != pdTRUE) { + return; + } + + stop_scan_if_running(); + + if (s_conn_handle != BLE_HS_CONN_HANDLE_NONE) { + ble_gap_terminate(s_conn_handle, BLE_ERR_REM_USER_CONN_TERM); + } + + s_conn_handle = BLE_HS_CONN_HANDLE_NONE; + reset_discovery_state(); + xSemaphoreGive(s_lock); +} + +bool ble_printer_client_is_connected(void) { + return (s_conn_handle != BLE_HS_CONN_HANDLE_NONE) && s_notify_ready; +} + +void ble_printer_client_get_link_state(ble_link_state_t *out_state) { + if (out_state == NULL) { + return; + } + + out_state->connected = (s_conn_handle != BLE_HS_CONN_HANDLE_NONE); + out_state->notify_ready = s_notify_ready; + out_state->mtu = s_mtu; +} + +esp_err_t ble_printer_client_write(const uint8_t *data, size_t len) { + if (data == NULL || len == 0) { + return ESP_ERR_INVALID_ARG; + } + + if (!ble_printer_client_is_connected() || s_write_val_handle == 0) { + return ESP_ERR_INVALID_STATE; + } + + int rc = ble_gattc_write_no_rsp_flat(s_conn_handle, + s_write_val_handle, + data, + len); + if (rc != 0) { + ESP_LOGW(TAG, "write_no_rsp failed rc=%d", rc); + return ESP_FAIL; + } + + return ESP_OK; +} diff --git a/main/platform/src/platform_bootstrap.c b/main/platform/src/platform_bootstrap.c new file mode 100644 index 0000000..542f493 --- /dev/null +++ b/main/platform/src/platform_bootstrap.c @@ -0,0 +1,23 @@ +#include "platform_bootstrap.h" + +#include + +#include "nvs_flash.h" + +static bool s_initialized; + +esp_err_t platform_bootstrap_init(void) { + if (s_initialized) { + return ESP_OK; + } + + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK(err); + + s_initialized = true; + return ESP_OK; +} diff --git a/main/platform/src/wifi_manager.c b/main/platform/src/wifi_manager.c new file mode 100644 index 0000000..a236cbb --- /dev/null +++ b/main/platform/src/wifi_manager.c @@ -0,0 +1,144 @@ +#include "wifi_manager.h" + +#include +#include + +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" + +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +static const char *TAG = "wifi_manager"; + +static EventGroupHandle_t s_wifi_event_group; +static int s_retry_num; +static bool s_ready; +static esp_netif_t *s_sta_netif; + +static void wifi_event_handler(void *arg, + esp_event_base_t event_base, + int32_t event_id, + void *event_data) { + (void)arg; + + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + return; + } + + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + if (s_retry_num < CONFIG_LYF_WIFI_MAXIMUM_RETRY) { + esp_wifi_connect(); + s_retry_num++; + ESP_LOGW(TAG, "retry to connect to the AP (%d/%d)", s_retry_num, CONFIG_LYF_WIFI_MAXIMUM_RETRY); + } else { + xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); + } + return; + } + + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; + ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip)); + s_retry_num = 0; + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + return; + } +} + +static esp_err_t start_sta_mode(void) { + wifi_config_t wifi_config = {0}; + if (strlen(CONFIG_LYF_WIFI_SSID) == 0) { + ESP_LOGE(TAG, "CONFIG_LYF_WIFI_SSID is empty; STA-only mode requires valid SSID"); + return ESP_ERR_INVALID_STATE; + } + + strlcpy((char *)wifi_config.sta.ssid, CONFIG_LYF_WIFI_SSID, sizeof(wifi_config.sta.ssid)); + strlcpy((char *)wifi_config.sta.password, CONFIG_LYF_WIFI_PASSWORD, sizeof(wifi_config.sta.password)); + wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; + wifi_config.sta.pmf_cfg.capable = true; + wifi_config.sta.pmf_cfg.required = false; + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + + ESP_LOGI(TAG, "wifi_init_sta finished"); + + EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, + pdFALSE, + pdMS_TO_TICKS(15000)); + + if (bits & WIFI_CONNECTED_BIT) { + ESP_LOGI(TAG, "connected to AP SSID:%s", CONFIG_LYF_WIFI_SSID); + s_ready = true; + return ESP_OK; + } + + if (bits & WIFI_FAIL_BIT) { + ESP_LOGW(TAG, "Failed to connect to SSID:%s", CONFIG_LYF_WIFI_SSID); + return ESP_FAIL; + } + + ESP_LOGW(TAG, "Wi-Fi connect timeout"); + return ESP_ERR_TIMEOUT; +} + +esp_err_t wifi_manager_start(void) { + s_wifi_event_group = xEventGroupCreate(); + if (s_wifi_event_group == NULL) { + return ESP_ERR_NO_MEM; + } + + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + s_sta_netif = esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, + ESP_EVENT_ANY_ID, + &wifi_event_handler, + NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, + IP_EVENT_STA_GOT_IP, + &wifi_event_handler, + NULL)); + + esp_err_t err = start_sta_mode(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "STA-only mode failed to connect (%s)", esp_err_to_name(err)); + } + return err; +} + +bool wifi_manager_is_ready(void) { + return s_ready; +} + +void wifi_manager_get_ip(char *buf, size_t buf_len) { + if (buf == NULL || buf_len == 0) { + return; + } + + buf[0] = '\0'; + + esp_netif_ip_info_t ip_info; + memset(&ip_info, 0, sizeof(ip_info)); + + if (s_sta_netif != NULL && esp_netif_get_ip_info(s_sta_netif, &ip_info) == ESP_OK) { + snprintf(buf, buf_len, IPSTR, IP2STR(&ip_info.ip)); + return; + } + + strlcpy(buf, "0.0.0.0", buf_len); +} diff --git a/main/third_party/qrcodegen.c b/main/third_party/qrcodegen.c new file mode 100644 index 0000000..34f1002 --- /dev/null +++ b/main/third_party/qrcodegen.c @@ -0,0 +1,1027 @@ +/* + * QR Code generator library (C) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#include +#include +#include +#include +#include "qrcodegen.h" + +#ifndef QRCODEGEN_TEST + #define testable static // Keep functions private +#else + #define testable // Expose private functions +#endif + + +/*---- Forward declarations for private functions ----*/ + +// Regarding all public and private functions defined in this source file: +// - They require all pointer/array arguments to be not null unless the array length is zero. +// - They only read input scalar/array arguments, write to output pointer/array +// arguments, and return scalar values; they are "pure" functions. +// - They don't read mutable global variables or write to any global variables. +// - They don't perform I/O, read the clock, print to console, etc. +// - They allocate a small and constant amount of stack memory. +// - They don't allocate or free any memory on the heap. +// - They don't recurse or mutually recurse. All the code +// could be inlined into the top-level public functions. +// - They run in at most quadratic time with respect to input arguments. +// Most functions run in linear time, and some in constant time. +// There are no unbounded loops or non-obvious termination conditions. +// - They are completely thread-safe if the caller does not give the +// same writable buffer to concurrent calls to these functions. + +testable void appendBitsToBuffer(unsigned int val, int numBits, uint8_t buffer[], int *bitLen); + +testable void addEccAndInterleave(uint8_t data[], int version, enum qrcodegen_Ecc ecl, uint8_t result[]); +testable int getNumDataCodewords(int version, enum qrcodegen_Ecc ecl); +testable int getNumRawDataModules(int ver); + +testable void reedSolomonComputeDivisor(int degree, uint8_t result[]); +testable void reedSolomonComputeRemainder(const uint8_t data[], int dataLen, + const uint8_t generator[], int degree, uint8_t result[]); +testable uint8_t reedSolomonMultiply(uint8_t x, uint8_t y); + +testable void initializeFunctionModules(int version, uint8_t qrcode[]); +static void drawLightFunctionModules(uint8_t qrcode[], int version); +static void drawFormatBits(enum qrcodegen_Ecc ecl, enum qrcodegen_Mask mask, uint8_t qrcode[]); +testable int getAlignmentPatternPositions(int version, uint8_t result[7]); +static void fillRectangle(int left, int top, int width, int height, uint8_t qrcode[]); + +static void drawCodewords(const uint8_t data[], int dataLen, uint8_t qrcode[]); +static void applyMask(const uint8_t functionModules[], uint8_t qrcode[], enum qrcodegen_Mask mask); +static long getPenaltyScore(const uint8_t qrcode[]); +static int finderPenaltyCountPatterns(const int runHistory[7], int qrsize); +static int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, int runHistory[7], int qrsize); +static void finderPenaltyAddHistory(int currentRunLength, int runHistory[7], int qrsize); + +testable bool getModuleBounded(const uint8_t qrcode[], int x, int y); +testable void setModuleBounded(uint8_t qrcode[], int x, int y, bool isDark); +testable void setModuleUnbounded(uint8_t qrcode[], int x, int y, bool isDark); +static bool getBit(int x, int i); + +testable int calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars); +testable int getTotalBits(const struct qrcodegen_Segment segs[], size_t len, int version); +static int numCharCountBits(enum qrcodegen_Mode mode, int version); + + + +/*---- Private tables of constants ----*/ + +// The set of all legal characters in alphanumeric mode, where each character +// value maps to the index in the string. For checking text and encoding segments. +static const char *ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; + +// Sentinel value for use in only some functions. +#define LENGTH_OVERFLOW -1 + +// For generating error correction codes. +testable const int8_t ECC_CODEWORDS_PER_BLOCK[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low + {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium + {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile + {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High +}; + +#define qrcodegen_REED_SOLOMON_DEGREE_MAX 30 // Based on the table above + +// For generating error correction codes. +testable const int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low + {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium + {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile + {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High +}; + +// For automatic mask pattern selection. +static const int PENALTY_N1 = 3; +static const int PENALTY_N2 = 3; +static const int PENALTY_N3 = 40; +static const int PENALTY_N4 = 10; + + + +/*---- High-level QR Code encoding functions ----*/ + +// Public function - see documentation comment in header file. +bool qrcodegen_encodeText(const char *text, uint8_t tempBuffer[], uint8_t qrcode[], + enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl) { + + size_t textLen = strlen(text); + if (textLen == 0) + return qrcodegen_encodeSegmentsAdvanced(NULL, 0, ecl, minVersion, maxVersion, mask, boostEcl, tempBuffer, qrcode); + size_t bufLen = (size_t)qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion); + + struct qrcodegen_Segment seg; + if (qrcodegen_isNumeric(text)) { + if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_NUMERIC, textLen) > bufLen) + goto fail; + seg = qrcodegen_makeNumeric(text, tempBuffer); + } else if (qrcodegen_isAlphanumeric(text)) { + if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_ALPHANUMERIC, textLen) > bufLen) + goto fail; + seg = qrcodegen_makeAlphanumeric(text, tempBuffer); + } else { + if (textLen > bufLen) + goto fail; + for (size_t i = 0; i < textLen; i++) + tempBuffer[i] = (uint8_t)text[i]; + seg.mode = qrcodegen_Mode_BYTE; + seg.bitLength = calcSegmentBitLength(seg.mode, textLen); + if (seg.bitLength == LENGTH_OVERFLOW) + goto fail; + seg.numChars = (int)textLen; + seg.data = tempBuffer; + } + return qrcodegen_encodeSegmentsAdvanced(&seg, 1, ecl, minVersion, maxVersion, mask, boostEcl, tempBuffer, qrcode); + +fail: + qrcode[0] = 0; // Set size to invalid value for safety + return false; +} + + +// Public function - see documentation comment in header file. +bool qrcodegen_encodeBinary(uint8_t dataAndTemp[], size_t dataLen, uint8_t qrcode[], + enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl) { + + struct qrcodegen_Segment seg; + seg.mode = qrcodegen_Mode_BYTE; + seg.bitLength = calcSegmentBitLength(seg.mode, dataLen); + if (seg.bitLength == LENGTH_OVERFLOW) { + qrcode[0] = 0; // Set size to invalid value for safety + return false; + } + seg.numChars = (int)dataLen; + seg.data = dataAndTemp; + return qrcodegen_encodeSegmentsAdvanced(&seg, 1, ecl, minVersion, maxVersion, mask, boostEcl, dataAndTemp, qrcode); +} + + +// Appends the given number of low-order bits of the given value to the given byte-based +// bit buffer, increasing the bit length. Requires 0 <= numBits <= 16 and val < 2^numBits. +testable void appendBitsToBuffer(unsigned int val, int numBits, uint8_t buffer[], int *bitLen) { + assert(0 <= numBits && numBits <= 16 && (unsigned long)val >> numBits == 0); + for (int i = numBits - 1; i >= 0; i--, (*bitLen)++) + buffer[*bitLen >> 3] |= ((val >> i) & 1) << (7 - (*bitLen & 7)); +} + + + +/*---- Low-level QR Code encoding functions ----*/ + +// Public function - see documentation comment in header file. +bool qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[], size_t len, + enum qrcodegen_Ecc ecl, uint8_t tempBuffer[], uint8_t qrcode[]) { + return qrcodegen_encodeSegmentsAdvanced(segs, len, ecl, + qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true, tempBuffer, qrcode); +} + + +// Public function - see documentation comment in header file. +bool qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[], size_t len, enum qrcodegen_Ecc ecl, + int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl, uint8_t tempBuffer[], uint8_t qrcode[]) { + assert(segs != NULL || len == 0); + assert(qrcodegen_VERSION_MIN <= minVersion && minVersion <= maxVersion && maxVersion <= qrcodegen_VERSION_MAX); + assert(0 <= (int)ecl && (int)ecl <= 3 && -1 <= (int)mask && (int)mask <= 7); + + // Find the minimal version number to use + int version, dataUsedBits; + for (version = minVersion; ; version++) { + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available + dataUsedBits = getTotalBits(segs, len, version); + if (dataUsedBits != LENGTH_OVERFLOW && dataUsedBits <= dataCapacityBits) + break; // This version number is found to be suitable + if (version >= maxVersion) { // All versions in the range could not fit the given data + qrcode[0] = 0; // Set size to invalid value for safety + return false; + } + } + assert(dataUsedBits != LENGTH_OVERFLOW); + + // Increase the error correction level while the data still fits in the current version number + for (int i = (int)qrcodegen_Ecc_MEDIUM; i <= (int)qrcodegen_Ecc_HIGH; i++) { // From low to high + if (boostEcl && dataUsedBits <= getNumDataCodewords(version, (enum qrcodegen_Ecc)i) * 8) + ecl = (enum qrcodegen_Ecc)i; + } + + // Concatenate all segments to create the data bit string + memset(qrcode, 0, (size_t)qrcodegen_BUFFER_LEN_FOR_VERSION(version) * sizeof(qrcode[0])); + int bitLen = 0; + for (size_t i = 0; i < len; i++) { + const struct qrcodegen_Segment *seg = &segs[i]; + appendBitsToBuffer((unsigned int)seg->mode, 4, qrcode, &bitLen); + appendBitsToBuffer((unsigned int)seg->numChars, numCharCountBits(seg->mode, version), qrcode, &bitLen); + for (int j = 0; j < seg->bitLength; j++) { + int bit = (seg->data[j >> 3] >> (7 - (j & 7))) & 1; + appendBitsToBuffer((unsigned int)bit, 1, qrcode, &bitLen); + } + } + assert(bitLen == dataUsedBits); + + // Add terminator and pad up to a byte if applicable + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; + assert(bitLen <= dataCapacityBits); + int terminatorBits = dataCapacityBits - bitLen; + if (terminatorBits > 4) + terminatorBits = 4; + appendBitsToBuffer(0, terminatorBits, qrcode, &bitLen); + appendBitsToBuffer(0, (8 - bitLen % 8) % 8, qrcode, &bitLen); + assert(bitLen % 8 == 0); + + // Pad with alternating bytes until data capacity is reached + for (uint8_t padByte = 0xEC; bitLen < dataCapacityBits; padByte ^= 0xEC ^ 0x11) + appendBitsToBuffer(padByte, 8, qrcode, &bitLen); + + // Compute ECC, draw modules + addEccAndInterleave(qrcode, version, ecl, tempBuffer); + initializeFunctionModules(version, qrcode); + drawCodewords(tempBuffer, getNumRawDataModules(version) / 8, qrcode); + drawLightFunctionModules(qrcode, version); + initializeFunctionModules(version, tempBuffer); + + // Do masking + if (mask == qrcodegen_Mask_AUTO) { // Automatically choose best mask + long minPenalty = LONG_MAX; + for (int i = 0; i < 8; i++) { + enum qrcodegen_Mask msk = (enum qrcodegen_Mask)i; + applyMask(tempBuffer, qrcode, msk); + drawFormatBits(ecl, msk, qrcode); + long penalty = getPenaltyScore(qrcode); + if (penalty < minPenalty) { + mask = msk; + minPenalty = penalty; + } + applyMask(tempBuffer, qrcode, msk); // Undoes the mask due to XOR + } + } + assert(0 <= (int)mask && (int)mask <= 7); + applyMask(tempBuffer, qrcode, mask); // Apply the final choice of mask + drawFormatBits(ecl, mask, qrcode); // Overwrite old format bits + return true; +} + + + +/*---- Error correction code generation functions ----*/ + +// Appends error correction bytes to each block of the given data array, then interleaves +// bytes from the blocks and stores them in the result array. data[0 : dataLen] contains +// the input data. data[dataLen : rawCodewords] is used as a temporary work area and will +// be clobbered by this function. The final answer is stored in result[0 : rawCodewords]. +testable void addEccAndInterleave(uint8_t data[], int version, enum qrcodegen_Ecc ecl, uint8_t result[]) { + // Calculate parameter numbers + assert(0 <= (int)ecl && (int)ecl < 4 && qrcodegen_VERSION_MIN <= version && version <= qrcodegen_VERSION_MAX); + int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[(int)ecl][version]; + int blockEccLen = ECC_CODEWORDS_PER_BLOCK [(int)ecl][version]; + int rawCodewords = getNumRawDataModules(version) / 8; + int dataLen = getNumDataCodewords(version, ecl); + int numShortBlocks = numBlocks - rawCodewords % numBlocks; + int shortBlockDataLen = rawCodewords / numBlocks - blockEccLen; + + // Split data into blocks, calculate ECC, and interleave + // (not concatenate) the bytes into a single sequence + uint8_t rsdiv[qrcodegen_REED_SOLOMON_DEGREE_MAX]; + reedSolomonComputeDivisor(blockEccLen, rsdiv); + const uint8_t *dat = data; + for (int i = 0; i < numBlocks; i++) { + int datLen = shortBlockDataLen + (i < numShortBlocks ? 0 : 1); + uint8_t *ecc = &data[dataLen]; // Temporary storage + reedSolomonComputeRemainder(dat, datLen, rsdiv, blockEccLen, ecc); + for (int j = 0, k = i; j < datLen; j++, k += numBlocks) { // Copy data + if (j == shortBlockDataLen) + k -= numShortBlocks; + result[k] = dat[j]; + } + for (int j = 0, k = dataLen + i; j < blockEccLen; j++, k += numBlocks) // Copy ECC + result[k] = ecc[j]; + dat += datLen; + } +} + + +// Returns the number of 8-bit codewords that can be used for storing data (not ECC), +// for the given version number and error correction level. The result is in the range [9, 2956]. +testable int getNumDataCodewords(int version, enum qrcodegen_Ecc ecl) { + int v = version, e = (int)ecl; + assert(0 <= e && e < 4); + return getNumRawDataModules(v) / 8 + - ECC_CODEWORDS_PER_BLOCK [e][v] + * NUM_ERROR_CORRECTION_BLOCKS[e][v]; +} + + +// Returns the number of data bits that can be stored in a QR Code of the given version number, after +// all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8. +// The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table. +testable int getNumRawDataModules(int ver) { + assert(qrcodegen_VERSION_MIN <= ver && ver <= qrcodegen_VERSION_MAX); + int result = (16 * ver + 128) * ver + 64; + if (ver >= 2) { + int numAlign = ver / 7 + 2; + result -= (25 * numAlign - 10) * numAlign - 55; + if (ver >= 7) + result -= 36; + } + assert(208 <= result && result <= 29648); + return result; +} + + + +/*---- Reed-Solomon ECC generator functions ----*/ + +// Computes a Reed-Solomon ECC generator polynomial for the given degree, storing in result[0 : degree]. +// This could be implemented as a lookup table over all possible parameter values, instead of as an algorithm. +testable void reedSolomonComputeDivisor(int degree, uint8_t result[]) { + assert(1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX); + // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1. + // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array {255, 8, 93}. + memset(result, 0, (size_t)degree * sizeof(result[0])); + result[degree - 1] = 1; // Start off with the monomial x^0 + + // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), + // drop the highest monomial term which is always 1x^degree. + // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). + uint8_t root = 1; + for (int i = 0; i < degree; i++) { + // Multiply the current product by (x - r^i) + for (int j = 0; j < degree; j++) { + result[j] = reedSolomonMultiply(result[j], root); + if (j + 1 < degree) + result[j] ^= result[j + 1]; + } + root = reedSolomonMultiply(root, 0x02); + } +} + + +// Computes the Reed-Solomon error correction codeword for the given data and divisor polynomials. +// The remainder when data[0 : dataLen] is divided by divisor[0 : degree] is stored in result[0 : degree]. +// All polynomials are in big endian, and the generator has an implicit leading 1 term. +testable void reedSolomonComputeRemainder(const uint8_t data[], int dataLen, + const uint8_t generator[], int degree, uint8_t result[]) { + assert(1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX); + memset(result, 0, (size_t)degree * sizeof(result[0])); + for (int i = 0; i < dataLen; i++) { // Polynomial division + uint8_t factor = data[i] ^ result[0]; + memmove(&result[0], &result[1], (size_t)(degree - 1) * sizeof(result[0])); + result[degree - 1] = 0; + for (int j = 0; j < degree; j++) + result[j] ^= reedSolomonMultiply(generator[j], factor); + } +} + +#undef qrcodegen_REED_SOLOMON_DEGREE_MAX + + +// Returns the product of the two given field elements modulo GF(2^8/0x11D). +// All inputs are valid. This could be implemented as a 256*256 lookup table. +testable uint8_t reedSolomonMultiply(uint8_t x, uint8_t y) { + // Russian peasant multiplication + uint8_t z = 0; + for (int i = 7; i >= 0; i--) { + z = (uint8_t)((z << 1) ^ ((z >> 7) * 0x11D)); + z ^= ((y >> i) & 1) * x; + } + return z; +} + + + +/*---- Drawing function modules ----*/ + +// Clears the given QR Code grid with light modules for the given +// version's size, then marks every function module as dark. +testable void initializeFunctionModules(int version, uint8_t qrcode[]) { + // Initialize QR Code + int qrsize = version * 4 + 17; + memset(qrcode, 0, (size_t)((qrsize * qrsize + 7) / 8 + 1) * sizeof(qrcode[0])); + qrcode[0] = (uint8_t)qrsize; + + // Fill horizontal and vertical timing patterns + fillRectangle(6, 0, 1, qrsize, qrcode); + fillRectangle(0, 6, qrsize, 1, qrcode); + + // Fill 3 finder patterns (all corners except bottom right) and format bits + fillRectangle(0, 0, 9, 9, qrcode); + fillRectangle(qrsize - 8, 0, 8, 9, qrcode); + fillRectangle(0, qrsize - 8, 9, 8, qrcode); + + // Fill numerous alignment patterns + uint8_t alignPatPos[7]; + int numAlign = getAlignmentPatternPositions(version, alignPatPos); + for (int i = 0; i < numAlign; i++) { + for (int j = 0; j < numAlign; j++) { + // Don't draw on the three finder corners + if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0))) + fillRectangle(alignPatPos[i] - 2, alignPatPos[j] - 2, 5, 5, qrcode); + } + } + + // Fill version blocks + if (version >= 7) { + fillRectangle(qrsize - 11, 0, 3, 6, qrcode); + fillRectangle(0, qrsize - 11, 6, 3, qrcode); + } +} + + +// Draws light function modules and possibly some dark modules onto the given QR Code, without changing +// non-function modules. This does not draw the format bits. This requires all function modules to be previously +// marked dark (namely by initializeFunctionModules()), because this may skip redrawing dark function modules. +static void drawLightFunctionModules(uint8_t qrcode[], int version) { + // Draw horizontal and vertical timing patterns + int qrsize = qrcodegen_getSize(qrcode); + for (int i = 7; i < qrsize - 7; i += 2) { + setModuleBounded(qrcode, 6, i, false); + setModuleBounded(qrcode, i, 6, false); + } + + // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) + for (int dy = -4; dy <= 4; dy++) { + for (int dx = -4; dx <= 4; dx++) { + int dist = abs(dx); + if (abs(dy) > dist) + dist = abs(dy); + if (dist == 2 || dist == 4) { + setModuleUnbounded(qrcode, 3 + dx, 3 + dy, false); + setModuleUnbounded(qrcode, qrsize - 4 + dx, 3 + dy, false); + setModuleUnbounded(qrcode, 3 + dx, qrsize - 4 + dy, false); + } + } + } + + // Draw numerous alignment patterns + uint8_t alignPatPos[7]; + int numAlign = getAlignmentPatternPositions(version, alignPatPos); + for (int i = 0; i < numAlign; i++) { + for (int j = 0; j < numAlign; j++) { + if ((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0)) + continue; // Don't draw on the three finder corners + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) + setModuleBounded(qrcode, alignPatPos[i] + dx, alignPatPos[j] + dy, dx == 0 && dy == 0); + } + } + } + + // Draw version blocks + if (version >= 7) { + // Calculate error correction code and pack bits + int rem = version; // version is uint6, in the range [7, 40] + for (int i = 0; i < 12; i++) + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + long bits = (long)version << 12 | rem; // uint18 + assert(bits >> 18 == 0); + + // Draw two copies + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 3; j++) { + int k = qrsize - 11 + j; + setModuleBounded(qrcode, k, i, (bits & 1) != 0); + setModuleBounded(qrcode, i, k, (bits & 1) != 0); + bits >>= 1; + } + } + } +} + + +// Draws two copies of the format bits (with its own error correction code) based +// on the given mask and error correction level. This always draws all modules of +// the format bits, unlike drawLightFunctionModules() which might skip dark modules. +static void drawFormatBits(enum qrcodegen_Ecc ecl, enum qrcodegen_Mask mask, uint8_t qrcode[]) { + // Calculate error correction code and pack bits + assert(0 <= (int)mask && (int)mask <= 7); + static const int table[] = {1, 0, 3, 2}; + int data = table[(int)ecl] << 3 | (int)mask; // errCorrLvl is uint2, mask is uint3 + int rem = data; + for (int i = 0; i < 10; i++) + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + int bits = (data << 10 | rem) ^ 0x5412; // uint15 + assert(bits >> 15 == 0); + + // Draw first copy + for (int i = 0; i <= 5; i++) + setModuleBounded(qrcode, 8, i, getBit(bits, i)); + setModuleBounded(qrcode, 8, 7, getBit(bits, 6)); + setModuleBounded(qrcode, 8, 8, getBit(bits, 7)); + setModuleBounded(qrcode, 7, 8, getBit(bits, 8)); + for (int i = 9; i < 15; i++) + setModuleBounded(qrcode, 14 - i, 8, getBit(bits, i)); + + // Draw second copy + int qrsize = qrcodegen_getSize(qrcode); + for (int i = 0; i < 8; i++) + setModuleBounded(qrcode, qrsize - 1 - i, 8, getBit(bits, i)); + for (int i = 8; i < 15; i++) + setModuleBounded(qrcode, 8, qrsize - 15 + i, getBit(bits, i)); + setModuleBounded(qrcode, 8, qrsize - 8, true); // Always dark +} + + +// Calculates and stores an ascending list of positions of alignment patterns +// for this version number, returning the length of the list (in the range [0,7]). +// Each position is in the range [0,177), and are used on both the x and y axes. +// This could be implemented as lookup table of 40 variable-length lists of unsigned bytes. +testable int getAlignmentPatternPositions(int version, uint8_t result[7]) { + if (version == 1) + return 0; + int numAlign = version / 7 + 2; + int step = (version * 8 + numAlign * 3 + 5) / (numAlign * 4 - 4) * 2; + for (int i = numAlign - 1, pos = version * 4 + 10; i >= 1; i--, pos -= step) + result[i] = (uint8_t)pos; + result[0] = 6; + return numAlign; +} + + +// Sets every module in the range [left : left + width] * [top : top + height] to dark. +static void fillRectangle(int left, int top, int width, int height, uint8_t qrcode[]) { + for (int dy = 0; dy < height; dy++) { + for (int dx = 0; dx < width; dx++) + setModuleBounded(qrcode, left + dx, top + dy, true); + } +} + + + +/*---- Drawing data modules and masking ----*/ + +// Draws the raw codewords (including data and ECC) onto the given QR Code. This requires the initial state of +// the QR Code to be dark at function modules and light at codeword modules (including unused remainder bits). +static void drawCodewords(const uint8_t data[], int dataLen, uint8_t qrcode[]) { + int qrsize = qrcodegen_getSize(qrcode); + int i = 0; // Bit index into the data + // Do the funny zigzag scan + for (int right = qrsize - 1; right >= 1; right -= 2) { // Index of right column in each column pair + if (right == 6) + right = 5; + for (int vert = 0; vert < qrsize; vert++) { // Vertical counter + for (int j = 0; j < 2; j++) { + int x = right - j; // Actual x coordinate + bool upward = ((right + 1) & 2) == 0; + int y = upward ? qrsize - 1 - vert : vert; // Actual y coordinate + if (!getModuleBounded(qrcode, x, y) && i < dataLen * 8) { + bool dark = getBit(data[i >> 3], 7 - (i & 7)); + setModuleBounded(qrcode, x, y, dark); + i++; + } + // If this QR Code has any remainder bits (0 to 7), they were assigned as + // 0/false/light by the constructor and are left unchanged by this method + } + } + } + assert(i == dataLen * 8); +} + + +// XORs the codeword modules in this QR Code with the given mask pattern +// and given pattern of function modules. The codeword bits must be drawn +// before masking. Due to the arithmetic of XOR, calling applyMask() with +// the same mask value a second time will undo the mask. A final well-formed +// QR Code needs exactly one (not zero, two, etc.) mask applied. +static void applyMask(const uint8_t functionModules[], uint8_t qrcode[], enum qrcodegen_Mask mask) { + assert(0 <= (int)mask && (int)mask <= 7); // Disallows qrcodegen_Mask_AUTO + int qrsize = qrcodegen_getSize(qrcode); + for (int y = 0; y < qrsize; y++) { + for (int x = 0; x < qrsize; x++) { + if (getModuleBounded(functionModules, x, y)) + continue; + bool invert; + switch ((int)mask) { + case 0: invert = (x + y) % 2 == 0; break; + case 1: invert = y % 2 == 0; break; + case 2: invert = x % 3 == 0; break; + case 3: invert = (x + y) % 3 == 0; break; + case 4: invert = (x / 3 + y / 2) % 2 == 0; break; + case 5: invert = x * y % 2 + x * y % 3 == 0; break; + case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; + case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; + default: assert(false); return; + } + bool val = getModuleBounded(qrcode, x, y); + setModuleBounded(qrcode, x, y, val ^ invert); + } + } +} + + +// Calculates and returns the penalty score based on state of the given QR Code's current modules. +// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. +static long getPenaltyScore(const uint8_t qrcode[]) { + int qrsize = qrcodegen_getSize(qrcode); + long result = 0; + + // Adjacent modules in row having same color, and finder-like patterns + for (int y = 0; y < qrsize; y++) { + bool runColor = false; + int runX = 0; + int runHistory[7] = {0}; + for (int x = 0; x < qrsize; x++) { + if (getModuleBounded(qrcode, x, y) == runColor) { + runX++; + if (runX == 5) + result += PENALTY_N1; + else if (runX > 5) + result++; + } else { + finderPenaltyAddHistory(runX, runHistory, qrsize); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory, qrsize) * PENALTY_N3; + runColor = getModuleBounded(qrcode, x, y); + runX = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runX, runHistory, qrsize) * PENALTY_N3; + } + // Adjacent modules in column having same color, and finder-like patterns + for (int x = 0; x < qrsize; x++) { + bool runColor = false; + int runY = 0; + int runHistory[7] = {0}; + for (int y = 0; y < qrsize; y++) { + if (getModuleBounded(qrcode, x, y) == runColor) { + runY++; + if (runY == 5) + result += PENALTY_N1; + else if (runY > 5) + result++; + } else { + finderPenaltyAddHistory(runY, runHistory, qrsize); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory, qrsize) * PENALTY_N3; + runColor = getModuleBounded(qrcode, x, y); + runY = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runY, runHistory, qrsize) * PENALTY_N3; + } + + // 2*2 blocks of modules having same color + for (int y = 0; y < qrsize - 1; y++) { + for (int x = 0; x < qrsize - 1; x++) { + bool color = getModuleBounded(qrcode, x, y); + if ( color == getModuleBounded(qrcode, x + 1, y) && + color == getModuleBounded(qrcode, x, y + 1) && + color == getModuleBounded(qrcode, x + 1, y + 1)) + result += PENALTY_N2; + } + } + + // Balance of dark and light modules + int dark = 0; + for (int y = 0; y < qrsize; y++) { + for (int x = 0; x < qrsize; x++) { + if (getModuleBounded(qrcode, x, y)) + dark++; + } + } + int total = qrsize * qrsize; // Note that size is odd, so dark/total != 1/2 + // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)% + int k = (int)((labs(dark * 20L - total * 10L) + total - 1) / total) - 1; + assert(0 <= k && k <= 9); + result += k * PENALTY_N4; + assert(0 <= result && result <= 2568888L); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4 + return result; +} + + +// Can only be called immediately after a light run is added, and +// returns either 0, 1, or 2. A helper function for getPenaltyScore(). +static int finderPenaltyCountPatterns(const int runHistory[7], int qrsize) { + int n = runHistory[1]; + assert(n <= qrsize * 3); (void)qrsize; + bool core = n > 0 && runHistory[2] == n && runHistory[3] == n * 3 && runHistory[4] == n && runHistory[5] == n; + // The maximum QR Code size is 177, hence the dark run length n <= 177. + // Arithmetic is promoted to int, so n*4 will not overflow. + return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0); +} + + +// Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore(). +static int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, int runHistory[7], int qrsize) { + if (currentRunColor) { // Terminate dark run + finderPenaltyAddHistory(currentRunLength, runHistory, qrsize); + currentRunLength = 0; + } + currentRunLength += qrsize; // Add light border to final run + finderPenaltyAddHistory(currentRunLength, runHistory, qrsize); + return finderPenaltyCountPatterns(runHistory, qrsize); +} + + +// Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore(). +static void finderPenaltyAddHistory(int currentRunLength, int runHistory[7], int qrsize) { + if (runHistory[0] == 0) + currentRunLength += qrsize; // Add light border to initial run + memmove(&runHistory[1], &runHistory[0], 6 * sizeof(runHistory[0])); + runHistory[0] = currentRunLength; +} + + + +/*---- Basic QR Code information ----*/ + +// Public function - see documentation comment in header file. +int qrcodegen_getSize(const uint8_t qrcode[]) { + assert(qrcode != NULL); + int result = qrcode[0]; + assert((qrcodegen_VERSION_MIN * 4 + 17) <= result + && result <= (qrcodegen_VERSION_MAX * 4 + 17)); + return result; +} + + +// Public function - see documentation comment in header file. +bool qrcodegen_getModule(const uint8_t qrcode[], int x, int y) { + assert(qrcode != NULL); + int qrsize = qrcode[0]; + return (0 <= x && x < qrsize && 0 <= y && y < qrsize) && getModuleBounded(qrcode, x, y); +} + + +// Returns the color of the module at the given coordinates, which must be in bounds. +testable bool getModuleBounded(const uint8_t qrcode[], int x, int y) { + int qrsize = qrcode[0]; + assert(21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsize); + int index = y * qrsize + x; + return getBit(qrcode[(index >> 3) + 1], index & 7); +} + + +// Sets the color of the module at the given coordinates, which must be in bounds. +testable void setModuleBounded(uint8_t qrcode[], int x, int y, bool isDark) { + int qrsize = qrcode[0]; + assert(21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsize); + int index = y * qrsize + x; + int bitIndex = index & 7; + int byteIndex = (index >> 3) + 1; + if (isDark) + qrcode[byteIndex] |= 1 << bitIndex; + else + qrcode[byteIndex] &= (1 << bitIndex) ^ 0xFF; +} + + +// Sets the color of the module at the given coordinates, doing nothing if out of bounds. +testable void setModuleUnbounded(uint8_t qrcode[], int x, int y, bool isDark) { + int qrsize = qrcode[0]; + if (0 <= x && x < qrsize && 0 <= y && y < qrsize) + setModuleBounded(qrcode, x, y, isDark); +} + + +// Returns true iff the i'th bit of x is set to 1. Requires x >= 0 and 0 <= i <= 14. +static bool getBit(int x, int i) { + return ((x >> i) & 1) != 0; +} + + + +/*---- Segment handling ----*/ + +// Public function - see documentation comment in header file. +bool qrcodegen_isNumeric(const char *text) { + assert(text != NULL); + for (; *text != '\0'; text++) { + if (*text < '0' || *text > '9') + return false; + } + return true; +} + + +// Public function - see documentation comment in header file. +bool qrcodegen_isAlphanumeric(const char *text) { + assert(text != NULL); + for (; *text != '\0'; text++) { + if (strchr(ALPHANUMERIC_CHARSET, *text) == NULL) + return false; + } + return true; +} + + +// Public function - see documentation comment in header file. +size_t qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars) { + int temp = calcSegmentBitLength(mode, numChars); + if (temp == LENGTH_OVERFLOW) + return SIZE_MAX; + assert(0 <= temp && temp <= INT16_MAX); + return ((size_t)temp + 7) / 8; +} + + +// Returns the number of data bits needed to represent a segment +// containing the given number of characters using the given mode. Notes: +// - Returns LENGTH_OVERFLOW on failure, i.e. numChars > INT16_MAX +// or the number of needed bits exceeds INT16_MAX (i.e. 32767). +// - Otherwise, all valid results are in the range [0, INT16_MAX]. +// - For byte mode, numChars measures the number of bytes, not Unicode code points. +// - For ECI mode, numChars must be 0, and the worst-case number of bits is returned. +// An actual ECI segment can have shorter data. For non-ECI modes, the result is exact. +testable int calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars) { + // All calculations are designed to avoid overflow on all platforms + if (numChars > (unsigned int)INT16_MAX) + return LENGTH_OVERFLOW; + long result = (long)numChars; + if (mode == qrcodegen_Mode_NUMERIC) + result = (result * 10 + 2) / 3; // ceil(10/3 * n) + else if (mode == qrcodegen_Mode_ALPHANUMERIC) + result = (result * 11 + 1) / 2; // ceil(11/2 * n) + else if (mode == qrcodegen_Mode_BYTE) + result *= 8; + else if (mode == qrcodegen_Mode_KANJI) + result *= 13; + else if (mode == qrcodegen_Mode_ECI && numChars == 0) + result = 3 * 8; + else { // Invalid argument + assert(false); + return LENGTH_OVERFLOW; + } + assert(result >= 0); + if (result > INT16_MAX) + return LENGTH_OVERFLOW; + return (int)result; +} + + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]) { + assert(data != NULL || len == 0); + struct qrcodegen_Segment result; + result.mode = qrcodegen_Mode_BYTE; + result.bitLength = calcSegmentBitLength(result.mode, len); + assert(result.bitLength != LENGTH_OVERFLOW); + result.numChars = (int)len; + if (len > 0) + memcpy(buf, data, len * sizeof(buf[0])); + result.data = buf; + return result; +} + + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment qrcodegen_makeNumeric(const char *digits, uint8_t buf[]) { + assert(digits != NULL); + struct qrcodegen_Segment result; + size_t len = strlen(digits); + result.mode = qrcodegen_Mode_NUMERIC; + int bitLen = calcSegmentBitLength(result.mode, len); + assert(bitLen != LENGTH_OVERFLOW); + result.numChars = (int)len; + if (bitLen > 0) + memset(buf, 0, ((size_t)bitLen + 7) / 8 * sizeof(buf[0])); + result.bitLength = 0; + + unsigned int accumData = 0; + int accumCount = 0; + for (; *digits != '\0'; digits++) { + char c = *digits; + assert('0' <= c && c <= '9'); + accumData = accumData * 10 + (unsigned int)(c - '0'); + accumCount++; + if (accumCount == 3) { + appendBitsToBuffer(accumData, 10, buf, &result.bitLength); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 or 2 digits remaining + appendBitsToBuffer(accumData, accumCount * 3 + 1, buf, &result.bitLength); + assert(result.bitLength == bitLen); + result.data = buf; + return result; +} + + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]) { + assert(text != NULL); + struct qrcodegen_Segment result; + size_t len = strlen(text); + result.mode = qrcodegen_Mode_ALPHANUMERIC; + int bitLen = calcSegmentBitLength(result.mode, len); + assert(bitLen != LENGTH_OVERFLOW); + result.numChars = (int)len; + if (bitLen > 0) + memset(buf, 0, ((size_t)bitLen + 7) / 8 * sizeof(buf[0])); + result.bitLength = 0; + + unsigned int accumData = 0; + int accumCount = 0; + for (; *text != '\0'; text++) { + const char *temp = strchr(ALPHANUMERIC_CHARSET, *text); + assert(temp != NULL); + accumData = accumData * 45 + (unsigned int)(temp - ALPHANUMERIC_CHARSET); + accumCount++; + if (accumCount == 2) { + appendBitsToBuffer(accumData, 11, buf, &result.bitLength); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 character remaining + appendBitsToBuffer(accumData, 6, buf, &result.bitLength); + assert(result.bitLength == bitLen); + result.data = buf; + return result; +} + + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment qrcodegen_makeEci(long assignVal, uint8_t buf[]) { + struct qrcodegen_Segment result; + result.mode = qrcodegen_Mode_ECI; + result.numChars = 0; + result.bitLength = 0; + if (assignVal < 0) + assert(false); + else if (assignVal < (1 << 7)) { + memset(buf, 0, 1 * sizeof(buf[0])); + appendBitsToBuffer((unsigned int)assignVal, 8, buf, &result.bitLength); + } else if (assignVal < (1 << 14)) { + memset(buf, 0, 2 * sizeof(buf[0])); + appendBitsToBuffer(2, 2, buf, &result.bitLength); + appendBitsToBuffer((unsigned int)assignVal, 14, buf, &result.bitLength); + } else if (assignVal < 1000000L) { + memset(buf, 0, 3 * sizeof(buf[0])); + appendBitsToBuffer(6, 3, buf, &result.bitLength); + appendBitsToBuffer((unsigned int)(assignVal >> 10), 11, buf, &result.bitLength); + appendBitsToBuffer((unsigned int)(assignVal & 0x3FF), 10, buf, &result.bitLength); + } else + assert(false); + result.data = buf; + return result; +} + + +// Calculates the number of bits needed to encode the given segments at the given version. +// Returns a non-negative number if successful. Otherwise returns LENGTH_OVERFLOW if a segment +// has too many characters to fit its length field, or the total bits exceeds INT16_MAX. +testable int getTotalBits(const struct qrcodegen_Segment segs[], size_t len, int version) { + assert(segs != NULL || len == 0); + long result = 0; + for (size_t i = 0; i < len; i++) { + int numChars = segs[i].numChars; + int bitLength = segs[i].bitLength; + assert(0 <= numChars && numChars <= INT16_MAX); + assert(0 <= bitLength && bitLength <= INT16_MAX); + int ccbits = numCharCountBits(segs[i].mode, version); + assert(0 <= ccbits && ccbits <= 16); + if (numChars >= (1L << ccbits)) + return LENGTH_OVERFLOW; // The segment's length doesn't fit the field's bit width + result += 4L + ccbits + bitLength; + if (result > INT16_MAX) + return LENGTH_OVERFLOW; // The sum might overflow an int type + } + assert(0 <= result && result <= INT16_MAX); + return (int)result; +} + + +// Returns the bit width of the character count field for a segment in the given mode +// in a QR Code at the given version number. The result is in the range [0, 16]. +static int numCharCountBits(enum qrcodegen_Mode mode, int version) { + assert(qrcodegen_VERSION_MIN <= version && version <= qrcodegen_VERSION_MAX); + int i = (version + 7) / 17; + switch (mode) { + case qrcodegen_Mode_NUMERIC : { static const int temp[] = {10, 12, 14}; return temp[i]; } + case qrcodegen_Mode_ALPHANUMERIC: { static const int temp[] = { 9, 11, 13}; return temp[i]; } + case qrcodegen_Mode_BYTE : { static const int temp[] = { 8, 16, 16}; return temp[i]; } + case qrcodegen_Mode_KANJI : { static const int temp[] = { 8, 10, 12}; return temp[i]; } + case qrcodegen_Mode_ECI : return 0; + default: assert(false); return -1; // Dummy value + } +} + + +#undef LENGTH_OVERFLOW diff --git a/main/third_party/qrcodegen.h b/main/third_party/qrcodegen.h new file mode 100644 index 0000000..6bbc157 --- /dev/null +++ b/main/third_party/qrcodegen.h @@ -0,0 +1,385 @@ +/* + * QR Code generator library (C) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#pragma once + +#include +#include +#include + + +#ifdef __cplusplus +extern "C" { +#endif + + +/* + * This library creates QR Code symbols, which is a type of two-dimension barcode. + * Invented by Denso Wave and described in the ISO/IEC 18004 standard. + * A QR Code structure is an immutable square grid of dark and light cells. + * The library provides functions to create a QR Code from text or binary data. + * The library covers the QR Code Model 2 specification, supporting all versions (sizes) + * from 1 to 40, all 4 error correction levels, and 4 character encoding modes. + * + * Ways to create a QR Code object: + * - High level: Take the payload data and call qrcodegen_encodeText() or qrcodegen_encodeBinary(). + * - Low level: Custom-make the list of segments and call + * qrcodegen_encodeSegments() or qrcodegen_encodeSegmentsAdvanced(). + * (Note that all ways require supplying the desired error correction level and various byte buffers.) + */ + + +/*---- Enum and struct types----*/ + +/* + * The error correction level in a QR Code symbol. + */ +enum qrcodegen_Ecc { + // Must be declared in ascending order of error protection + // so that an internal qrcodegen function works properly + qrcodegen_Ecc_LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords + qrcodegen_Ecc_MEDIUM , // The QR Code can tolerate about 15% erroneous codewords + qrcodegen_Ecc_QUARTILE, // The QR Code can tolerate about 25% erroneous codewords + qrcodegen_Ecc_HIGH , // The QR Code can tolerate about 30% erroneous codewords +}; + + +/* + * The mask pattern used in a QR Code symbol. + */ +enum qrcodegen_Mask { + // A special value to tell the QR Code encoder to + // automatically select an appropriate mask pattern + qrcodegen_Mask_AUTO = -1, + // The eight actual mask patterns + qrcodegen_Mask_0 = 0, + qrcodegen_Mask_1, + qrcodegen_Mask_2, + qrcodegen_Mask_3, + qrcodegen_Mask_4, + qrcodegen_Mask_5, + qrcodegen_Mask_6, + qrcodegen_Mask_7, +}; + + +/* + * Describes how a segment's data bits are interpreted. + */ +enum qrcodegen_Mode { + qrcodegen_Mode_NUMERIC = 0x1, + qrcodegen_Mode_ALPHANUMERIC = 0x2, + qrcodegen_Mode_BYTE = 0x4, + qrcodegen_Mode_KANJI = 0x8, + qrcodegen_Mode_ECI = 0x7, +}; + + +/* + * A segment of character/binary/control data in a QR Code symbol. + * The mid-level way to create a segment is to take the payload data + * and call a factory function such as qrcodegen_makeNumeric(). + * The low-level way to create a segment is to custom-make the bit buffer + * and initialize a qrcodegen_Segment struct with appropriate values. + * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data. + * Any segment longer than this is meaningless for the purpose of generating QR Codes. + * Moreover, the maximum allowed bit length is 32767 because + * the largest QR Code (version 40) has 31329 modules. + */ +struct qrcodegen_Segment { + // The mode indicator of this segment. + enum qrcodegen_Mode mode; + + // The length of this segment's unencoded data. Measured in characters for + // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. + // Always zero or positive. Not the same as the data's bit length. + int numChars; + + // The data bits of this segment, packed in bitwise big endian. + // Can be null if the bit length is zero. + uint8_t *data; + + // The number of valid data bits used in the buffer. Requires + // 0 <= bitLength <= 32767, and bitLength <= (capacity of data array) * 8. + // The character count (numChars) must agree with the mode and the bit buffer length. + int bitLength; +}; + + + +/*---- Macro constants and functions ----*/ + +#define qrcodegen_VERSION_MIN 1 // The minimum version number supported in the QR Code Model 2 standard +#define qrcodegen_VERSION_MAX 40 // The maximum version number supported in the QR Code Model 2 standard + +// Calculates the number of bytes needed to store any QR Code up to and including the given version number, +// as a compile-time constant. For example, 'uint8_t buffer[qrcodegen_BUFFER_LEN_FOR_VERSION(25)];' +// can store any single QR Code from version 1 to 25 (inclusive). The result fits in an int (or int16). +// Requires qrcodegen_VERSION_MIN <= n <= qrcodegen_VERSION_MAX. +#define qrcodegen_BUFFER_LEN_FOR_VERSION(n) ((((n) * 4 + 17) * ((n) * 4 + 17) + 7) / 8 + 1) + +// The worst-case number of bytes needed to store one QR Code, up to and including +// version 40. This value equals 3918, which is just under 4 kilobytes. +// Use this more convenient value to avoid calculating tighter memory bounds for buffers. +#define qrcodegen_BUFFER_LEN_MAX qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX) + + + +/*---- Functions (high level) to generate QR Codes ----*/ + +/* + * Encodes the given text string to a QR Code, returning true if successful. + * If the data is too long to fit in any version in the given range + * at the given ECC level, then false is returned. + * + * The input text must be encoded in UTF-8 and contain no NULs. + * Requires 1 <= minVersion <= maxVersion <= 40. + * + * The smallest possible QR Code version within the given range is automatically + * chosen for the output. Iff boostEcl is true, then the ECC level of the result + * may be higher than the ecl argument if it can be done without increasing the + * version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or + * qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow). + * + * About the arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion): + * - Before calling the function: + * - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow + * reading and writing; hence each array must have a length of at least len. + * - The two ranges must not overlap (aliasing). + * - The initial state of both ranges can be uninitialized + * because the function always writes before reading. + * - After the function returns: + * - Both ranges have no guarantee on which elements are initialized and what values are stored. + * - tempBuffer contains no useful data and should be treated as entirely uninitialized. + * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule(). + * + * If successful, the resulting QR Code may use numeric, + * alphanumeric, or byte mode to encode the text. + * + * 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. + * These numbers represent the hard upper limit of the QR Code standard. + * + * Please consult the QR Code specification for information on + * data capacities per version, ECC level, and text encoding mode. + */ +bool qrcodegen_encodeText(const char *text, uint8_t tempBuffer[], uint8_t qrcode[], + enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl); + + +/* + * Encodes the given binary data to a QR Code, returning true if successful. + * If the data is too long to fit in any version in the given range + * at the given ECC level, then false is returned. + * + * Requires 1 <= minVersion <= maxVersion <= 40. + * + * The smallest possible QR Code version within the given range is automatically + * chosen for the output. Iff boostEcl is true, then the ECC level of the result + * may be higher than the ecl argument if it can be done without increasing the + * version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or + * qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow). + * + * About the arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion): + * - Before calling the function: + * - The array ranges dataAndTemp[0 : len] and qrcode[0 : len] must allow + * reading and writing; hence each array must have a length of at least len. + * - The two ranges must not overlap (aliasing). + * - The input array range dataAndTemp[0 : dataLen] should normally be + * valid UTF-8 text, but is not required by the QR Code standard. + * - The initial state of dataAndTemp[dataLen : len] and qrcode[0 : len] + * can be uninitialized because the function always writes before reading. + * - After the function returns: + * - Both ranges have no guarantee on which elements are initialized and what values are stored. + * - dataAndTemp contains no useful data and should be treated as entirely uninitialized. + * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule(). + * + * If successful, the resulting QR Code will use byte mode to encode the data. + * + * In the most optimistic case, a QR Code at version 40 with low ECC can hold any byte + * sequence up to length 2953. This is the hard upper limit of the QR Code standard. + * + * Please consult the QR Code specification for information on + * data capacities per version, ECC level, and text encoding mode. + */ +bool qrcodegen_encodeBinary(uint8_t dataAndTemp[], size_t dataLen, uint8_t qrcode[], + enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl); + + +/*---- Functions (low level) to generate QR Codes ----*/ + +/* + * Encodes the given segments to a QR Code, returning true if successful. + * If the data is too long to fit in any version at the given ECC level, + * then false is returned. + * + * The smallest possible QR Code version is automatically chosen for + * the output. The ECC level of the result may be higher than the + * ecl argument if it can be done without increasing the version. + * + * About the byte arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX): + * - Before calling the function: + * - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow + * reading and writing; hence each array must have a length of at least len. + * - The two ranges must not overlap (aliasing). + * - The initial state of both ranges can be uninitialized + * because the function always writes before reading. + * - The input array segs can contain segments whose data buffers overlap with tempBuffer. + * - After the function returns: + * - Both ranges have no guarantee on which elements are initialized and what values are stored. + * - tempBuffer contains no useful data and should be treated as entirely uninitialized. + * - Any segment whose data buffer overlaps with tempBuffer[0 : len] + * must be treated as having invalid values in that array. + * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule(). + * + * Please consult the QR Code specification for information on + * data capacities per version, ECC level, and text encoding mode. + * + * This function allows the user to create a custom sequence of segments that switches + * between modes (such as alphanumeric and byte) to encode text in less space. + * This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary(). + */ +bool qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[], size_t len, + enum qrcodegen_Ecc ecl, uint8_t tempBuffer[], uint8_t qrcode[]); + + +/* + * Encodes the given segments to a QR Code, returning true if successful. + * If the data is too long to fit in any version in the given range + * at the given ECC level, then false is returned. + * + * Requires 1 <= minVersion <= maxVersion <= 40. + * + * The smallest possible QR Code version within the given range is automatically + * chosen for the output. Iff boostEcl is true, then the ECC level of the result + * may be higher than the ecl argument if it can be done without increasing the + * version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or + * qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow). + * + * About the byte arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX): + * - Before calling the function: + * - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow + * reading and writing; hence each array must have a length of at least len. + * - The two ranges must not overlap (aliasing). + * - The initial state of both ranges can be uninitialized + * because the function always writes before reading. + * - The input array segs can contain segments whose data buffers overlap with tempBuffer. + * - After the function returns: + * - Both ranges have no guarantee on which elements are initialized and what values are stored. + * - tempBuffer contains no useful data and should be treated as entirely uninitialized. + * - Any segment whose data buffer overlaps with tempBuffer[0 : len] + * must be treated as having invalid values in that array. + * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule(). + * + * Please consult the QR Code specification for information on + * data capacities per version, ECC level, and text encoding mode. + * + * This function allows the user to create a custom sequence of segments that switches + * between modes (such as alphanumeric and byte) to encode text in less space. + * This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary(). + */ +bool qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[], size_t len, enum qrcodegen_Ecc ecl, + int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl, uint8_t tempBuffer[], uint8_t qrcode[]); + + +/* + * Tests whether the given string can be encoded as a segment in numeric mode. + * A string is encodable iff each character is in the range 0 to 9. + */ +bool qrcodegen_isNumeric(const char *text); + + +/* + * Tests whether the given string can be encoded as a segment in alphanumeric mode. + * A string is encodable iff each character is in the following set: 0 to 9, A to Z + * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ +bool qrcodegen_isAlphanumeric(const char *text); + + +/* + * Returns the number of bytes (uint8_t) needed for the data buffer of a segment + * containing the given number of characters using the given mode. Notes: + * - Returns SIZE_MAX on failure, i.e. numChars > INT16_MAX or the internal + * calculation of the number of needed bits exceeds INT16_MAX (i.e. 32767). + * - Otherwise, all valid results are in the range [0, ceil(INT16_MAX / 8)], i.e. at most 4096. + * - It is okay for the user to allocate more bytes for the buffer than needed. + * - For byte mode, numChars measures the number of bytes, not Unicode code points. + * - For ECI mode, numChars must be 0, and the worst-case number of bytes is returned. + * An actual ECI segment can have shorter data. For non-ECI modes, the result is exact. + */ +size_t qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars); + + +/* + * Returns a segment representing the given binary data encoded in + * byte mode. All input byte arrays are acceptable. Any text string + * can be converted to UTF-8 bytes and encoded as a byte mode segment. + */ +struct qrcodegen_Segment qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]); + + +/* + * Returns a segment representing the given string of decimal digits encoded in numeric mode. + */ +struct qrcodegen_Segment qrcodegen_makeNumeric(const char *digits, uint8_t buf[]); + + +/* + * Returns a segment representing the given text string encoded in alphanumeric mode. + * The characters allowed are: 0 to 9, A to Z (uppercase only), space, + * dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ +struct qrcodegen_Segment qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]); + + +/* + * Returns a segment representing an Extended Channel Interpretation + * (ECI) designator with the given assignment value. + */ +struct qrcodegen_Segment qrcodegen_makeEci(long assignVal, uint8_t buf[]); + + +/*---- Functions to extract raw data from QR Codes ----*/ + +/* + * Returns the side length of the given QR Code, assuming that encoding succeeded. + * The result is in the range [21, 177]. Note that the length of the array buffer + * is related to the side length - every 'uint8_t qrcode[]' must have length at least + * qrcodegen_BUFFER_LEN_FOR_VERSION(version), which equals ceil(size^2 / 8 + 1). + */ +int qrcodegen_getSize(const uint8_t qrcode[]); + + +/* + * Returns the color of the module (pixel) at the given coordinates, which is false + * for light or true for dark. The top left corner has the coordinates (x=0, y=0). + * If the given coordinates are out of bounds, then false (light) is returned. + */ +bool qrcodegen_getModule(const uint8_t qrcode[], int x, int y); + + +#ifdef __cplusplus +} +#endif diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..6fb9896 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,13 @@ +CONFIG_IDF_TARGET="esp32s3" +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ROLE_CENTRAL=y +CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=247 +# CONFIG_ESP_WIFI_SOFTAP_SUPPORT is not set +CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=n +CONFIG_LYF_WIFI_SSID="TalkingQ" +CONFIG_LYF_WIFI_PASSWORD="TalkingQ123" +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_ESPTOOLPY_FLASHSIZE="16MB" diff --git a/tools/gen_cn16_font.py b/tools/gen_cn16_font.py new file mode 100644 index 0000000..e6e17fa --- /dev/null +++ b/tools/gen_cn16_font.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +""" +Generate compact Chinese 16x16 bitmap font assets for ESP32 firmware. + +Output: + - main/domain/assets/fonts/cn16_index.bin (entry: uint32 codepoint + uint16 glyph_id, little-endian) + - main/domain/assets/fonts/cn16_glyphs.bin (glyph bytes, 32 bytes per glyph, row-major, MSB-left) + +Glyph set: + All valid single-character GB2312 code points. +""" + +from __future__ import annotations + +import argparse +import struct +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +def default_out_dir() -> Path: + repo_root = Path(__file__).resolve().parent.parent + return repo_root / "main" / "domain" / "assets" / "fonts" + + +def iter_gb2312_codepoints(): + seen = set() + for hi in range(0xA1, 0xFF): + for lo in range(0xA1, 0xFF): + bs = bytes((hi, lo)) + try: + ch = bs.decode("gb2312") + except UnicodeDecodeError: + continue + if len(ch) != 1: + continue + cp = ord(ch) + if cp in seen: + continue + seen.add(cp) + yield cp + + +def render_glyph_16(font: ImageFont.FreeTypeFont, cp: int) -> bytes: + ch = chr(cp) + img = Image.new("L", (16, 16), color=255) + draw = ImageDraw.Draw(img) + + # Center glyph in 16x16 cell. + bbox = draw.textbbox((0, 0), ch, font=font) + if bbox is None: + bbox = (0, 0, 0, 0) + x0, y0, x1, y1 = bbox + w = x1 - x0 + h = y1 - y0 + + draw_x = (16 - w) // 2 - x0 + draw_y = (16 - h) // 2 - y0 + draw.text((draw_x, draw_y), ch, font=font, fill=0) + + # Convert to 1bpp packed rows, MSB-left. + px = img.load() + out = bytearray() + for y in range(16): + row_bits = 0 + for x in range(16): + bit = 1 if px[x, y] < 128 else 0 + row_bits = (row_bits << 1) | bit + out.append((row_bits >> 8) & 0xFF) + out.append(row_bits & 0xFF) + return bytes(out) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--font", + default="app/src/main/assets/fonts/msyh.ttc", + help="Source CJK font path (TTF/TTC/OTF)", + ) + parser.add_argument( + "--font-index", + type=int, + default=0, + help="TTC face index", + ) + parser.add_argument( + "--out-dir", + default=str(default_out_dir()), + help="Output directory (default: /main/domain/assets/fonts)", + ) + args = parser.parse_args() + + font_path = Path(args.font).resolve() + if not font_path.exists(): + raise FileNotFoundError(f"font not found: {font_path}") + + out_dir = Path(args.out_dir).resolve() + out_dir.mkdir(parents=True, exist_ok=True) + index_path = out_dir / "cn16_index.bin" + glyph_path = out_dir / "cn16_glyphs.bin" + + font = ImageFont.truetype(str(font_path), 16, index=args.font_index) + + codepoints = sorted(iter_gb2312_codepoints()) + glyphs = [] + index_entries = [] + + for gid, cp in enumerate(codepoints): + glyph = render_glyph_16(font, cp) + glyphs.append(glyph) + index_entries.append((cp, gid)) + + with open(index_path, "wb") as f: + for cp, gid in index_entries: + f.write(struct.pack("