diff options
-rw-r--r-- | Makefile | 35 | ||||
-rw-r--r-- | README.md | 113 | ||||
-rw-r--r-- | build/.gitignore | 1 | ||||
-rw-r--r-- | build/.keepme | 0 | ||||
-rw-r--r-- | example.png | bin | 0 -> 542 bytes | |||
-rw-r--r-- | include/loadpng.h | 15 | ||||
-rw-r--r-- | include/ql.h | 155 | ||||
-rw-r--r-- | src/loadpng.c | 99 | ||||
-rw-r--r-- | src/main.c | 205 | ||||
-rw-r--r-- | src/ql.c | 333 |
10 files changed, 956 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d170eb3 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +default: build/qlprint + +# Get rid of most of the implicit rules by clearing the .SUFFIXES target +.SUFFIXES: +# Get rid of the auto-checkout from old version control systems rules +%: %,v +%: RCS/%,v +%: RCS/% +%: s.% +%: SCCS/s.% + + +CFLAGS=-std=c11 -Wall -Wextra -g -Iinclude -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809 $(shell pkg-config --cflags libpng) +LDFLAGS=$(shell pkg-config --libs libpng) + +OBJS=$(addprefix build/, \ + main.o \ + ql.o \ + loadpng.o \ +) + +vpath %.c src + +build/%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +build/qlprint: $(OBJS) + $(CC) $(LDFLAGS) $^ -o $@ + +$(OBJS): $(wildcard include/*) Makefile + +.PHONY: clean +clean: + -rm -f build/* + diff --git a/README.md b/README.md new file mode 100644 index 0000000..425cf39 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# qlprint +Command-line utility for printing to Brother QL series label printers. + +Tested with: + * QL-570 + +Hopefully also works with: + * QL-500/550 + * QL-560 + * QL-580N + * QL-650TD + * QL-700 + * QL-710W + * QL-720W + * QL-1050 + * QL-1060N + +Inspired, though not derived from, the [ql570](https://github.com/sudomesh/ql570.git) tool. Licensed under the GPLv3 nevertheless. + +## Building +Requires `GNU make` and `libpng` (with development headers), with `pkg-config` to locate libpng headers & libs. + +Simply run `make` in this directory, e.g.: +``` +$ make +cc -std=c11 -Wall -Wextra -g -Iinclude -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809 -I/usr/include/libpng12 -c src/main.c -o build/main.o +cc -std=c11 -Wall -Wextra -g -Iinclude -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809 -I/usr/include/libpng12 -c src/ql.c -o build/ql.o +cc -std=c11 -Wall -Wextra -g -Iinclude -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809 -I/usr/include/libpng12 -c src/loadpng.c -o build/loadpng.o +cc -lpng12 build/main.o build/ql.o build/loadpng.o -o build/qlprint +$ +``` + +## Running +``` +Syntax: + qlprint [-p lp] -i + [-p lp] [-m margin] [-a] [-C|-D] [-W width] [-L length] [-Q] [-n num] [-t threshold] png... +Where: + -p lp Printer port (default /dev/usb/lp0) + -i Print status information only, then exit + -m margin Margin (dots) + -a Enable auto-cut + -C Request continuous-length-tape when printing (error if not) + -D Request die-cut-labels when printing (error if not) + -W width Request particular width media when printing (error if not) + -L length Request particular length media when printing (error if not) + -Q Prioritise quality of speed + -n num Print num copies + -t threshold Threshold for black-vs-white (default 128, i.e. 0-127=black) + png... One or more png files to print + +``` + +The PNG files are converted to monochrome internally. The black-vs-white +threshold for this conversion may be tuned with the `-t threshold` argument. + +Image height is limited to the capability of the printer (720 for most, 1296 +for 1050/1060N models). Attempting to print larger images will fail. + +On successful printing, the exit code is zero; in case of any error, the exit +code is non-zero and an error message is printed to stderr. + +## Examples + +### Show printer status information +Here with a narrow continuous-length-tape cartridge loaded. +``` +$ ./build/qlprint -i + Printer: QL-570 + Mode: no-auto-cut + Errors: none + Media type: continuous-length-tape + Media width (mm): 29 +$ +``` + +### Printing with auto-cutter enabled: +``` +$ ./build/qlprint -a example.png +example.png (135x135) OK +$ +``` + +### Printing two images, cutting only once +``` +$ ./build/qlprint -a example.png example.png +example.png (135x135) OK +example.png (135x135) OK +$ +``` + +### Printing two copies of the one image, cutting after each +``` +$ ./build/qlprint -a -n 2 example.png +example.png (135x135) OK +example.png (135x135) OK +$ +``` + +### Print only on the correct media type +Assuming a continuous-length-tape cartridge is installed: +``` +$ ./build/qlprint -C example.png +example.png (135x135) OK +$ +``` +...otherwise: +``` +$ ./build/qlprint -C example.png +Printer reported error(s): replace-media +$ +``` + diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/build/.gitignore @@ -0,0 +1 @@ +* diff --git a/build/.keepme b/build/.keepme new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/build/.keepme diff --git a/example.png b/example.png Binary files differnew file mode 100644 index 0000000..f5f8c41 --- /dev/null +++ b/example.png diff --git a/include/loadpng.h b/include/loadpng.h new file mode 100644 index 0000000..cace43a --- /dev/null +++ b/include/loadpng.h @@ -0,0 +1,15 @@ +/* + * Copyright 2017 DiUS Computing Pty Ltd. All rights reserved. + * + * Released under GPLv3, see LICENSE for details. + * + * @author Johny Mattsson <jmattsson@dius.com.au> + */ +#ifndef _LOADPNG_H_ +#define _LOADPNG_H_ + +#include "ql.h" + +ql_raster_image_t *loadpng(const char *path); + +#endif diff --git a/include/ql.h b/include/ql.h new file mode 100644 index 0000000..b854c06 --- /dev/null +++ b/include/ql.h @@ -0,0 +1,155 @@ +/* + * Copyright 2017 DiUS Computing Pty Ltd. All rights reserved. + * + * Released under GPLv3, see LICENSE for details. + * + * @author Johny Mattsson <jmattsson@dius.com.au> + */ +#ifndef _QL_H_ +#define _QL_H_ + +#include <stdint.h> +#include <stdbool.h> +#include <stdio.h> +#include <unistd.h> + +/* The QL module is based on an amalgamation of information from: + * + * - Brother QL-500/550/560/570/580N/650TD/700/1050/1060N Command Reference + * http://download.brother.com/welcome/docp000678/cv_qlseries_eng_raster_600.pdf + * - Software Developer's Manual Raster Command Reference QL-710W/720NW + * http://download.brother.com/welcome/docp000698/cv_ql710720_eng_raster_100.pdf + * - Actual experience communicating with a QL-570 + * + */ + +typedef struct +{ + uint8_t print_head_mark; + uint8_t sz; + uint8_t rsvd_2; // 'B' + uint8_t model_class; + uint8_t model_code; + uint8_t rsvd_5; // '0' + uint8_t rsvd_6; // '0' + uint8_t rsvd_7; // 0x00 + uint8_t err_info_1; + uint8_t err_info_2; + uint8_t media_width_mm; + uint8_t media_type; + uint8_t rsvd_12; // 0x00 + uint8_t rsvd_13; // 0x00 + uint8_t rsvd_14; // 0x3f + uint8_t mode; + uint8_t rsvd_16; // 0x00 + uint8_t media_length_mm; + uint8_t status_type; + uint8_t phase_type; + uint8_t phase_hi; + uint8_t phase_lo; + uint8_t notification; + uint8_t rsvd_23; // 0x00 + uint8_t rsvd_24[8]; // 0x00... +} ql_status_t; + +#define QL_ERR_1_NO_MEDIA 0x01 +#define QL_ERR_1_END_OF_MEDIA 0x02 +#define QL_ERR_1_CUTTER_JAM 0x04 +// 0x08 not defined +#define QL_ERR_1_PRINTER_IN_USE 0x10 +#define QL_ERR_1_PRINTER_TURNED_OFF 0x20 +#define QL_ERR_1_HIGH_VOLTAGE_ADAPTER 0x40 +#define QL_ERR_1_FAN_MOTOR_ERROR 0x80 + +#define QL_ERR_2_REPLACE_MEDIA 0x01 +#define QL_ERR_2_EXPANSION_BUFFER_FULL 0x02 +#define QL_ERR_2_COMMUNICATION_ERROR 0x04 +#define QL_ERR_2_COMMUNICATION_BUFFER_FULL 0x08 +#define QL_ERR_2_COVER_OPEN 0x10 +#define QL_ERR_2_CANCEL_KEY 0x20 +#define QL_ERR_2_MEDIA_CANNOT_BE_FED 0x40 +#define QL_ERR_2_SYSTEM_ERROR 0x80 + +#define QL_MEDIA_LENGTH_CONTINUOUS 0x00 + +#define QL_MEDIA_TYPE_NO_MEDIA 0x00 +#define QL_MEDIA_TYPE_CONTINUOUS 0x0a +#define QL_MEDIA_TYPE_DIECUT_LABELS 0x0b +// The 710/720 might report these instead +#define QL_MEDIA_TYPE_CONTINUOUS_ALT 0x4a +#define QL_MEDIA_TYPE_DIECUT_LABELS_ALT 0x4b + +// Flags for mode +#define QL_MODE_NO_AUTOCUT 0x00 +#define QL_MODE_AUTOCUT 0x40 + +#define QL_STATUS_TYPE_REPLY 0x00 +#define QL_STATUS_TYPE_PRINTING_DONE 0x01 +#define QL_STATUS_TYPE_ERROR_OCCURRED 0x02 +#define QL_STATUS_TYPE_TURNED_OFF 0x04 +#define QL_STATUS_TYPE_NOTIFICATION 0x05 +#define QL_STATUS_TYPE_PHASE_CHANGE 0x06 + +#define QL_PHASE_TYPE_RECEIVING 0x00 +#define QL_PHASE_TYPE_PRINTING 0x01 + +#define QL_NOTIFICATION_NONE 0x00 +#define QL_NOTIFICATION_COOLING_STARTED 0x03 +#define QL_NOTIFICATION_COOLING_DONE 0x04 + +// Flags for expanded mode +#define QL_EXPANDED_MODE_CUT_AT_END 0x10 /* Gah, 710 doc claims 0x08! */ +#define QL_EXPANDED_MODE_HIGH_RES 0x40 /* QL-570/580N/700 */ + +typedef struct { + uint16_t width; + uint16_t height; + uint8_t data[]; +} ql_raster_image_t; + +typedef struct { + uint8_t threshold; // pixel values below threshold deemed black + uint8_t flags; // QL_PRINT_CFG_xxx flags, indicating which other fields valid + uint8_t media_type; + uint8_t media_width; + uint8_t media_length; + bool first_page; // used for autocut pagination +} ql_print_cfg_t; + +#define QL_PRINT_CFG_MEDIA_TYPE 0x02 +#define QL_PRINT_CFG_MEDIA_WIDTH 0x04 +#define QL_PRINT_CFG_MEDIA_LENGTH 0x08 +#define QL_PRINT_CFG_QUALITY_PRIO 0x40 + +typedef struct ql_ctx *ql_ctx_t; + +ql_ctx_t ql_open(const char *printer); +void ql_close(ql_ctx_t ctx); + +bool ql_init(ql_ctx_t ctx); // also cancel +bool ql_request_status(ql_ctx_t ctx); +bool ql_read_status(ql_ctx_t ctx, ql_status_t *status); + +bool ql_needs_mode_switch(const ql_status_t *status); +bool ql_switch_to_raster_mode(ql_ctx_t ctx); + +bool ql_set_mode(ql_ctx_t ctx, unsigned mode); +bool ql_set_expanded_mode(ql_ctx_t ctx, unsigned mode); +bool ql_set_autocut_every_n(ql_ctx_t ctx, uint8_t n); +bool ql_set_margin(ql_ctx_t ctx, uint16_t dots); + +// Note: status needed for 1050/1060N detection to adjust command format +bool ql_print_raster_image(ql_ctx_t ctx, const ql_status_t *status, const ql_raster_image_t *img, const ql_print_cfg_t *cfg); + +// Caution: ql_decode_*() are *not* multi-thread safe +const char *ql_decode_mode(const ql_status_t *status); +const char *ql_decode_errors(const ql_status_t *status); +const char *ql_decode_model(const ql_status_t *status); +const char *ql_decode_media_type(const ql_status_t *status); +#define QL_DECODE_MODEL 0x01 +#define QL_DECODE_ERROR 0x02 +#define QL_DECODE_MEDIA 0x04 +#define QL_DECODE_MODE 0x08 +void ql_decode_print_status(FILE *out, const ql_status_t *status, unsigned flags); + +#endif diff --git a/src/loadpng.c b/src/loadpng.c new file mode 100644 index 0000000..c986ce2 --- /dev/null +++ b/src/loadpng.c @@ -0,0 +1,99 @@ +/* + * Copyright 2017 DiUS Computing Pty Ltd. All rights reserved. + * + * Released under GPLv3, see LICENSE for details. + * + * @author Johny Mattsson <jmattsson@dius.com.au> + */ +#include "loadpng.h" +#include <stdlib.h> +#include <assert.h> +#include <png.h> + +static_assert(sizeof(png_byte) == sizeof( ((ql_raster_image_t *)0)->data[0]), "Code relies on png_byte being compatible with ql_raster_image_t data "); + +ql_raster_image_t *loadpng(const char *path) +{ + ql_raster_image_t *ret = NULL; + + if (!path) + goto out; + + FILE *f = fopen(path, "rb"); + if (!f) + goto out; + + uint8_t header[8]; + if (fread(header, 1, sizeof(header), f) != 8) + goto close_out; + + if (!png_check_sig(header, sizeof(header))) + goto close_out; + + png_infop info_ptr = NULL, end_ptr = NULL; + png_structp png_ptr = + png_create_read_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png_ptr) + goto close_out; + + info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) + goto destroy_read_out; + + end_ptr = png_create_info_struct(png_ptr); + if (!end_ptr) + goto destroy_read_out; + + if (setjmp(png_jmpbuf(png_ptr))) + goto destroy_read_out; + + png_init_io(png_ptr, f); + png_set_sig_bytes(png_ptr, sizeof(header)); + + png_read_info(png_ptr, info_ptr); + + png_set_strip_alpha(png_ptr); + png_set_rgb_to_gray_fixed(png_ptr, 1, -1, -1); // force into grayscale + png_set_expand_gray_1_2_4_to_8(png_ptr); // get us a known output format + + png_read_update_info(png_ptr, info_ptr); + + png_uint_32 width = png_get_image_width(png_ptr, info_ptr); + png_uint_32 height = png_get_image_height(png_ptr, info_ptr); + + png_bytepp row_ptrs = calloc(height, sizeof(png_bytep)); + if (!row_ptrs) + goto destroy_read_out; + + const unsigned row_bytes = width * sizeof(png_byte); + ql_raster_image_t *img = + calloc(1, sizeof(ql_raster_image_t) + height * row_bytes); + if (!img) + goto free_image_out; + + if (setjmp(png_jmpbuf(png_ptr))) + goto free_image_out; + + for (png_uint_32 i = 0; i < height; ++i) + row_ptrs[i] = (png_bytep)(img->data + (i * row_bytes)); + + png_read_image(png_ptr, row_ptrs); + png_read_end(png_ptr, end_ptr); + + img->height = height; + img->width = width; + + ret = img; + img = NULL; // don't free it, we're returning it now + +free_image_out: + free(img); + free(row_ptrs); +destroy_read_out: + png_destroy_read_struct( + &png_ptr, info_ptr ? &info_ptr : NULL, end_ptr ? &end_ptr : NULL); +close_out: + fclose(f); +out: + return ret; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..4b34ec1 --- /dev/null +++ b/src/main.c @@ -0,0 +1,205 @@ +/* + * Copyright 2017 DiUS Computing Pty Ltd. All rights reserved. + * + * Released under GPLv3, see LICENSE for details. + * + * @author Johny Mattsson <jmattsson@dius.com.au> + */ +#include "ql.h" +#include "loadpng.h" +#include <errno.h> +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <signal.h> + +static bool timed_out = false; +void on_alarm(int ignored) +{ + (void)ignored; + timed_out = true; +} + + +void syntax(void) +{ + fprintf(stderr, +"Syntax:\n" +" qlprint [-p lp] -i\n" +" [-p lp] [-m margin] [-a] [-C|-D] [-W width] [-L length] [-Q] [-n num] [-t threshold] png...\n" +"Where:\n" +" -p lp Printer port (default /dev/usb/lp0)\n" +" -i Print status information only, then exit\n" +" -m margin Margin (dots)\n" +" -a Enable auto-cut\n" +" -C Request continuous-length-tape when printing (error if not)\n" +" -D Request die-cut-labels when printing (error if not)\n" +" -W width Request particular width media when printing (error if not)\n" +" -L length Request particular length media when printing (error if not)\n" +" -Q Prioritise quality of speed\n" +" -n num Print num copies\n" +" -t threshold Threshold for black-vs-white (default 128, i.e. 0-127=black)\n" +" png... One or more png files to print\n" +"\n"); + + exit(EXIT_FAILURE); +} + +int main (int argc, char *argv[]) +{ + bool info_only = false; + int32_t margin = -1; + bool autocut = false; + int num = 1; + ql_print_cfg_t cfg = { + .threshold = 0x80, + .flags = 0 + }; + int opt; + const char *printer = "/dev/usb/lp0"; + while ((opt = getopt(argc, argv, "ip:m:an:CDW:L:Q")) != -1) + { + switch(opt) + { + case 'i': info_only = true; break; + case 'p': printer = optarg; break; + case 'm': margin = atoi(optarg); break; + case 'a': autocut = true; break; + case 'n': num = atoi(optarg); break; + case 'C': cfg.media_type = QL_MEDIA_TYPE_CONTINUOUS; + cfg.flags |= QL_PRINT_CFG_MEDIA_TYPE; break; + case 'D': cfg.media_type = QL_MEDIA_TYPE_DIECUT_LABELS; + cfg.flags |= QL_PRINT_CFG_MEDIA_TYPE; break; + case 'W': cfg.media_width = atoi(optarg); + cfg.flags |= QL_PRINT_CFG_MEDIA_WIDTH; break; + case 'L': cfg.media_length = atoi(optarg); + cfg.flags |= QL_PRINT_CFG_MEDIA_LENGTH; break; + case 'Q': cfg.flags |= QL_PRINT_CFG_QUALITY_PRIO; break; + default: syntax(); + } + } + + if (optind >= argc && !info_only) + syntax(); + + ql_ctx_t ctx = ql_open(printer); + if (!ctx) + { + fprintf(stderr, "Unable to open '%s': %s\n", printer, strerror(errno)); + return EXIT_FAILURE; + } + + if (!ql_init(ctx)) + { + fprintf(stderr, "Failed to send initialisation sequence to printer: %s\n", + strerror(errno)); + return EXIT_FAILURE; + } + + if (!ql_request_status(ctx)) + { + fprintf(stderr, "Failed to request status from printer: %s\n", + strerror(errno)); + return EXIT_FAILURE; + } + + ql_status_t status = { 0, }; + if (!ql_read_status(ctx, &status)) + { + fprintf(stderr, "Failed to read status from printer: %s\n", + strerror(errno)); + return EXIT_FAILURE; + } + +/* +for (int i = 0; i < 32; ++i) + printf("%02hhx ", ((char *)&status)[i]); +printf("\n"); +*/ + + if (info_only) + { + ql_decode_print_status(stdout, &status, + QL_DECODE_MODEL | QL_DECODE_MEDIA | QL_DECODE_ERROR | QL_DECODE_MODE); + return EXIT_SUCCESS; + } + + if (margin >= 0 && !ql_set_margin(ctx, (uint16_t)margin)) + { + fprintf(stderr, "Failed to set margin: %s\n", strerror(errno)); + return EXIT_FAILURE; + } + if (autocut && + (!ql_set_mode(ctx, QL_MODE_AUTOCUT) || + !ql_set_autocut_every_n(ctx, argc - optind))) + { + fprintf(stderr, "Failed to set autocut: %s\n", strerror(errno)); + return EXIT_FAILURE; + } + + if (ql_needs_mode_switch(&status) && !ql_switch_to_raster_mode(ctx)) + { + fprintf(stderr, "Failed to set raster mode: %s\n", strerror(errno)); + return EXIT_FAILURE; + } + + signal(SIGALRM, on_alarm); + while (num--) + { + cfg.first_page = true; + for (int i = optind; i < argc; ++i) + { + ql_raster_image_t *img = loadpng(argv[i]); + if (!img) + { + fprintf(stderr, "Failed to load image '%s'\n", argv[i]); + return EXIT_FAILURE; + } +/* +for(int i = 0; i < img->height; ++i) +{ + for(int j = 0; j < img->width; ++j) + printf("%c", img->data[i*img->width + j] < cfg.threshold ? '#' : '.'); + printf("\n"); +} +*/ + if (!ql_print_raster_image(ctx, &status, img, &cfg)) + { + fprintf(stderr, "Failed to print '%s' (%ux%u)\n", + argv[i], img->width, img->height); + return EXIT_FAILURE; + } + alarm(5); + do { + if (!ql_read_status(ctx, &status)) + { + if (!timed_out) // try again, soon + { + usleep(50); + continue; + } + fprintf(stderr, "Printer stopped responding!\n"); + return EXIT_FAILURE; + } + if (status.err_info_1 || status.err_info_2) + { + fprintf(stderr, "Printer reported error(s): %s\n", + ql_decode_errors(&status)); + return EXIT_FAILURE; + } + } while (status.status_type != QL_STATUS_TYPE_PRINTING_DONE); + alarm(0); + + printf("%s (%ux%u) OK\n", argv[i], img->width, img->height); + + free(img); + cfg.first_page = false; + } + } + + ql_close(ctx); + + return EXIT_SUCCESS; +} diff --git a/src/ql.c b/src/ql.c new file mode 100644 index 0000000..f98a5f4 --- /dev/null +++ b/src/ql.c @@ -0,0 +1,333 @@ +/* + * Copyright 2017 DiUS Computing Pty Ltd. All rights reserved. + * + * Released under GPLv3, see LICENSE for details. + * + * @author Johny Mattsson <jmattsson@dius.com.au> + */ +#include "ql.h" +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> + +struct ql_ctx +{ + char *printer; + int fd; +}; + +#define ESC 0x1b + +#define NUM_STATUS_READ_RETRIES 100 + +#define full_write(fd, buf) (retry_write(fd, buf, sizeof(buf)) == (ssize_t)sizeof(buf)) + +ssize_t retry_write(int fd, const char *buf, size_t len) +{ + size_t written = 0; + while (written != len) + { + ssize_t n = write(fd, buf + written, len - written); + if (n == -1) + { + if (errno == EAGAIN || errno == EINTR) + continue; + else + break; + } + else + written += n; + } + return written; +} + + +ql_ctx_t ql_open(const char *printer) +{ + int fd = open(printer, O_RDWR); + if (fd < 0) + return NULL; + + ql_ctx_t ctx = malloc(sizeof(struct ql_ctx)); + if (!ctx) + return NULL; + ctx->printer = strdup(printer); + ctx->fd = fd; + + const char clear[200] = { 0, }; + (void)full_write(fd, clear); // recommended to clear old/errored jobs + + return ctx; +} + +void ql_close(ql_ctx_t ctx) +{ + free(ctx->printer); + close(ctx->fd); + free(ctx); +} + +bool ql_init(ql_ctx_t ctx) +{ + const char init[] = { ESC, '@' }; + return full_write(ctx->fd, init); +} + + +bool ql_request_status(ql_ctx_t ctx) +{ + const char status_req[] = { ESC, 'i', 'S' }; + return full_write(ctx->fd, status_req); +} + + +bool ql_read_status(ql_ctx_t ctx, ql_status_t *status) +{ + for (int i = 0; i < NUM_STATUS_READ_RETRIES; ++i) + { + int ret = read(ctx->fd, status, sizeof(*status)); + if (ret == 32) + return true; + else if ( (ret == 0) // "no data yet, too bad we just eof'd your fd, sucker" + || (ret == -1 && errno == EBADF)) // in case we messed up, somehow + { + close(ctx->fd); + ctx->fd = open(ctx->printer, O_RDWR); + if (ctx->fd < 0) + return false; + } + else if (ret == -1 && (errno != EAGAIN || errno != EINTR)) + return false; // non-recoverable + } + errno = ETIME; + return false; +} + + +bool ql_set_mode(ql_ctx_t ctx, unsigned mode) +{ + char cmd[] = { ESC, 'i', 'M', mode }; + return full_write(ctx->fd, cmd); +} + + +bool ql_set_expanded_mode(ql_ctx_t ctx, unsigned mode) +{ + char cmd[] = { ESC, 'i', 'K', mode }; + return full_write(ctx->fd, cmd); +} + + +bool ql_set_autocut_every_n(ql_ctx_t ctx, uint8_t n) +{ + char cmd[] = { ESC, 'i', 'A', n }; + return full_write(ctx->fd, cmd); +} + + +bool ql_set_margin(ql_ctx_t ctx, uint16_t dots) +{ + char cmd[] = { ESC, 'i', 'd', dots & 0xff, dots >> 8}; + return full_write(ctx->fd, cmd); +} + + +bool ql_needs_mode_switch(const ql_status_t *status) +{ + switch(status->model_code) + { + case '3': case '4': + case 'P': case 'Q': + return true; + default: break; + } + return false; +} + + +bool ql_switch_to_raster_mode(ql_ctx_t ctx) +{ + #define MODE_ESC_P 0 + #define MODE_RASTER 1 + #define MODE_P_TOUCH_TEMPLATE 3 + char cmd[] = { ESC, 'i', 'a', MODE_RASTER }; + return full_write(ctx->fd, cmd); +} + + +static void pack_column(uint8_t *out, uint16_t bytes, uint16_t colno, const ql_raster_image_t *img, uint8_t black_below_v) +{ + for (unsigned n = 0; n < bytes; ++n, ++out) + { + *out = 0; + for (unsigned i = 0; i < 8; ++i) + { + unsigned img_row = n * 8 + i; + if (img_row < img->height) + if (img->data[img_row * img->width + colno] < black_below_v) + *out |= 1 << (7 - i); + } +//for(int i = 7; i >= 0; --i) fprintf(stderr, "%c", (*out & (1<<i)) ? '#' : '.'); + } +//fprintf(stderr,"\n"); +} + + +bool ql_print_raster_image(ql_ctx_t ctx, const ql_status_t *status, const ql_raster_image_t *img, const ql_print_cfg_t *cfg) +{ + unsigned dn = 90; // default raster transmission block size (720 pixels) + if (status->model_code == 'P' || status->model_code == '4') + dn = 162; // 1296 pixels + + if (img->width > dn * 8) + return false; // image too wide for printer + + char print_info[] = { ESC, 'i', 'z', + cfg->flags | 0x80, + (cfg->flags & QL_PRINT_CFG_MEDIA_TYPE) ? cfg->media_type : 0, + (cfg->flags & QL_PRINT_CFG_MEDIA_WIDTH) ? cfg->media_width : 0, + (cfg->flags & QL_PRINT_CFG_MEDIA_LENGTH) ? cfg->media_length : 0, + img->width & 0xff, img->width >> 8, 0, 0, + cfg->first_page ? 0 : 1, 0 }; + if (!full_write(ctx->fd, print_info)) + return false; + + for (unsigned w = 0; w < img->width; ++w) + { + char block[dn + 3]; + block[0] = 'g'; block[1] = 0; block[2] = dn; + pack_column((uint8_t *)block+3, dn, w, img, cfg->threshold); + if (!full_write(ctx->fd, block)) + return false; + } + + char done[] = { 0x1a }; // print with feeding + return full_write(ctx->fd, done); +} + + +const char *ql_decode_model(const ql_status_t *status) +{ + switch(status->model_code) + { + case '1': return "QL-560"; + case '2': return "QL-570"; + case '3': return "QL-580N"; + case '4': return "QL-1060N"; + case '5': return "QL-700"; + case '6': return "QL-710W"; + case '7': return "QL-720NW"; + case 'O': return "QL-500/550"; + case 'P': return "QL-1050"; + case 'Q': return "QL-650TD"; + default: + { + static char buf[] = "unrecognised (type code 0x$$)"; + char *q = strchr(buf, '$'); + sprintf(q, "%02hhx)", status->model_code); + return buf; + } + } +} + +const char *ql_decode_mode(const ql_status_t *status) +{ + if (status->mode & QL_MODE_AUTOCUT) + return "auto-cut"; + else + return "no-auto-cut"; +} + +const char *ql_decode_errors(const ql_status_t *status) +{ + typedef struct { + uint16_t bit; + const char *str; + } strmap_t; + + #define ERR1(x) (x << 0) + #define ERR2(x) (x << 8) + const strmap_t strmap[] = { + { ERR1(QL_ERR_1_NO_MEDIA), "no-media " }, + { ERR1(QL_ERR_1_END_OF_MEDIA), "end-of-media " }, + { ERR1(QL_ERR_1_CUTTER_JAM), "cutter-jam " }, + { ERR1(QL_ERR_1_PRINTER_IN_USE), "printer-in-use " }, + { ERR1(QL_ERR_1_PRINTER_TURNED_OFF), "printer-turned-off " }, + { ERR1(QL_ERR_1_HIGH_VOLTAGE_ADAPTER), "high-voltage-adapter " }, + { ERR1(QL_ERR_1_FAN_MOTOR_ERROR), "fan-motor-error " }, + + { ERR2(QL_ERR_2_REPLACE_MEDIA), "replace-media " }, + { ERR2(QL_ERR_2_EXPANSION_BUFFER_FULL), "expansion-buffer-full " }, + { ERR2(QL_ERR_2_COMMUNICATION_ERROR), "communication-error " }, + { ERR2(QL_ERR_2_COMMUNICATION_BUFFER_FULL), "communication-buffer-full " }, + { ERR2(QL_ERR_2_COVER_OPEN), "cover-open " }, + { ERR2(QL_ERR_2_CANCEL_KEY), "cancel-key-pressed " }, + { ERR2(QL_ERR_2_MEDIA_CANNOT_BE_FED), "media-cannot-be-fed " }, + { ERR2(QL_ERR_2_SYSTEM_ERROR), "system-error " } + }; + + static char *buf = 0; + if (!buf) + { + int len = 0; + for (unsigned i = 0; i < (sizeof(strmap)/sizeof(strmap[0])); ++i) + len += strlen(strmap[i].str); + buf = malloc(len + 1); + if (!buf) + return "<host-out-of-memory>"; + } + + uint16_t errs = ERR1(status->err_info_1) | ERR2(status->err_info_2); + buf[0] = 0; + for (unsigned i = 0; i < (sizeof(strmap)/sizeof(strmap[0])); ++i) + { + if (errs & strmap[i].bit) + strcat(buf, strmap[i].str); + } + return (buf[0] == 0) ? "none" : buf; +} + +const char *ql_decode_media_type(const ql_status_t *status) +{ + switch(status->media_type) + { + case QL_MEDIA_TYPE_NO_MEDIA: return "no-media"; + case QL_MEDIA_TYPE_CONTINUOUS: + case QL_MEDIA_TYPE_CONTINUOUS_ALT: + return "continuous-length-tape"; + case QL_MEDIA_TYPE_DIECUT_LABELS: + case QL_MEDIA_TYPE_DIECUT_LABELS_ALT: + return "die-cut-labels"; + default: { + static char buf[] = "unknown (code 0x$$)"; + char *q = strchr(buf, '$'); + sprintf(q, "%02hhx)", status->media_type); + return buf; + } + } +} + +void ql_decode_print_status(FILE *f, const ql_status_t *status, unsigned flags) +{ + if (!status) + return; + + const char *fmt_s = "%17s: %s\n"; + const char *fmt_u = "%17s: %u\n"; + if (flags & QL_DECODE_MODEL) + fprintf(f, fmt_s, "Printer", ql_decode_model(status)); + if (flags & QL_DECODE_MODE) + fprintf(f, fmt_s, "Mode", ql_decode_mode(status)); + if (flags & QL_DECODE_ERROR) + fprintf(f, fmt_s, "Errors", ql_decode_errors(status)); + if (flags & QL_DECODE_MEDIA) + { + fprintf(f, fmt_s, "Media type", ql_decode_media_type(status)); + fprintf(f, fmt_u, "Media width (mm)", status->media_width_mm); + if (status->media_type != QL_MEDIA_TYPE_CONTINUOUS) + fprintf(f, fmt_u, "Media length (mm)", status->media_length_mm); + } +} + |