From 42a785f0868d92bfa138ed3338a06af0d79697d9 Mon Sep 17 00:00:00 2001 From: knb Date: Fri, 17 Apr 2026 03:24:00 +0900 Subject: [PATCH] Fix PT-P710BT raster protocol handling and add regression coverage. Align print command payload generation with model-specific protocol requirements, add verbose CLI diagnostics and robust status retries, and introduce protocol regression tests to prevent future GF/ESC i z regressions. Made-with: Cursor --- CMakeLists.txt | 14 +++++++ README.md | 23 +++++++----- src/cli/main.c | 63 +++++++++++++++++++++++++++++++- src/lib/libptouch_print.c | 43 ++++++++++------------ src/lib/libptouch_protocol.c | 41 +++++++++++++++++++++ src/lib/libptouch_protocol.h | 12 ++++++ src/lib/libptouch_status.c | 18 +++++++-- tests/protocol_regression_test.c | 44 ++++++++++++++++++++++ 8 files changed, 219 insertions(+), 39 deletions(-) create mode 100644 src/lib/libptouch_protocol.c create mode 100644 src/lib/libptouch_protocol.h create mode 100644 tests/protocol_regression_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 2eecb03..84fc02b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ configure_file( set(LIBPTOUCH_SOURCES src/lib/libptouch_core.c src/lib/libptouch_usb.c + src/lib/libptouch_protocol.c src/lib/libptouch_layout.c src/lib/libptouch_media_info.c src/lib/libptouch_trim.c @@ -74,6 +75,19 @@ if(NOT MSVC) target_compile_options(ptouch-print PRIVATE -Wall -Wextra -Wpedantic) endif() +enable_testing() +add_executable(ptouch-protocol-regression-test + tests/protocol_regression_test.c + src/lib/libptouch_protocol.c +) +target_include_directories(ptouch-protocol-regression-test PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src/lib" +) +if(NOT MSVC) + target_compile_options(ptouch-protocol-regression-test PRIVATE -Wall -Wextra -Wpedantic) +endif() +add_test(NAME protocol_regression_test COMMAND ptouch-protocol-regression-test) + install(TARGETS ptouch ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}") install(TARGETS ptouch_shared LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") diff --git a/README.md b/README.md index 4069a70..f2cbb0c 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,16 @@ Brother P-touch シリーズ向けのラベル印刷用 **C コアライブラ ## レイアウト -| パス | 内容 | -|------|------| -| `include/libptouch.h` | 公開 API | -| `src/lib/libptouch_*.c` | ライブラリ本体(core / usb / print / status / png / svg) | -| `src/cli/main.c` | `ptouch-print` エントリ | -| `samples/` | 試験用サンプル画像の置き場(PNG/SVG 等) | -| `ruby/` | Ruby FFI gem(`libptouch`)・コマンド `ptouch-print-png`(PNG のみ)— `ruby/README.md` | -| `reference/` | 仕様・参考資料(例: ラスター PDF) | + +| パス | 内容 | +| ----------------------- | --------------------------------------------------------------------------- | +| `include/libptouch.h` | 公開 API | +| `src/lib/libptouch_*.c` | ライブラリ本体(core / usb / print / status / png / svg) | +| `src/cli/main.c` | `ptouch-print` エントリ | +| `samples/` | 試験用サンプル画像の置き場(PNG/SVG 等) | +| `ruby/` | Ruby FFI gem(`libptouch`)・コマンド `ptouch-print-png`(PNG のみ)— `ruby/README.md` | +| `reference/` | 仕様・参考資料(例: ラスター PDF) | + ## ビルド @@ -24,6 +26,7 @@ Brother P-touch シリーズ向けのラベル印刷用 **C コアライブラ ```bash cmake -S . -B build cmake --build build +ctest --test-dir build --output-on-failure ``` 成果物(`build/` 以下): @@ -96,7 +99,7 @@ cmake --build build - 接続は **libusb-1.0** のみ。機種ごとに **VID/PID**(`lsusb` 等)を調べ、`libptouch_open_usb_vid_pid` に渡すか、既定の PT-P900W なら `libptouch_open_usb` を使います。PT-P750W は **04f9:2062**、PT-P710BT は **04f9:20af**(`include/libptouch.h` の定数参照)。 - PT-P750W / PT-P710BT ではラスター幅方向は **180 dpi**(P900W は 360 dpi)。PNG から印刷する場合は解像度に合わせて画像を用意してください。 -- ラスターコマンドの詳細は **`reference/` の PDF** および Brother 公開資料に沿って `src/lib/libptouch_*.c` に実装してください。 +- ラスターコマンドの詳細は `**reference/` の PDF** および Brother 公開資料に沿って `src/lib/libptouch_*.c` に実装してください。 ### Ubuntu で sudo なしで USB を開く(udev) @@ -115,4 +118,4 @@ sudo usermod -aG plugdev "$USER" ## ライセンス -[MIT License](LICENSE)(`LICENSE` ファイルを参照)。 +[MIT License](LICENSE)(`LICENSE` ファイルを参照)。 \ No newline at end of file diff --git a/src/cli/main.c b/src/cli/main.c index fbf8fdb..4ed60e1 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -24,6 +24,7 @@ static void usage(const char *argv0) " -t, --threshold N しきい値 0–255(既定 %u、PNG/SVG)\n" " --trim-right[=DOTS] 右側空白を削減(省略時は左余白、失敗時 0)\n" " -n, --dry-run 読み込みと check_raster のみ(USB なし)\n" + " -v, --verbose 印刷前情報と印刷後ステータスを標準出力\n" " -p, --pid HEX USB 製品 ID(既定: P900W の 0x2085)。例: P750W 0x2062、P710BT 0x20af\n" " -S, --status ステータスを表示して終了\n" " -V, --version バージョンを表示して終了\n" @@ -101,6 +102,53 @@ static int read_file(const char *path, uint8_t **out, size_t *out_len) return 0; } +static void verbose_print_pre_print_info(libptouch_ctx *ctx, + const libptouch_raster_params_t *params, + size_t data_len) +{ + if (!ctx || !params) + return; + + printf("=== verbose: pre-print info ===\n"); + printf("raster bytes: %zu\n", data_len); + printf("raster size: %ux%u dots (length x width)\n", + (unsigned)params->width_dots, (unsigned)params->height_dots); + printf("margin_mm: %u\n", (unsigned)params->margin_mm); + + libptouch_media_info_t mi; + if (libptouch_get_current_media_info(ctx, &mi) == LIBPTOUCH_OK) { + printf("media width code: 0x%02X\n", (unsigned)mi.media_width_code); + printf("media kind code: 0x%02X\n", (unsigned)mi.media_kind_code); + printf("tape width: %.1f mm\n", mi.tape_width_mm); + printf("printable dots: %u\n", (unsigned)mi.printable_dots); + printf("left/right margins: %u/%u dots\n", + (unsigned)mi.left_margin_dots, (unsigned)mi.right_margin_dots); + printf("print/feed dpi: %.1f/%.1f\n", mi.print_dpi, mi.feed_dpi); + } else { + printf("media info: unavailable (%s)\n", libptouch_strerror(ctx)); + } + + uint8_t st[LIBPTOUCH_STATUS_LENGTH]; + if (libptouch_get_status(ctx, st) == LIBPTOUCH_OK) { + libptouch_status_fprint(stdout, st); + } else { + printf("status: unavailable (%s)\n", libptouch_strerror(ctx)); + } +} + +static void verbose_print_post_print_status(libptouch_ctx *ctx, const char *label) +{ + if (!ctx) + return; + printf("=== verbose: post-print status (%s) ===\n", label); + uint8_t st[LIBPTOUCH_STATUS_LENGTH]; + if (libptouch_get_status(ctx, st) == LIBPTOUCH_OK) { + libptouch_status_fprint(stdout, st); + } else { + printf("status: unavailable (%s)\n", libptouch_strerror(ctx)); + } +} + int main(int argc, char **argv) { const char *file = NULL; @@ -108,6 +156,7 @@ int main(int argc, char **argv) unsigned threshold = LIBPTOUCH_PNG_DEFAULT_THRESHOLD; unsigned usb_pid_arg = 0; int dry_run = 0; + int verbose = 0; int trim_right_enabled = 0; int trim_right_auto = 0; unsigned trim_right_dots = 0; @@ -120,6 +169,7 @@ int main(int argc, char **argv) { "threshold", required_argument, NULL, 't' }, { "trim-right", optional_argument, NULL, 'r' }, { "dry-run", no_argument, NULL, 'n' }, + { "verbose", no_argument, NULL, 'v' }, { "pid", required_argument, NULL, 'p' }, { "status", no_argument, NULL, 'S' }, { "version", no_argument, NULL, 'V' }, @@ -128,7 +178,7 @@ int main(int argc, char **argv) }; int c; - while ((c = getopt_long(argc, argv, "w:H:f:t:r::p:nhSV", longopts, NULL)) != + while ((c = getopt_long(argc, argv, "w:H:f:t:r::p:nvhSV", longopts, NULL)) != -1) { switch (c) { case 'w': @@ -151,6 +201,9 @@ int main(int argc, char **argv) case 'n': dry_run = 1; break; + case 'v': + verbose = 1; + break; case 'r': trim_right_enabled = 1; if (!optarg) { @@ -397,8 +450,13 @@ int main(int argc, char **argv) } } + if (verbose) + verbose_print_pre_print_info(ctx, ¶ms, data_len); + e = libptouch_print_raster(ctx, data, data_len, ¶ms); if (e != LIBPTOUCH_OK) { + if (verbose) + verbose_print_post_print_status(ctx, "print failed"); fprintf(stderr, "print_raster: %s\n", libptouch_strerror(ctx)); libptouch_close(ctx); @@ -410,6 +468,9 @@ int main(int argc, char **argv) return 1; } + if (verbose) + verbose_print_post_print_status(ctx, "print success"); + libptouch_close(ctx); libptouch_destroy(ctx); if (data_from_lib) diff --git a/src/lib/libptouch_print.c b/src/lib/libptouch_print.c index 2e7e2f0..c9bcbc1 100644 --- a/src/lib/libptouch_print.c +++ b/src/lib/libptouch_print.c @@ -7,6 +7,7 @@ #include "libptouch_internal.h" #include "libptouch_layout.h" +#include "libptouch_protocol.h" #include #include @@ -102,7 +103,7 @@ libptouch_err_t libptouch_print_raster(libptouch_ctx *ctx, (void)right_dots; unsigned head_dots = prof->head_width_dots; - size_t line_payload = (size_t)((head_dots + 7u) / 8u); + size_t line_payload = ptouch_line_payload_bytes(head_dots); size_t gf_packet = 3u + line_payload; uint32_t wd = params->width_dots; @@ -139,21 +140,6 @@ libptouch_err_t libptouch_print_raster(libptouch_ctx *ctx, } uint32_t lines = ht; - uint8_t n5 = (uint8_t)(lines & 0xFFu); - uint8_t n6 = (uint8_t)((lines >> 8) & 0xFFu); - uint8_t n7 = (uint8_t)((lines >> 16) & 0xFFu); - uint8_t n8 = (uint8_t)((lines >> 24) & 0xFFu); - - uint8_t n2_paper = 0x09u; - if (media_kind == 0x03u) - n2_paper = 0x00u; - else if (media_kind == 0x11u) - n2_paper = 0x11u; - else if (media_kind == 0x17u) - n2_paper = 0x17u; - else if (media_kind == 0x13u) - n2_paper = 0x13u; - uint8_t head[256]; size_t pos = 0; memset(head + pos, 0, 200); @@ -165,18 +151,28 @@ libptouch_err_t libptouch_print_raster(libptouch_ctx *ctx, memcpy(head + pos, raster_mode, sizeof(raster_mode)); pos += sizeof(raster_mode); - uint8_t esc_iz[] = { 0x1B, 0x69, 0x7A, 0x0Eu, n2_paper, media_w, - 0x00u, n5, n6, n7, n8, 0x02u, 0x00u }; + uint8_t esc_iz[13]; + ptouch_fill_esc_iz(esc_iz, media_kind, media_w, lines); memcpy(head + pos, esc_iz, sizeof(esc_iz)); pos += sizeof(esc_iz); static const uint8_t esc_im[] = { 0x1B, 0x69, 0x4D, 0x40 }; memcpy(head + pos, esc_im, sizeof(esc_im)); pos += sizeof(esc_im); - static const uint8_t esc_ia[] = { 0x1B, 0x69, 0x41, 0x01 }; - memcpy(head + pos, esc_ia, sizeof(esc_ia)); - pos += sizeof(esc_ia); - static const uint8_t esc_ik[] = { 0x1B, 0x69, 0x4B, 0x0C }; + + /* + * ESC i A ("cut each n labels") is not supported on PT-P710BT class + * devices. Sending it can trigger a communication error (red LED blink). + * Keep it only for 560-dot family. + */ + if (prof->head_width_dots > 128u) { + static const uint8_t esc_ia[] = { 0x1B, 0x69, 0x41, 0x01 }; + memcpy(head + pos, esc_ia, sizeof(esc_ia)); + pos += sizeof(esc_ia); + } + + /* Chain printing off; safer default for 128-dot family as well. */ + static const uint8_t esc_ik[] = { 0x1B, 0x69, 0x4B, 0x08 }; memcpy(head + pos, esc_ik, sizeof(esc_ik)); pos += sizeof(esc_ik); @@ -199,7 +195,6 @@ libptouch_err_t libptouch_print_raster(libptouch_ctx *ctx, } size_t row_b = ((size_t)wd + 7u) / 8u; - static const uint8_t g_hdr[] = { 0x47, 0x46, 0x00 }; uint8_t *gbuf = (uint8_t *)malloc(gf_packet); if (!gbuf) { @@ -213,7 +208,7 @@ libptouch_err_t libptouch_print_raster(libptouch_ctx *ctx, const uint8_t *row = src + (size_t)y * row_b; pack_line(gbuf + 3, line_payload, head_dots, row, wd, left_dots, print_dots); - memcpy(gbuf, g_hdr, sizeof(g_hdr)); + ptouch_fill_gf_header(gbuf, line_payload); v = ptouch_bulk_send_job(ctx, gbuf, gf_packet, "raster line"); if (v != LIBPTOUCH_OK) { free(gbuf); diff --git a/src/lib/libptouch_protocol.c b/src/lib/libptouch_protocol.c new file mode 100644 index 0000000..15b767e --- /dev/null +++ b/src/lib/libptouch_protocol.c @@ -0,0 +1,41 @@ +#include "libptouch_protocol.h" + +size_t ptouch_line_payload_bytes(unsigned head_dots) +{ + return (size_t)((head_dots + 7u) / 8u); +} + +void ptouch_fill_gf_header(uint8_t out[3], size_t line_payload_bytes) +{ + out[0] = 0x47u; + out[1] = (uint8_t)(line_payload_bytes & 0xFFu); + out[2] = (uint8_t)((line_payload_bytes >> 8) & 0xFFu); +} + +void ptouch_fill_esc_iz(uint8_t out[13], uint8_t media_kind, uint8_t media_width, + uint32_t raster_lines) +{ + uint8_t n2_paper = 0x09u; + if (media_kind == 0x03u) + n2_paper = 0x00u; + else if (media_kind == 0x11u) + n2_paper = 0x11u; + else if (media_kind == 0x17u) + n2_paper = 0x17u; + else if (media_kind == 0x13u) + n2_paper = 0x13u; + + out[0] = 0x1Bu; + out[1] = 0x69u; + out[2] = 0x7Au; + out[3] = 0x0Eu; + out[4] = n2_paper; + out[5] = media_width; + out[6] = 0x00u; + out[7] = (uint8_t)(raster_lines & 0xFFu); + out[8] = (uint8_t)((raster_lines >> 8) & 0xFFu); + out[9] = (uint8_t)((raster_lines >> 16) & 0xFFu); + out[10] = (uint8_t)((raster_lines >> 24) & 0xFFu); + out[11] = 0x00u; /* first page */ + out[12] = 0x00u; /* fixed */ +} diff --git a/src/lib/libptouch_protocol.h b/src/lib/libptouch_protocol.h new file mode 100644 index 0000000..702e6be --- /dev/null +++ b/src/lib/libptouch_protocol.h @@ -0,0 +1,12 @@ +#ifndef LIBPTOUCH_PROTOCOL_H +#define LIBPTOUCH_PROTOCOL_H + +#include +#include + +size_t ptouch_line_payload_bytes(unsigned head_dots); +void ptouch_fill_gf_header(uint8_t out[3], size_t line_payload_bytes); +void ptouch_fill_esc_iz(uint8_t out[13], uint8_t media_kind, uint8_t media_width, + uint32_t raster_lines); + +#endif /* LIBPTOUCH_PROTOCOL_H */ diff --git a/src/lib/libptouch_status.c b/src/lib/libptouch_status.c index a0cd3fd..07d37b8 100644 --- a/src/lib/libptouch_status.c +++ b/src/lib/libptouch_status.c @@ -9,6 +9,7 @@ #include #include +#include libptouch_err_t libptouch_get_status(libptouch_ctx *ctx, uint8_t *status) { @@ -28,18 +29,26 @@ libptouch_err_t libptouch_get_status(libptouch_ctx *ctx, uint8_t *status) static const uint8_t req[] = { 0x1B, 0x69, 0x53 }; int r = LIBUSB_ERROR_OTHER; - for (int attempt = 0; attempt < 2; attempt++) { + for (int attempt = 0; attempt < 5; attempt++) { + /* First status read after reconnect can fail on some P-touch units. */ + (void)libusb_clear_halt(h, ctx->bulk_in_ep); + (void)libusb_clear_halt(h, ctx->bulk_out_ep); if (attempt > 0) { - (void)libusb_clear_halt(h, ctx->bulk_in_ep); - (void)libusb_clear_halt(h, ctx->bulk_out_ep); + usleep(120000); /* 120ms backoff */ } r = ptouch_bulk_out(h, ctx->bulk_out_ep, init, 2, 5000u); if (r != 0) { + if (r == LIBUSB_ERROR_IO || r == LIBUSB_ERROR_PIPE || + r == LIBUSB_ERROR_TIMEOUT) + continue; ptouch_set_error_usb(ctx, r, "bulk OUT ESC @"); return LIBPTOUCH_ERR_USB; } r = ptouch_bulk_out(h, ctx->bulk_out_ep, req, 3, 5000u); if (r != 0) { + if (r == LIBUSB_ERROR_IO || r == LIBUSB_ERROR_PIPE || + r == LIBUSB_ERROR_TIMEOUT) + continue; ptouch_set_error_usb(ctx, r, "bulk OUT ESC i S"); return LIBPTOUCH_ERR_USB; } @@ -47,7 +56,8 @@ libptouch_err_t libptouch_get_status(libptouch_ctx *ctx, uint8_t *status) (int)LIBPTOUCH_STATUS_LENGTH, 5000u); if (r == 0) break; - if (r != LIBUSB_ERROR_IO && r != LIBUSB_ERROR_PIPE) + if (r != LIBUSB_ERROR_IO && r != LIBUSB_ERROR_PIPE && + r != LIBUSB_ERROR_TIMEOUT) break; } if (r != 0) { diff --git a/tests/protocol_regression_test.c b/tests/protocol_regression_test.c new file mode 100644 index 0000000..1ac0ba2 --- /dev/null +++ b/tests/protocol_regression_test.c @@ -0,0 +1,44 @@ +#include "libptouch_protocol.h" + +#include +#include + +static int expect_int(const char *name, int got, int want) +{ + if (got == want) + return 0; + fprintf(stderr, "%s mismatch: got=%d want=%d\n", name, got, want); + return 1; +} + +int main(void) +{ + int fail = 0; + + fail |= expect_int("line_payload_128", (int)ptouch_line_payload_bytes(128u), 16); + fail |= expect_int("line_payload_560", (int)ptouch_line_payload_bytes(560u), 70); + + uint8_t gf[3]; + ptouch_fill_gf_header(gf, 16u); + fail |= expect_int("gf16_cmd", gf[0], 0x47); + fail |= expect_int("gf16_n1", gf[1], 0x10); + fail |= expect_int("gf16_n2", gf[2], 0x00); + + ptouch_fill_gf_header(gf, 70u); + fail |= expect_int("gf70_cmd", gf[0], 0x47); + fail |= expect_int("gf70_n1", gf[1], 0x46); + fail |= expect_int("gf70_n2", gf[2], 0x00); + + uint8_t iz[13]; + ptouch_fill_esc_iz(iz, 0x01u, 0x0Cu, 70u); + fail |= expect_int("iz_cmd_0", iz[0], 0x1B); + fail |= expect_int("iz_cmd_1", iz[1], 0x69); + fail |= expect_int("iz_cmd_2", iz[2], 0x7A); + fail |= expect_int("iz_media_kind_map", iz[4], 0x09); + fail |= expect_int("iz_media_width", iz[5], 0x0C); + fail |= expect_int("iz_lines_lsb", iz[7], 70); + fail |= expect_int("iz_page_index", iz[11], 0x00); + fail |= expect_int("iz_last_fixed", iz[12], 0x00); + + return fail ? EXIT_FAILURE : EXIT_SUCCESS; +}