From 9e141b8de0887c3f12d054912ba8dcf3928eb16d Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 24 Feb 2026 18:10:40 +0800 Subject: [PATCH] init --- .gitignore | 10 + .vscode/settings.json | 5 + AGENTS.md | 53 + CMakeLists.txt | 3 + README.md | 277 +++++ main/CMakeLists.txt | 42 + main/Kconfig.projbuild | 23 + main/app_composition/README.md | 8 + main/app_composition/include/.gitkeep | 0 main/app_composition/internal/.gitkeep | 0 main/app_composition/src/.gitkeep | 0 main/app_composition/src/app_main.c | 12 + main/control_plane/README.md | 8 + main/control_plane/include/.gitkeep | 0 .../include/controller_lifecycle.h | 6 + main/control_plane/include/rest_server.h | 6 + main/control_plane/internal/.gitkeep | 0 .../internal/rest_server_internal.h | 46 + main/control_plane/src/.gitkeep | 0 main/control_plane/src/controller_lifecycle.c | 27 + main/control_plane/src/rest_server.c | 487 ++++++++ main/control_plane/src/rest_server_common.c | 164 +++ main/control_plane/src/rest_server_jobs.c | 173 +++ main/control_plane/src/rest_server_ops.c | 535 +++++++++ main/control_plane/src/rest_server_print.c | 659 +++++++++++ main/dependency_whitelist.md | 13 + main/domain/README.md | 9 + main/domain/assets/fonts/cn16_glyphs.bin | Bin 0 -> 238240 bytes main/domain/assets/fonts/cn16_index.bin | Bin 0 -> 44670 bytes main/domain/include/.gitkeep | 0 main/domain/include/printer_protocol.h | 90 ++ main/domain/include/raster_tools.h | 44 + main/domain/include/system_runtime.h | 11 + main/domain/internal/.gitkeep | 0 .../internal/printer_protocol_internal.h | 32 + main/domain/src/.gitkeep | 0 main/domain/src/printer_protocol.c | 931 +++++++++++++++ main/domain/src/printer_protocol_commands.c | 322 ++++++ main/domain/src/raster_tools.c | 467 ++++++++ main/domain/src/raster_tools_image_qr.c | 258 +++++ main/domain/src/system_runtime.c | 20 + main/platform/README.md | 13 + main/platform/include/.gitkeep | 0 main/platform/include/ble_printer_client.h | 25 + main/platform/include/platform_bootstrap.h | 5 + main/platform/include/wifi_manager.h | 11 + main/platform/internal/.gitkeep | 0 main/platform/src/.gitkeep | 0 main/platform/src/ble_printer_client.c | 655 +++++++++++ main/platform/src/platform_bootstrap.c | 23 + main/platform/src/wifi_manager.c | 144 +++ main/third_party/qrcodegen.c | 1027 +++++++++++++++++ main/third_party/qrcodegen.h | 385 ++++++ sdkconfig.defaults | 13 + tools/gen_cn16_font.py | 130 +++ 55 files changed, 7172 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 AGENTS.md create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 main/CMakeLists.txt create mode 100644 main/Kconfig.projbuild create mode 100644 main/app_composition/README.md create mode 100644 main/app_composition/include/.gitkeep create mode 100644 main/app_composition/internal/.gitkeep create mode 100644 main/app_composition/src/.gitkeep create mode 100644 main/app_composition/src/app_main.c create mode 100644 main/control_plane/README.md create mode 100644 main/control_plane/include/.gitkeep create mode 100644 main/control_plane/include/controller_lifecycle.h create mode 100644 main/control_plane/include/rest_server.h create mode 100644 main/control_plane/internal/.gitkeep create mode 100644 main/control_plane/internal/rest_server_internal.h create mode 100644 main/control_plane/src/.gitkeep create mode 100644 main/control_plane/src/controller_lifecycle.c create mode 100644 main/control_plane/src/rest_server.c create mode 100644 main/control_plane/src/rest_server_common.c create mode 100644 main/control_plane/src/rest_server_jobs.c create mode 100644 main/control_plane/src/rest_server_ops.c create mode 100644 main/control_plane/src/rest_server_print.c create mode 100644 main/dependency_whitelist.md create mode 100644 main/domain/README.md create mode 100644 main/domain/assets/fonts/cn16_glyphs.bin create mode 100644 main/domain/assets/fonts/cn16_index.bin create mode 100644 main/domain/include/.gitkeep create mode 100644 main/domain/include/printer_protocol.h create mode 100644 main/domain/include/raster_tools.h create mode 100644 main/domain/include/system_runtime.h create mode 100644 main/domain/internal/.gitkeep create mode 100644 main/domain/internal/printer_protocol_internal.h create mode 100644 main/domain/src/.gitkeep create mode 100644 main/domain/src/printer_protocol.c create mode 100644 main/domain/src/printer_protocol_commands.c create mode 100644 main/domain/src/raster_tools.c create mode 100644 main/domain/src/raster_tools_image_qr.c create mode 100644 main/domain/src/system_runtime.c create mode 100644 main/platform/README.md create mode 100644 main/platform/include/.gitkeep create mode 100644 main/platform/include/ble_printer_client.h create mode 100644 main/platform/include/platform_bootstrap.h create mode 100644 main/platform/include/wifi_manager.h create mode 100644 main/platform/internal/.gitkeep create mode 100644 main/platform/src/.gitkeep create mode 100644 main/platform/src/ble_printer_client.c create mode 100644 main/platform/src/platform_bootstrap.c create mode 100644 main/platform/src/wifi_manager.c create mode 100644 main/third_party/qrcodegen.c create mode 100644 main/third_party/qrcodegen.h create mode 100644 sdkconfig.defaults create mode 100644 tools/gen_cn16_font.py 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 0000000000000000000000000000000000000000..631b943a6dd15ad4c42df31508848a1c3c66c39f GIT binary patch literal 238240 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
};)N3ipHaD54k|}ex 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
  • ^(lVTs^?ZC|Hi{# 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 J#r>Re{XdX;z&ii{ literal 0 HcmV?d00001 diff --git a/main/domain/assets/fonts/cn16_index.bin b/main/domain/assets/fonts/cn16_index.bin new file mode 100644 index 0000000000000000000000000000000000000000..2638f0d345e2dc73b0f864e3481775da282dbd2f GIT binary patch literal 44670 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 +#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("