Przeglądaj źródła

protocols/printer: Initial printer protocol support

Signed-off-by: Kris Bahnsen <Kris@KBEmbedded.com>
Kris Bahnsen 1 rok temu
rodzic
commit
86284b00f1

+ 3 - 0
gblink.h

@@ -105,6 +105,9 @@ void gblink_start(void *handle);
 
 void gblink_stop(void *handle);
 
+// void gblink_blink_led_on_byte(handle, color?)
+// get blink?
+
 #ifdef __cplusplus
 }
 #endif

+ 100 - 0
protocols/printer_i.h

@@ -0,0 +1,100 @@
+#ifndef PRINTER_I_H
+#define PRINTER_I_H
+
+#include <protocols/printer_proto.h>
+
+#define PKT_START_L		0x88
+#define PKT_START_H		0x33
+
+#define PRINTER_ID		0x81
+#define CMD_INIT		0x01
+#define CMD_PRINT		0x02
+#define CMD_TRANSFER		0x10
+#define CMD_DATA		0x04
+#define CMD_BREAK		0x08 // ??
+#define CMD_STATUS		0x0f
+
+#define STATUS_LOWBATT		(1 << 7)
+#define STATUS_ERR		(1 << 6)
+#define STATUS_JAM		(1 << 5)
+#define STATUS_PKT_ERR		(1 << 4)
+#define STATUS_READY		(1 << 3)
+#define STATUS_FULL		(1 << 2)
+#define STATUS_PRINTING		(1 << 1)
+#define STATUS_CKSUM		(1 << 0)
+
+// Extra status bits used not used by the printer
+#define STATUS_PRINT_CMD	(1 << 8)
+#define STATUS_PRINTED		(1 << 9)
+
+/* emulate printer's internal print receive buffer */
+#define TILE_SIZE		16 // 8x8 tile, 2bpp color
+#define WIDTH			20 // 20 tiles wide
+#define HEIGHT			18 // 18 tiles tall
+#define PRINT_RECV_SZ		640 // (TILE_SIZE * WIDTH * 2)
+#define PRINT_FULL_SZ		5760 // (PRINT_RECV_SZ * HEIGHT / 2)
+#define TRANSFER_RECV_SZ	3584 // (16*16*14) // Image minus frame
+
+//Note that TRANSFER uses a locked size, 16x14 tiles, 16*16*14
+
+//GB seems to use 2 second busy timeout? I think that is a go to busy/printing within 2 seconds?
+//20 second print timeout
+
+
+enum packet_state {
+	START_L,
+	START_H,
+	COMMAND,
+	COMPRESS,
+	LEN_L,
+	LEN_H,
+	COMMAND_DAT,
+	CKSUM_L,
+	CKSUM_H,
+	ALIVE,
+	STATUS,
+};
+
+enum printer_method {
+	PROTO_RECV,
+	PROTO_SEND,
+};
+
+/* Does not need to care about start bytes */
+struct packet {
+	uint8_t cmd;
+	uint8_t compress;
+	uint16_t len; // This is stored in the flipper endianness, arrives LSB first from GB, unmodified in code
+	uint8_t recv_data[PRINT_RECV_SZ]; // 640 bytes, enough for two lines of tiles
+	uint16_t cksum; // This is stored in the flipper endianness, arrives LSB first from GB
+	
+	/* These are not part of the packet, but used by us */
+	uint16_t cksum_calc;
+	size_t recv_data_sz;
+	uint16_t status;
+	uint8_t zero_counter;
+	enum packet_state state;
+	uint32_t time;
+};
+
+#define THREAD_FLAGS_EXIT	(1 << 0)
+#define THREAD_FLAGS_DATA	(1 << 1)
+#define THREAD_FLAGS_PRINT	(1 << 2)
+#define THREAD_FLAGS_COMPLETE	(1 << 3)
+#define THREAD_FLAGS_ALL	(THREAD_FLAGS_EXIT | THREAD_FLAGS_DATA | THREAD_FLAGS_PRINT | THREAD_FLAGS_COMPLETE)
+
+struct printer_proto {
+	void *gblink_handle;
+
+	void (*callback)(void *cb_context, struct gb_image *image, enum cb_reason reason);
+	void *cb_context;
+
+	struct packet *packet; //packet data used by send()/receive() for tracking
+
+	struct gb_image *image; // Details of the current image being sent/received
+
+
+	FuriThread *thread;
+};
+
+#endif // PRINTER_I_H

+ 146 - 0
protocols/printer_proto.c

@@ -0,0 +1,146 @@
+#include <furi.h>
+
+#include <gblink.h>
+#include <protocols/printer_proto.h>
+#include "printer_i.h"
+
+/* XXX: Does this make sense to be a message dispatcher rather than calling callbacks?
+ * In order to keep the stack small for the thread, need to be weary of all calls made from here. */
+/* XXX TODO Test using a timer pending callback instead of this */
+/* XXX: TODO: Create a more streamlined callback that can simply pass a struct that has
+ * pointers to data, sz, reason, margins (aka is there more data coming), etc., could even place
+ * the callback context in there which would allow using the timer pending callback function
+ */
+static int32_t printer_callback_thread(void *context)
+{
+	struct printer_proto *printer = context;
+	uint32_t flags;
+
+	while (1) {
+		/* XXX: TODO: align flags and enum cb_reason to share them */
+		flags = furi_thread_flags_wait(THREAD_FLAGS_ALL, FuriFlagWaitAny, FuriWaitForever);
+		furi_check(!(flags & FuriFlagError));
+		if (flags & THREAD_FLAGS_EXIT)
+			break;
+		if (flags & THREAD_FLAGS_DATA)
+			printer->callback(printer->cb_context, printer->image, reason_data);
+		if (flags & THREAD_FLAGS_PRINT)
+			printer->callback(printer->cb_context, printer->image, reason_print);
+	}
+
+	return 0;
+}
+
+void *printer_alloc(void)
+{
+	struct printer_proto *printer = NULL;
+
+	printer = malloc(sizeof(struct printer_proto));
+
+	/* Allocate and start callback handling thread */
+	/* XXX: TODO: The stack can decrease if FURI_LOG calls are removed in callbacks! */
+	printer->thread = furi_thread_alloc_ex("GBLinkPrinterProtoCB",
+						1024,
+						printer_callback_thread,
+						printer);
+	/* Highest priority to ensure it runs ASAP */
+	furi_thread_set_priority(printer->thread, FuriThreadPriorityHighest);
+	furi_thread_start(printer->thread);
+
+	printer->packet = malloc(sizeof(struct packet));
+	printer->image = printer_image_buffer_alloc();
+
+	printer->gblink_handle = gblink_alloc();
+
+	/* Set up some settings for the print protocol. The final send/receive() calls
+	 * may clobber some of these, but that is intentional and they don't need to
+	 * care about some of the other details that are specified here.
+	 */
+	/* Reported 1.49 ms timeout between bytes, need confirmation */
+	gblink_timeout_set(printer->gblink_handle, 1490);
+	gblink_nobyte_set(printer->gblink_handle, 0x00);
+
+	return printer;
+}
+
+/* TODO: Allow free() without stop, add a way to check if printer_stop has not
+ * yet been called.
+ */
+void printer_free(void *printer_handle)
+{
+	struct printer_proto *printer = printer_handle;
+
+	furi_thread_flags_set(printer->thread, THREAD_FLAGS_EXIT);
+	furi_thread_join(printer->thread);
+	furi_thread_free(printer->thread);
+	gblink_free(printer->gblink_handle);
+	free(printer->packet);
+	free(printer->image);
+	free(printer);
+}
+
+void printer_callback_context_set(void *printer_handle, void *context)
+{
+	struct printer_proto *printer = printer_handle;
+
+	printer->cb_context = context;
+}
+
+void printer_callback_set(void *printer_handle, void (*callback)(void *context, struct gb_image *image, enum cb_reason reason))
+{
+	struct printer_proto *printer = printer_handle;
+
+	printer->callback = callback;
+}
+
+int printer_pin_set_default(void *printer_handle, gblink_pinouts pinout)
+{
+	struct printer_proto *printer = printer_handle;
+	return gblink_pin_set_default(printer->gblink_handle, pinout);
+}
+
+int printer_pin_set(void *printer_handle, gblink_bus_pins pin, const GpioPin *gpio)
+{
+	struct printer_proto *printer = printer_handle;
+	return gblink_pin_set(printer->gblink_handle, pin, gpio);
+}
+
+const GpioPin *printer_pin_get(void *printer_handle, gblink_bus_pins pin)
+{
+	struct printer_proto *printer = printer_handle;
+	return gblink_pin_get(printer->gblink_handle, pin);
+}
+
+
+void printer_stop(void *printer_handle)
+{
+	struct printer_proto *printer = printer_handle;
+
+	gblink_stop(printer->gblink_handle);
+	/* TODO: Call the callback one last time with a flag to indicate that the transfer has completely
+	 * ended.
+	 * Receive/send should also have a separate timeout, doesn't need to call stop, but, will
+	 * also retrigger the complete callback. This allows for both the actual process to signal
+	 * there was a gap (I think the gameboy print normally has a "I'm done" marker as well),
+	 * and then the actual application that started the send/receive, can catch a back or other
+	 * nav event, call stop itself, which will then call the callback again with a "we're done here"
+	 * message as well.
+	 */
+	 
+	/* TODO: Figure out what mode we're in, and run stop. Though, it might
+	 * not be necessary to actually to know the mode. We should be able to
+	 * just stop?
+	 */
+}
+
+
+struct gb_image *printer_image_buffer_alloc(void)
+{
+	struct gb_image *image = malloc(sizeof(struct gb_image) + PRINT_FULL_SZ);
+	return image;
+}
+
+void printer_image_buffer_free(struct gb_image *image)
+{
+	free(image);
+}

+ 56 - 0
protocols/printer_proto.h

@@ -0,0 +1,56 @@
+#ifndef PRINTER_PROTO_H
+#define PRINTER_PROTO_H
+
+#include "../gblink.h"
+
+#pragma once
+
+enum cb_reason {
+	reason_data,
+	reason_print,
+	reason_complete,
+};
+
+/* Dual purpose struct used for both receiving image data from game boy, and
+ * sending it to printer.
+ */
+struct gb_image {
+	/* TODO: Need to understand this more */
+	uint8_t num_sheets;
+	uint8_t margins;
+	/* TODO: Does this actually matter? */
+	uint8_t palette;
+	/* TODO: Need to play with this more */
+	uint8_t exposure;
+
+	/* Always expected to be 160 px wide */
+	size_t data_sz;
+	uint8_t data[];
+};
+
+void *printer_alloc(void);
+
+void printer_free(void *printer_handle);
+
+void printer_callback_context_set(void *printer_handle, void *context);
+
+void printer_callback_set(void *printer_handle, void (*callback)(void *context, struct gb_image *image, enum cb_reason reason));
+
+/* Can only be run after alloc, before start */
+int printer_pin_set_default(void *printer_handle, gblink_pinouts pinout);
+
+int printer_pin_set(void *printer_handle, gblink_bus_pins pin, const GpioPin *gpio);
+
+const GpioPin *printer_pin_get(void *printer_handle, gblink_bus_pins pin);
+
+void printer_stop(void *printer_handle);
+
+/* Allocates a buffer of the maximum size that the printer can handle, must be
+ * freed manually. Provided as a convenience function so the application doesn't
+ * need to know how big of a buffer to create.
+ */
+struct gb_image *printer_image_buffer_alloc(void);
+
+void printer_image_buffer_free(struct gb_image *image);
+
+#endif // PRINTER_PROTO_H

+ 224 - 0
protocols/printer_receive.c

@@ -0,0 +1,224 @@
+#include <stdint.h>
+
+#include <furi.h>
+
+#include <gblink.h>
+#include "printer_i.h"
+
+#define TAG "printer_receive"
+
+/* XXX: TODO: Double check this */
+#define TIMEOUT_US 1000000
+
+static void printer_reset(struct printer_proto *printer)
+{
+	/* Clear out the current packet data */
+	memset(printer->packet, '\0', sizeof(struct packet));
+
+	printer->image->data_sz = 0;
+	/* This is technically redundant, done for completeness */
+	printer->packet->state = START_L;
+
+	/* Packet timeout start */
+	printer->packet->time = DWT->CYCCNT;
+}
+
+static void byte_callback(void *context, uint8_t val)
+{
+	struct printer_proto *printer = context;
+	struct packet *packet = printer->packet;
+	const uint32_t time_ticks = furi_hal_cortex_instructions_per_microsecond() * TIMEOUT_US;
+	uint8_t data_out = 0x00;
+
+	if ((DWT->CYCCNT - printer->packet->time) > time_ticks)
+		printer_reset(printer);
+
+	/* TODO: flash led? */
+
+	switch (packet->state) {
+	case START_L:
+		if (val == PKT_START_L) {
+			packet->state = START_H;
+			/* Packet timeout restart */
+			packet->time = DWT->CYCCNT;
+			packet->zero_counter = 0;
+		}
+		if (val == 0x00) {
+			packet->zero_counter++;
+			if (packet->zero_counter == 16)
+				printer_reset(printer);
+		}
+		break;
+	case START_H:
+		if (val == PKT_START_H)
+			packet->state = COMMAND;
+		else
+			packet->state = START_L;
+		break;
+	case COMMAND:
+		packet->cmd = val;
+		packet->state = COMPRESS;
+		packet->cksum_calc += val;
+
+		/* We only do a real reset after the packet is completed, however
+		 * we need to clear the status flags at this point.
+		 */
+		if (val == CMD_INIT)
+			packet->status = 0;
+
+		break;
+	case COMPRESS:
+		packet->cksum_calc += val;
+		packet->state = LEN_L;
+		if (val) {
+			FURI_LOG_E(TAG, "Compression not supported!");
+			packet->status |= STATUS_PKT_ERR;
+		}
+		break;
+	case LEN_L:
+		packet->cksum_calc += val;
+		packet->state = LEN_H;
+		packet->len = (val & 0xff);
+		break;
+	case LEN_H:
+		packet->cksum_calc += val;
+		packet->len |= ((val & 0xff) << 8);
+		/* Override length for a TRANSFER */
+		if (packet->cmd == CMD_TRANSFER)
+			packet->len = TRANSFER_RECV_SZ;
+
+		if (packet->len) {
+			packet->state = COMMAND_DAT;
+		} else {
+			packet->state = CKSUM_L;
+		}
+		break;
+	case COMMAND_DAT:
+		packet->cksum_calc += val;
+		packet->recv_data[packet->recv_data_sz] = val;
+		packet->recv_data_sz++;
+		if (packet->recv_data_sz == packet->len) 
+			packet->state = CKSUM_L;
+		break;
+	case CKSUM_L:
+		packet->state = CKSUM_H;
+		packet->cksum = (val & 0xff);
+		break;
+	case CKSUM_H:
+		packet->state = ALIVE;
+		packet->cksum |= ((val & 0xff) << 8);
+		if (packet->cksum != packet->cksum_calc)
+			packet->status |= STATUS_CKSUM;
+		// TRANSFER does not set checksum bytes
+		if (packet->cmd == CMD_TRANSFER)
+			packet->status &= ~STATUS_CKSUM;
+		data_out = PRINTER_ID;
+		break;
+	case ALIVE:
+		packet->state = STATUS;
+		data_out = packet->status;
+		break;
+	case STATUS:
+		packet->state = START_L;
+		switch (packet->cmd) {
+		case CMD_INIT:
+			printer_reset(printer);
+			break;
+		case CMD_DATA:
+			if (printer->image->data_sz < PRINT_FULL_SZ) {
+				if ((printer->image->data_sz + packet->len) <= PRINT_FULL_SZ) {
+					memcpy((printer->image->data)+printer->image->data_sz, packet->recv_data, packet->len);
+					printer->image->data_sz += packet->len;
+				} else {
+					memcpy((printer->image->data)+printer->image->data_sz, packet->recv_data, ((printer->image->data_sz + packet->len)) - PRINT_FULL_SZ);
+					printer->image->data_sz += (PRINT_FULL_SZ - (printer->image->data_sz + packet->len));
+					furi_assert(printer->image->data_sz <= PRINT_FULL_SZ);
+				}
+			}
+			if (printer->image->data_sz == PRINT_FULL_SZ)
+				packet->status |= STATUS_READY;
+
+			furi_thread_flags_set(printer->thread, THREAD_FLAGS_DATA);
+			break;
+		case CMD_TRANSFER:
+			/* XXX: TODO: Check to see if we're still printing when getting
+			 * a transfer command. If so, then we have failed to beat the clock.
+			 */
+		case CMD_PRINT:
+			packet->status &= ~STATUS_READY;
+			packet->status |= (STATUS_PRINTING | STATUS_FULL);
+			furi_thread_flags_set(printer->thread, THREAD_FLAGS_PRINT);
+			break;
+		case CMD_STATUS:
+			/* READY cleared on status request */
+			packet->status &= ~STATUS_READY;
+			if ((packet->status & STATUS_PRINTING) &&
+			    (packet->status & STATUS_PRINTED)) {
+				packet->status &= ~(STATUS_PRINTING | STATUS_PRINTED);
+				furi_thread_flags_set(printer->thread, THREAD_FLAGS_COMPLETE);
+			}
+		}
+
+		packet->recv_data_sz = 0;
+		packet->cksum_calc = 0;
+
+
+		/* XXX: TODO: if the command had something we need to do, do it here. */
+		/* done! flush our buffers, deal with any status changes like
+		 * not printing -> printing -> not printing, etc.
+		 */
+		/* Do a callback here?
+		 * if so, I guess we should wait for callback completion before accepting more recv_data?
+		 * but that means the callback is in an interrupt context, which, is probably okay?
+		 */
+		/* XXX: TODO: NOTE: FIXME:
+		 * all of the notes..
+		 * This module needs to maintain the whole buffer, but it can be safely assumed that the buffer
+		 * will never exceed 20x18 tiles (no clue how many bytes) as that is the max the printer can
+		 * take on in a single print. Printing mulitples needs a print, and then a second print with
+		 * no margin. So the margins are important and need to be passed to the final application,
+		 * SOMEHOW.
+		 *
+		 * More imporatntly, is the completed callback NEEDS to have a return value. This allows
+		 * the end application to take that whole panel, however its laid out, and do whatever
+		 * it wants to do with it. Write it to a file, convert, etc., etc., so that this module
+		 * will forever return that it is printing until the callback returns true.
+		 *
+		 * Once we call the callback and it shows a true, then we can be sure the higher module
+		 * is done with the buffer, and we can tell the host that yes, its done, you can continue
+		 * if you want.
+		 */
+		/* XXX: On TRANSFER, there is no checking of status, it is only two packets in total.
+		 * I can assume that if we delay a bit in moving the buffer around that should be okay
+		 * but we probably don't want to wait too long.
+		 * Upon testing, transfer seems to doesn't 
+		 */
+		break;
+	default:
+		FURI_LOG_E(TAG, "unknown status!");
+		break;
+	}
+
+	/* transfer next byte */
+	gblink_transfer(printer->gblink_handle, data_out);
+}
+
+void printer_receive_start(void *printer_handle)
+{
+	struct printer_proto *printer = printer_handle;
+
+	/* Set up defaults the receive path needs */
+	gblink_callback_set(printer->gblink_handle, byte_callback, printer);
+	gblink_clk_source_set(printer->gblink_handle, GBLINK_CLK_EXT);
+
+	printer_reset(printer);
+
+	gblink_start(printer->gblink_handle);
+}
+
+void printer_receive_print_complete(void *printer_handle)
+{
+	struct printer_proto *printer = printer_handle;
+
+	printer->packet->status &= ~STATUS_PRINTING;
+}

+ 10 - 0
protocols/printer_receive.h

@@ -0,0 +1,10 @@
+#ifndef PRINTER_RECEIVE_H
+#define PRINTER_RECEIVE_H
+
+#pragma once
+
+void printer_receive_start(void *printer_handle);
+
+void printer_receive_print_complete(void *printer_handle);
+
+#endif // PRINTER_RECEIVE_H