1 /*
2  *   Mitsubishi P93D/P95D Monochrome Thermal Photo Printer CUPS backend
3  *
4  *   (c) 2016-2018 Solomon Peachy <pizza@shaftnet.org>
5  *
6  *   Development of this backend was sponsored by:
7  *
8  *     A benefactor who wishes to remain anonymous
9  *
10  *   The latest version of this program can be found at:
11  *
12  *     http://git.shaftnet.org/cgit/selphy_print.git
13  *
14  *   This program is free software; you can redistribute it and/or modify it
15  *   under the terms of the GNU General Public License as published by the Free
16  *   Software Foundation; either version 2 of the License, or (at your option)
17  *   any later version.
18  *
19  *   This program is distributed in the hope that it will be useful, but
20  *   WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
21  *   or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
22  *   for more details.
23  *
24  *   You should have received a copy of the GNU General Public License
25  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
26  *
27  *          [http://www.gnu.org/licenses/gpl-2.0.html]
28  *
29  *   SPDX-License-Identifier: GPL-2.0+
30  *
31  */
32 
33 #include <stdio.h>
34 #include <stdlib.h>
35 #include <string.h>
36 #include <unistd.h>
37 
38 #include <sys/types.h>
39 #include <sys/stat.h>
40 #include <fcntl.h>
41 #include <signal.h>
42 
43 #define BACKEND mitsup95d_backend
44 
45 #include "backend_common.h"
46 
47 #define USB_VID_MITSU       0x06D3
48 #define USB_PID_MITSU_P93D  0x0398
49 #define USB_PID_MITSU_P95D  0x3b10
50 
51 /* Private data structure */
52 struct mitsup95d_printjob {
53 	uint8_t *databuf;
54 	uint32_t datalen;
55 
56 	uint8_t hdr[2];  // 1b 51
57 	uint8_t hdr1[50]; // 1b 57 20 2e ...
58 	uint8_t hdr2[50]; // 1b 57 21 2e ...
59 	uint8_t hdr3[50]; // 1b 57 22 2e ...
60 	uint8_t hdr4[42];  // 1b 58 ...
61 	int hdr4_len;      // 36 (P95) or 42 (P93)
62 
63 	uint8_t plane[12]; // 1b 5a 74 00 ...
64 
65 	uint8_t mem_clr[4]; // 1b 5a 43 00
66 	int mem_clr_present;
67 
68 	uint8_t ftr[2];
69 };
70 
71 struct mitsup95d_ctx {
72 	struct libusb_device_handle *dev;
73 	uint8_t endp_up;
74 	uint8_t endp_down;
75 
76 	int type;
77 
78 	struct marker marker;
79 };
80 
81 #define QUERYRESP_SIZE_MAX 9
82 
mitsup93d_errors(uint8_t code)83 static const char *mitsup93d_errors(uint8_t code)
84 {
85 	switch (code) {
86 	case 0x6f: return "Door Open";
87 	case 0x50: return "No Paper";
88 	default:   return "Unknown Error";
89 	}
90 }
91 
mitsup95d_errors(uint8_t code)92 static const char *mitsup95d_errors(uint8_t code)
93 {
94 	switch (code & 0xf) {
95 	case 3: return "Door Open";
96 	case 4: return "No Paper";
97 	default: return "Unknown Error";
98 	}
99 }
100 
mitsup95d_init(void)101 static void *mitsup95d_init(void)
102 {
103 	struct mitsup95d_ctx *ctx = malloc(sizeof(struct mitsup95d_ctx));
104 	if (!ctx) {
105 		ERROR("Memory Allocation Failure!\n");
106 		return NULL;
107 	}
108 	memset(ctx, 0, sizeof(struct mitsup95d_ctx));
109 
110 	return ctx;
111 }
112 
mitsup95d_get_status(struct mitsup95d_ctx * ctx,uint8_t * resp)113 static int mitsup95d_get_status(struct mitsup95d_ctx *ctx, uint8_t *resp)
114 {
115 	uint8_t querycmd[4] = { 0x1b, 0x72, 0x00, 0x00 };
116 	int ret;
117 	int num;
118 
119 	/* P93D is ... special.  Windows switches to this halfway through
120 	   but it seems be okay to use it everywhere */
121 	if (ctx->type == P_MITSU_P93D) {
122 		querycmd[2] = 0x03;
123 	}
124 
125 	/* Query Status to sanity-check job */
126 	if ((ret = send_data(ctx->dev, ctx->endp_down,
127 			     querycmd, sizeof(querycmd))))
128 		return CUPS_BACKEND_FAILED;
129 	ret = read_data(ctx->dev, ctx->endp_up,
130 			resp, QUERYRESP_SIZE_MAX, &num);
131 
132 	if (ret < 0)
133 		return CUPS_BACKEND_FAILED;
134 	if (ctx->type == P_MITSU_P95D && num != 9) {
135 		return CUPS_BACKEND_FAILED;
136 	} else if (ctx->type == P_MITSU_P93D && num != 8) {
137 		return CUPS_BACKEND_FAILED;
138 	}
139 	return CUPS_BACKEND_OK;
140 }
141 
mitsup95d_attach(void * vctx,struct libusb_device_handle * dev,int type,uint8_t endp_up,uint8_t endp_down,uint8_t jobid)142 static int mitsup95d_attach(void *vctx, struct libusb_device_handle *dev, int type,
143 			    uint8_t endp_up, uint8_t endp_down, uint8_t jobid)
144 {
145 	struct mitsup95d_ctx *ctx = vctx;
146 
147 	UNUSED(jobid);
148 
149 	ctx->dev = dev;
150 	ctx->endp_up = endp_up;
151 	ctx->endp_down = endp_down;
152 	ctx->type = type;
153 
154 	ctx->marker.color = "#000000";  /* Ie black! */
155 	ctx->marker.name = "Unknown";
156 	ctx->marker.levelmax = -1;
157 	ctx->marker.levelnow = -2;
158 
159 	return CUPS_BACKEND_OK;
160 }
161 
mitsup95d_cleanup_job(const void * vjob)162 static void mitsup95d_cleanup_job(const void *vjob)
163 {
164 	const struct mitsup95d_printjob *job = vjob;
165 
166 	if (job->databuf)
167 		free(job->databuf);
168 
169 	free((void*)job);
170 }
171 
mitsup95d_read_parse(void * vctx,const void ** vjob,int data_fd,int copies)172 static int mitsup95d_read_parse(void *vctx, const void **vjob, int data_fd, int copies) {
173 	struct mitsup95d_ctx *ctx = vctx;
174 	uint8_t buf[2];  /* Enough to read in any header */
175 	uint8_t tmphdr[50];
176 	uint8_t *ptr;
177 	int i;
178 	int remain;
179 	int ptr_offset;
180 
181 	struct mitsup95d_printjob *job = NULL;
182 
183 	if (!ctx)
184 		return CUPS_BACKEND_FAILED;
185 
186 	job = malloc(sizeof(*job));
187 	if (!job) {
188 		ERROR("Memory allocation failure!\n");
189 		return CUPS_BACKEND_RETRY_CURRENT;
190 	}
191 	memset(job, 0, sizeof(*job));
192 
193 	job->mem_clr_present = 0;
194 
195 top:
196 	i = read(data_fd, buf, sizeof(buf));
197 
198 	if (i == 0) {
199 		mitsup95d_cleanup_job(job);
200 		return CUPS_BACKEND_CANCEL;
201 	}
202 	if (i < 0) {
203 		mitsup95d_cleanup_job(job);
204 		return CUPS_BACKEND_CANCEL;
205 	}
206 	if (buf[0] != 0x1b) {
207 		ERROR("malformed data stream\n");
208 		mitsup95d_cleanup_job(job);
209 		return CUPS_BACKEND_CANCEL;
210 	}
211 
212 	switch (buf[1]) {
213 	case 0x50: /* Footer */
214 		remain = 2;
215 		ptr = job->ftr;
216 		break;
217 	case 0x51: /* Job Header */
218 		remain = 2;
219 		ptr = job->hdr;
220 		break;
221 	case 0x57: /* Geeneral headers */
222 		remain = sizeof(tmphdr);
223 		ptr = tmphdr;
224 		break;
225 	case 0x58: /* User Comment */
226 		if (ctx->type == P_MITSU_P93D)
227 			job->hdr4_len = 42;
228 		else
229 			job->hdr4_len = 36;
230 		remain = job->hdr4_len;
231 		ptr = job->hdr4;
232 		break;
233 	case 0x5a: /* Plane header OR printer reset */
234 		// reset memory: 1b 5a 43 ...  [len 04]
235 		// plane header: 1b 5a 74 ...  [len 12]
236 		// Read in the minimum length, and clean it up later */
237 		ptr = tmphdr;
238 		remain = 4;
239 		break;
240 	default:
241 		ERROR("Unrecognized command! (%02x %02x)\n", buf[0], buf[1]);
242 		mitsup95d_cleanup_job(job);
243 		return CUPS_BACKEND_CANCEL;
244 	}
245 
246 	memcpy(ptr, buf, sizeof(buf));
247 	remain -= sizeof(buf);
248 	ptr_offset = sizeof(buf);
249 
250 	while (remain) {
251 		i = read(data_fd, ptr + ptr_offset, remain);
252 		if (i == 0) {
253 			mitsup95d_cleanup_job(job);
254 			return CUPS_BACKEND_CANCEL;
255 		}
256 		if (i < 0) {
257 			mitsup95d_cleanup_job(job);
258 			return CUPS_BACKEND_CANCEL;
259 		}
260 		remain -= i;
261 		ptr_offset += i;
262 
263 		/* Handle the ambiguous 0x5a block */
264 		if (buf[1] == 0x5a && remain == 0) {
265 			if (tmphdr[2] == 0x74) { /* plane header */
266 				ptr = job->plane;
267 				remain = 12 - ptr_offset; /* Finish reading */
268 			} else if (tmphdr[2] == 0x43) { /* reset memory */
269 				ptr = job->mem_clr;
270 				job->mem_clr_present = 1;
271 				remain = 4 - ptr_offset;
272 			}
273 			memcpy(ptr, tmphdr, ptr_offset);
274 			buf[1] = 0xff;
275 		}
276 	}
277 
278 	if (ptr == tmphdr) {
279 		if (tmphdr[3] != 46) {
280 			ERROR("Unexpected header chunk: %02x %02x %02x %02x\n",
281 			      tmphdr[0], tmphdr[1], tmphdr[2], tmphdr[3]);
282 			mitsup95d_cleanup_job(job);
283 			return CUPS_BACKEND_CANCEL;
284 		}
285 		switch (tmphdr[2]) {
286 		case 0x20:
287 			ptr = job->hdr1;
288 			break;
289 		case 0x21:
290 			ptr = job->hdr2;
291 			break;
292 		case 0x22:
293 			ptr = job->hdr3;
294 			break;
295 		default:
296 			WARNING("Unexpected header chunk: %02x %02x %02x %02x\n",
297 			      tmphdr[0], tmphdr[1], tmphdr[2], tmphdr[3]);
298 		}
299 		memcpy(ptr, tmphdr, sizeof(tmphdr));
300 	} else if (ptr == job->plane) {
301 		uint16_t rows = job->plane[10] << 8 | job->plane[11];
302 		uint16_t cols = job->plane[8] << 8 | job->plane[9];
303 
304 		remain = rows * cols;
305 
306 		/* Allocate buffer for the payload */
307 		job->datalen = 0;
308 		job->databuf = malloc(remain);
309 		if (!job->databuf) {
310 			ERROR("Memory allocation failure!\n");
311 			mitsup95d_cleanup_job(job);
312 			return CUPS_BACKEND_RETRY_CURRENT;
313 		}
314 
315 		/* Read it in */
316 		while (remain) {
317 			i = read(data_fd, job->databuf + job->datalen, remain);
318 			if (i == 0) {
319 				mitsup95d_cleanup_job(job);
320 				return CUPS_BACKEND_CANCEL;
321 			}
322 			if (i < 0) {
323 				mitsup95d_cleanup_job(job);
324 				return CUPS_BACKEND_CANCEL;
325 			}
326 			remain -= i;
327 			job->datalen += i;
328 		}
329 	} else if (ptr == job->ftr) {
330 
331 		/* XXX Update unknown header field to match sniffs */
332 		if (ctx->type == P_MITSU_P95D) {
333 			if (job->hdr1[18] == 0x00)
334 				job->hdr1[18] = 0x01;
335 		}
336 
337 		/* Update printjob header to reflect number of requested copies */
338 		if (job->hdr2[13] != 0xff)
339 			job->hdr2[13] = copies;
340 
341 		*vjob = job;
342 		return CUPS_BACKEND_OK;
343 	}
344 
345 	goto top;
346 }
347 
mitsup95d_main_loop(void * vctx,const void * vjob)348 static int mitsup95d_main_loop(void *vctx, const void *vjob) {
349 	struct mitsup95d_ctx *ctx = vctx;
350 	uint8_t queryresp[QUERYRESP_SIZE_MAX];
351 	int ret;
352 
353 	const struct mitsup95d_printjob *job = vjob;
354 
355 	if (!ctx)
356 		return CUPS_BACKEND_FAILED;
357 	if (!job)
358 		return CUPS_BACKEND_FAILED;
359 
360 
361 	INFO("Waiting for printer idle\n");
362 
363         /* Query Status to make sure printer is idle */
364 	do {
365 		ret = mitsup95d_get_status(ctx, queryresp);
366 		if (ret)
367 			return ret;
368 
369 		if (ctx->type == P_MITSU_P95D) {
370 			if (queryresp[6] & 0x40) {
371 				INFO("Printer Status: %s (%02x)\n", mitsup95d_errors(queryresp[6]), queryresp[6]);
372 				return CUPS_BACKEND_STOP;
373 			}
374 			if (queryresp[5] == 0x00)
375 				break;
376 		} else {
377 			if (queryresp[6] == 0x45) {
378 				ERROR("Printer error %02x\n", queryresp[7]);
379 				return CUPS_BACKEND_STOP;
380 			}
381 			if (queryresp[6] == 0x30)
382 				break;
383 		}
384 
385 		sleep(1);
386 	} while (1);
387 
388 	INFO("Sending print job\n");
389 
390 	/* Send over Memory Clear, if present */
391 	if (job->mem_clr_present) {
392 		if ((ret = send_data(ctx->dev, ctx->endp_down,
393 				     job->mem_clr, sizeof(job->mem_clr))))
394 			return CUPS_BACKEND_FAILED;
395 	}
396 
397 	/* Send Job Start */
398 	if ((ret = send_data(ctx->dev, ctx->endp_down,
399 			     job->hdr, sizeof(job->hdr))))
400 		return CUPS_BACKEND_FAILED;
401 
402 	/* Send over headers */
403 	if ((ret = send_data(ctx->dev, ctx->endp_down,
404 			     job->hdr1, sizeof(job->hdr1))))
405 		return CUPS_BACKEND_FAILED;
406 	if ((ret = send_data(ctx->dev, ctx->endp_down,
407 			     job->hdr2, sizeof(job->hdr2))))
408 		return CUPS_BACKEND_FAILED;
409 	if ((ret = send_data(ctx->dev, ctx->endp_down,
410 			     job->hdr3, sizeof(job->hdr3))))
411 		return CUPS_BACKEND_FAILED;
412 	if ((ret = send_data(ctx->dev, ctx->endp_down,
413 			     job->hdr4, job->hdr4_len)))
414 		return CUPS_BACKEND_FAILED;
415 
416 	/* Send plane header and image data */
417 	if ((ret = send_data(ctx->dev, ctx->endp_down,
418 			     job->plane, sizeof(job->plane))))
419 		return CUPS_BACKEND_FAILED;
420 	if ((ret = send_data(ctx->dev, ctx->endp_down,
421 			     job->databuf, job->datalen)))
422 		return CUPS_BACKEND_FAILED;
423 
424 	/* Query Status to sanity-check job */
425 	ret = mitsup95d_get_status(ctx, queryresp);
426 	if (ret)
427 		return ret;
428 
429 	if (ctx->type == P_MITSU_P95D) {
430 		if (queryresp[6] & 0x40) {
431 			INFO("Printer Status: %s (%02x)\n", mitsup95d_errors(queryresp[6]), queryresp[6]);
432 			return CUPS_BACKEND_STOP;
433 		}
434 		if (queryresp[5] != 0x00) {
435 			ERROR("Printer not ready (%02x)!\n", queryresp[5]);
436 			return CUPS_BACKEND_CANCEL;
437 		}
438 	} else {
439 		if (queryresp[6] == 0x45) {
440 			INFO("Printer Status: %s (%02x)\n", mitsup93d_errors(queryresp[7]), queryresp[7]);
441 			return CUPS_BACKEND_STOP;
442 		}
443 		if (queryresp[6] != 0x30) {
444 			ERROR("Printer not ready (%02x)!\n", queryresp[6]);
445 			return CUPS_BACKEND_CANCEL;
446 		}
447 	}
448 
449 	/* Send over Footer */
450 	if ((ret = send_data(ctx->dev, ctx->endp_down,
451 			     job->ftr, sizeof(job->ftr))))
452 		return CUPS_BACKEND_FAILED;
453 
454 	INFO("Waiting for completion\n");
455 
456 	/* Query status until we're done.. */
457 	do {
458 		sleep(1);
459 
460 		/* Query Status */
461 		ret = mitsup95d_get_status(ctx, queryresp);
462 		if (ret)
463 			return ret;
464 
465 		if (ctx->type == P_MITSU_P95D) {
466 			if (queryresp[6] & 0x40) {
467 				INFO("Printer Status: %s (%02x)\n", mitsup95d_errors(queryresp[6]), queryresp[6]);
468 				return CUPS_BACKEND_STOP;
469 			}
470 			if (queryresp[5] == 0x00)
471 				break;
472 
473 			if (queryresp[7] > 0) {
474 				if (fast_return) {
475 					INFO("Fast return mode enabled.\n");
476 					break;
477 				}
478 			}
479 		} else {
480 			if (queryresp[6] == 0x45) {
481 				INFO("Printer Status: %s (%02x)\n", mitsup93d_errors(queryresp[7]), queryresp[7]);
482 				return CUPS_BACKEND_STOP;
483 			}
484 			if (queryresp[6] == 0x30)
485 				break;
486 			if (queryresp[6] == 0x43 && queryresp[7] > 0) {
487 				if (fast_return) {
488 					INFO("Fast return mode enabled.\n");
489 					break;
490 				}
491 			}
492 		}
493 	} while(1);
494 
495 	INFO("Print complete\n");
496 	return CUPS_BACKEND_OK;
497 }
498 
mitsup95d_dump_status(struct mitsup95d_ctx * ctx)499 static int mitsup95d_dump_status(struct mitsup95d_ctx *ctx)
500 {
501 	uint8_t queryresp[QUERYRESP_SIZE_MAX];
502 	int ret;
503 
504 	ret = mitsup95d_get_status(ctx, queryresp);
505 	if (ret)
506 		return ret;
507 
508 	if (ctx->type == P_MITSU_P95D) {
509 		if (queryresp[6] & 0x40) {
510 			INFO("Printer Status: %s (%02x)\n", mitsup95d_errors(queryresp[6]), queryresp[6]);
511 		} else if (queryresp[5] == 0x00) {
512 			INFO("Printer Status: Idle\n");
513 		} else if (queryresp[5] == 0x02 && queryresp[7] > 0) {
514 			INFO("Printer Status: Printing (%d) copies remaining\n", queryresp[7]);
515 		}
516 	} else {
517 		if (queryresp[6] == 0x45) {
518 			INFO("Printer Status: %s (%02x)\n", mitsup93d_errors(queryresp[7]), queryresp[7]);
519 		} else if (queryresp[6] == 0x30) {
520 			INFO("Printer Status: Idle\n");
521 		} else if (queryresp[6] == 0x43 && queryresp[7] > 0) {
522 			INFO("Printer Status: Printing (%d) copies remaining\n", queryresp[7]);
523 		}
524 	}
525 
526 	return CUPS_BACKEND_OK;
527 }
528 
mitsup95d_cmdline(void)529 static void mitsup95d_cmdline(void)
530 {
531 	DEBUG("\t\t[ -s ]           # Query status\n");
532 }
533 
mitsup95d_cmdline_arg(void * vctx,int argc,char ** argv)534 static int mitsup95d_cmdline_arg(void *vctx, int argc, char **argv)
535 {
536 	struct mitsup95d_ctx *ctx = vctx;
537 	int i, j = 0;
538 
539 	if (!ctx)
540 		return -1;
541 
542 	while ((i = getopt(argc, argv, GETOPT_LIST_GLOBAL "s")) >= 0) {
543 		switch(i) {
544 		GETOPT_PROCESS_GLOBAL
545 		case 's':
546 			j = mitsup95d_dump_status(ctx);
547 			break;
548 		default:
549 			break;  /* Ignore completely */
550 		}
551 
552 		if (j) return j;
553 	}
554 
555 	return 0;
556 }
557 
mitsup95d_query_markers(void * vctx,struct marker ** markers,int * count)558 static int mitsup95d_query_markers(void *vctx, struct marker **markers, int *count)
559 {
560 	struct mitsup95d_ctx *ctx = vctx;
561 	uint8_t queryresp[QUERYRESP_SIZE_MAX];
562 
563 	if (mitsup95d_get_status(ctx, queryresp))
564 		return CUPS_BACKEND_FAILED;
565 
566 	ctx->marker.levelnow = -3;
567 
568 	if (ctx->type == P_MITSU_P95D) {
569 		if (queryresp[6] & 0x40) {
570 			ctx->marker.levelnow = 0;
571 		}
572 	} else {
573 		if (queryresp[6] == 0x45) {
574 			ctx->marker.levelnow = 0;
575 		}
576 	}
577 
578 	/* Lot state */
579 	if (ctx->marker.levelnow)
580 		STATE("-media-empty\n");
581 	else
582 		STATE("+media-empty\n");
583 
584 
585 	*markers = &ctx->marker;
586 	*count = 1;
587 
588 	return CUPS_BACKEND_OK;
589 }
590 
591 static const char *mitsup95d_prefixes[] = {
592 	"mitsup9x", // Family driver name
593 	"mitsubishi-p95d", "mitsubishi-p93d",
594 	// backwards compatibility
595 	"mitsup95d", "mitsup93d",
596 	NULL
597 };
598 
599 /* Exported */
600 struct dyesub_backend mitsup95d_backend = {
601 	.name = "Mitsubishi P93D/P95D",
602 	.version = "0.12",
603 	.uri_prefixes = mitsup95d_prefixes,
604 	.cmdline_arg = mitsup95d_cmdline_arg,
605 	.cmdline_usage = mitsup95d_cmdline,
606 	.init = mitsup95d_init,
607 	.attach = mitsup95d_attach,
608 	.cleanup_job = mitsup95d_cleanup_job,
609 	.read_parse = mitsup95d_read_parse,
610 	.main_loop = mitsup95d_main_loop,
611 	.query_markers = mitsup95d_query_markers,
612 	.devices = {
613 		{ USB_VID_MITSU, USB_PID_MITSU_P93D, P_MITSU_P93D, NULL, "mitsubishi-p93d"},
614 		{ USB_VID_MITSU, USB_PID_MITSU_P95D, P_MITSU_P95D, NULL, "mitsubishi-p95d"},
615 		{ 0, 0, 0, NULL, NULL}
616 	}
617 };
618 
619 /*****************************************************
620 
621  Mitsubishi P93D/P95D Spool Format
622 
623    ...All fields are BIG ENDIAN.
624 
625  MEMORY_CLEAR  (optional)
626 
627   1b 5a 43 00
628 
629  JOB_HDR
630 
631   1b 51
632 
633  PRINT_SETUP
634 
635   1b 57 20 2e  00 0a 00 ZZ   00 00 00 00  00 00 CC CC
636   RR RR XX 00  00 00 00 00   00 00 00 00  00 00 00 00
637   00 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00
638   00 00
639 
640    XX == 01 seen in sniffs, 00 seen in dumps.  Unknown purpose.
641    ZZ == 00 on P93D, 02 on P95D
642 
643    CC CC = columns, RR RR = rows (print dimensions)
644 
645  PRINT_OPTIONS
646 
647    P95:
648 
649   1b 57 21 2e  00 4a aa 00   20 TT 00 00  64 NN 00 MM
650   [[ 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00
651   00 ]] 00 00  00 02 00 00   00 00 00 00  00 00 00 00
652   00 XY
653 
654    P93:
655 
656   1b 57 21 2e  00 4a aa 00   00 TT 00 00  00 NN 00 MM
657   [[ 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00
658   00 ]] 00 00  00 02 00 00   00 00 00 00  00 00 00 00
659   00 XY
660 
661    NN = copies
662         1..200
663         0xff (continuous print)
664    MM = comment type
665         00 = None
666         01 = Printer Setting
667         02 = Date
668         03 = DateTime
669    [[ .. ]] = actual comment (18 bytes), see below.
670 
671    TT = media type
672 
673          P95D:
674 
675         00 = Standard
676         01 = High Density
677         02 = High Glossy
678         03 = High Glossy (K95HG)
679 
680          P93D:
681 
682         00 = High Density
683         01 = High Glossy
684         02 = Standard
685 
686    X = media cut length   (P95D ONLY.  P93 is 0)
687         4..8  (mm)
688    Y = flags
689         0x04 = Paper save
690         0x03 = Buzzer (3 = high, 2 = low, 0 = off)
691 
692  GAMMA  (P95)
693 
694   1b 57 22 2e  00 15 TT 00   00 00 00 00  LL BB CC 00
695   [[ 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00
696   00 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00
697   00 ]]
698 
699    LL = Gamma table
700         00 = Printer Setting
701         01..05  Gamma table 1..5
702         10 = Use LUT
703    BB = Brightness (signed 8-bit)
704    CC = Contrast (signed 8-bit)
705    TT = Table present
706         00 = No
707         01 = Yes
708    [[ .. ]] = Gamma table, loaded from LUT on disk.  (skip first 16 bytes)
709 
710  GAMMA  (P93)
711 
712   1b 57 22 2e  00 d5 00 00   00 00 00 00  SS 00 LL 00
713   BB 00 CC 00  00 00 00 00   00 00 00 00  00 00 00 00
714   00 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00
715   00 00
716 
717    SS = Sharpening (0 = low, 1 = normal, 2 = high)
718    LL = Gamma table
719         00..04  Gamma table 1..5
720    BB = Brightness (signed 8-bit)
721    CC = Contrast (signed 8-bit)
722 
723  USER_COMMENT  (P95)
724 
725   1b 58 [[ 20  20 20 20 20  20 20 20 20  20 20 20 20
726   20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20
727   20 20 20 ]]
728 
729    [[ .. ]] = Actual comment.  34 bytes payload, 0x20 -> 0x7e
730                (Null terminated?)
731 
732  USER_COMMENT  (P93)
733 
734   1b 58 [[ 20  20 20 20 20  20 20 20 20  20 20 20 20
735   20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20
736   20 20 20 20  20 20 20 20  20 20 ]]
737 
738    [[ .. ]] = Actual comment.  40 bytes payload, 0x20 -> 0x7e
739                (Null terminated?)
740 
741  IMAGE_DATA
742 
743   1b 5a 74 00  00 00 YY YY  CC CC RR RR
744   [[ .. data ... ]]
745 
746     CC CC = columns
747     RR CC = rows
748     YY YY = row offset
749 
750   Followed by C*R bytes of monochrome data, 0xff = white, 0x00 = black
751 
752  PRINT_START
753 
754   1b 50
755 
756  *********************************
757 
758  P95D Printer Comms:
759 
760  STATUS query
761 
762  -> 1b 72 00 00
763  <- e4 72 00 00  04 XX ZZ YY  00
764 
765   YY == remaining copies
766   XX == Status?
767     00 == Idle
768     02 == Printing
769   ZZ == Error!
770     00 == None
771     43 == Door open
772     44 == No Paper
773     4? == "Button"
774     4? == "Gear Lock"
775     4? == Head Up
776     ^
777     \--- 0x40 appears to be a flag that indicates error.
778 
779  P93D Printer Comms:
780 
781  STATUS query
782 
783  -> 1b 72 0? 00
784  <- e4 72 0? 00  03 XX YY ZZ
785 
786   ? could be 0x00 or 0x03.  Seen both.
787 
788 Seen:   30 30 30
789         30 43 01   <- 1 copies remaining
790         30 43 00   <- 0 copies remaining
791            ^^
792             \-- 30 == idle, 43 == printing
793 
794         30 45 6f  <- door open
795         30 45 50  <- no paper
796 
797            45 == error?
798  ****************************
799 
800 UNKNOWNS:
801 
802  * How multiple images are stacked for printing on a single page
803    (col offset too?  write four, then tell PRINT?)
804  * How to adjust P95D printer sharpness?
805  * Serial number query (iSerial appears bogus)
806  * What "custom gamma" table does to spool file?
807 
808 */
809