Ver Fonte

Merge pokemon_trading/lib/flipper-gblink from https://github.com/kbembedded/flipper-gblink

Willy-JL há 1 ano atrás
pai
commit
43e212ac02

+ 2 - 2
pokemon_trading/lib/flipper-gblink/README.md

@@ -1,7 +1,7 @@
 # Flipper Game Boy Game Link Cable API
 Simple API that can be included in projects to provide a flexible and easy way to handle data exchange over a Game Link Cable.
 
-Current Version: 0.6
+Current Version: 0.62
 
 Available from: https://github.com/kbembedded/flipper-gblink
 
@@ -23,7 +23,7 @@ Available from: https://github.com/kbembedded/flipper-gblink
 - [ ] Proper documentation  
 
 ## Use example
-See https://github.com/EstebanFuentealba/Flipper-Zero-Game-Boy-Pokemon-Trading
+See https://github.com/kbembedded/Flipper-Zero-Game-Boy-Pokemon-Trading
 
 To include this in a Flipper Zero application, add this repo as a submodule in the `lib/` directory of the application source. Then add the following to `application.fam`:
 ```

+ 0 - 466
pokemon_trading/lib/flipper-gblink/gblink.c

@@ -1,466 +0,0 @@
-// SPDX-License-Identifier: BSD-2-Clause
-// Copyright (c) 2023 KBEmbedded
-
-#include <furi.h>
-#include <furi_hal.h>
-#include <stm32wbxx_ll_exti.h>
-#include <stm32wbxx_ll_system.h>
-
-#include <stdint.h>
-
-#include "gblink.h"
-
-const struct gblink_pins common_pinouts[PINOUT_COUNT] = {
-	/* Original */
-	{
-		&gpio_ext_pc3,
-		&gpio_ext_pb3,
-		&gpio_ext_pb2,
-		&gpio_ext_pa4,
-	},
-	/* MALVEKE EXT1 */
-	{
-		&gpio_ext_pa6,
-		&gpio_ext_pa7,
-		&gpio_ext_pb3,
-		&gpio_ext_pa4,
-	},
-};
-
-struct gblink {
-	const GpioPin *serin;
-	const GpioPin *serout;
-	const GpioPin *clk;
-	const GpioPin *sd;
-
-	uint8_t in;
-	uint8_t out;
-	uint8_t out_buf;
-	bool out_buf_valid;
-	uint8_t shift;
-	uint8_t nobyte;
-	gblink_clk_source source;
-	gblink_mode mode;
-	gblink_speed speed;
-
-	uint32_t time;
-
-	uint32_t bitclk_timeout_us;
-	/* Clocks idle between bytes is nominally 430 us long for burst data,
-	 * 15 ms for idle polling (e.g. waiting for menu selection), some oddball
-	 * 2 ms gaps that appears between one 0xFE byte from the Game Boy every trade;
-	 * clock period is nominally 122 us.
-	 * Therefore, if we haven't seen a clock in 500 us, reset our bit counter.
-	 * Note that, this should never actually be a concern, but it is an additional
-	 * safeguard against desyncing.
-	 */
-
-	void (*callback)(void* cb_context, uint8_t in);
-	void *cb_context;
-
-	uint32_t* ivt_mirror;
-	uint32_t ivt_mirror_offs;
-	bool exti3_rise_enable;
-	bool exti3_fall_enable;
-	bool exti3_event_enable;
-};
-
-static void gblink_shift_in(struct gblink *gblink)
-{
-	const uint32_t time_ticks = furi_hal_cortex_instructions_per_microsecond() * gblink->bitclk_timeout_us;
-
-	/* If we exceeded the bit clock timeout, reset all counters */
-	if ((DWT->CYCCNT - gblink->time) > time_ticks) {
-		gblink->in = 0;
-		gblink->shift = 0;
-	}
-	gblink->time = DWT->CYCCNT;
-
-	gblink->in <<= 1;
-	gblink->in |= furi_hal_gpio_read(gblink->serin);
-	gblink->shift++;
-	/* If 8 bits transfered, reset shift counter, call registered
-	 * callback, re-set nobyte in output buffer.
-	 */
-	if (gblink->shift == 8) {
-		gblink->shift = 0;
-
-		/* Set up next out byte before calling the callback.
-		 * This is in case the callback itself sets a new out
-		 * byte which it will in most cases. It is up to the
-		 * main application at this time to ensure that
-		 * gblink_transfer() isn't called multiple times before
-		 * a byte has a chance to be sent out.
-		 */
-		if (gblink->out_buf_valid) {
-			gblink->out = gblink->out_buf;
-			gblink->out_buf_valid = false;
-		} else {
-			gblink->out = gblink->nobyte;
-		}
-		gblink->callback(gblink->cb_context, gblink->in);
-	}
-}
-
-static void gblink_shift_out(struct gblink *gblink)
-{
-	furi_hal_gpio_write(gblink->serout, !!(gblink->out & 0x80));
-	gblink->out <<= 1;
-}
-
-static void gblink_clk_isr(void *context)
-{
-	furi_assert(context);
-	struct gblink *gblink = context;
-
-	if (furi_hal_gpio_read(gblink->clk)) {
-		/* Posedge Shift in data */
-		gblink_shift_in(gblink);
-	} else {
-		/* Negedge shift out data */
-		gblink_shift_out(gblink);
-	}
-}
-
-/* NOTE WELL! This function is absurdly hacky and a stupid workaround to a
- * stupid issue that doesn't really have any other solution in the current
- * Flipper/FURI API. I'm over-commenting this so we know exactly what is going
- * on if we ever have to re-visit this mess.
- *
- * This block of text below describes the overall idea, more specific comments
- * in the function body.
- *
- * TODO: make this more generic for any other GPIOs that might conflict with
- * exti interrupts. PA6, PB3, PC3, PB2? (NFC), PA13, PB6
- * NOTE: This is only set up at the moment for PB3, hardcoded
- *
- * There are multiple problems that this workaround is handling. EXTI interrupts
- * are shared among multiple pins. The FURI core maintains per-pin ISRs in a
- * private struct that has no way to read, save, or otherwise be able to put
- * back the ISR that would service a conflicting EXTI. e.g. PB3 and PH3
- * (the OK button) both share EXTI3. Setting an interrupt on PB3 will clobber
- * the FURI ISR callback/context pair as well as change EXTI3 to use PB3 as
- * the interrupt source.
- *
- * To make an interrupt work correctly on PB3 and not break the OK button
- * we need a way to set an interrupt for PB3 in a way that doesn't clobber the
- * private FURI GPIO ISR handles and can let the interrupt for the OK button
- * work again when we're done.
- *
- * The general concept of this workaround is to modify the IVT to create our
- * own handler for EXTI3 interrupts. Doing this leaves the aforementioned private
- * GPIO struct unmodified and disables the OK button from triggering an interrupt.
- * The IVT is normally located at the lowest addresses of flash (which is located
- * at 0x08000000 and mapped at runtime to 0x00000000); this means the IVT cannot
- * be changed at runtime.
- *
- * To make this work, we use the Vector Table Offset Register (VTOR) in the
- * System Control Block (SCB). The VTOR allows for changing the location of the
- * IVT. We copy the IVT to a location in memory, and then do a dance to safely
- * set up the GPIO interrupt to PB3, and swap in our IVT with the modified EXTI3
- * handler.
- *
- * When undoing this, the process is not quite in reverse as we have to put back
- * specific interrupt settings that we very likely would have clobbered but have
- * the ability to save beforehand.
- *
- * Wrapping the steps in disabling the EXTI3 interrupt is probably not needed,
- * but is a precaution since we are changing the interrupt sources in weird ways.
- */
-/* Used to map our callback context in a way the handler can access */
-static void *exti3_cb_context;
-static void gblink_exti3_IRQHandler(void) {
-	if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_3)) {
-		gblink_clk_isr(exti3_cb_context);
-		LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_3);
-	}
-}
-
-static void gblink_gross_exti_workaround(struct gblink *gblink)
-{
-	/* This process makes a number of assumptions, including that the IVT
-	 * is located at 0x00000000, that the lowest flash page is mapped to
-	 * that base address, and that the VTOR points to 0x00000000.
-	 * There are runtime protections in place to prevent reading from the
-	 * first 1 MB of addresses. So we have to always assume that the lowest
-	 * page of flash is mapped to 0x00000000 and read the IVT from the that
-	 * page in flash directly.
-	 * The only check we can really do here is ensuring VTOR is 0 and that
-	 * Main memory is mapped to 0x00000000. If either of those are not true,
-	 * then we can't continue.
-	 */
-	furi_check(SCB->VTOR == 0x0);
-	furi_check(LL_SYSCFG_GetRemapMemory() == LL_SYSCFG_REMAP_FLASH);
-
-	/* Create a mirror of the existing IVT from CPU 1
-	 * The IVT on this platform has 79 entries; 63 maskable, 10 non-maskable,
-	 * 6 reserved. The maskable interrupts start at offset 16.
-	 * CMSIS documentation says that the boundary for IVT must be aligned to
-	 * the number of interrupts, rounded up to the nearest power of two, and
-	 * then multiplied by the word width of the CPU. 79 rounds up to 128
-	 * with a word width of 4, this is 512/0x200 bytes.
-	 * As there is no good way with FreeRTOS to request an alloc at an
-	 * aligned boundary, allocate the amount of data we need, plus 0x200
-	 * bytes, to guarantee that we can put the table in a location that is
-	 * properly aligned. Once we find a suitable base address, this offset
-	 * is saved for later.
-	 */
-	gblink->ivt_mirror = malloc((79 * sizeof(uint32_t)) + 0x200);
-	gblink->ivt_mirror_offs = (uint32_t)gblink->ivt_mirror;
-	while (gblink->ivt_mirror_offs & 0x1FF)
-		gblink->ivt_mirror_offs++;
-	/* 0x08000000 is used instead of 0x00000000 because everything complains
-	 * using a NULL pointer.
-	 */
-	memcpy((uint32_t *)gblink->ivt_mirror_offs, ((uint32_t *)0x08000000), 79 * sizeof(uint32_t));
-
-	/* Point our IVT's EXTI3 interrupt to our desired interrupt handler.
-	 * Also copy the gblink struct to the global var that the interrupt
-	 * handler will use to make further calls.
-	 */
-	((uint32_t *)gblink->ivt_mirror_offs)[25] = (uint32_t)gblink_exti3_IRQHandler; // 16 NMI + offset of 9 for EXTI3
-	exti3_cb_context = gblink;
-
-	/* Disable the EXTI3 interrupt. This lets us do bad things without
-	 * fear of an IRQ hitting in the middle.
-	 */
-	LL_EXTI_DisableIT_0_31(LL_EXTI_LINE_3);
-
-	/* Save the existing rise/fall trigger settings. In theory, these should
-	 * really never change through the life of the flipper OS. But for safety
-	 * we always save them rather than just blindly restoring the same settings
-	 * back when we undo this later.
-	 */
-	gblink->exti3_rise_enable = LL_EXTI_IsEnabledRisingTrig_0_31(LL_EXTI_LINE_3);
-	gblink->exti3_fall_enable = LL_EXTI_IsEnabledFallingTrig_0_31(LL_EXTI_LINE_3);
-	gblink->exti3_event_enable = LL_EXTI_IsEnabledEvent_0_31(LL_EXTI_LINE_3);
-
-	/* Now, set up our desired pin settings. This will only clobber exti3
-	 * settings and will not affect the actual interrupt vector address.
-	 * Settings include the rising/falling/event triggers which we just
-	 * saved.
-	 */
-	furi_hal_gpio_init(gblink->clk, GpioModeInterruptRiseFall, GpioPullUp, GpioSpeedVeryHigh);
-
-	/* Update the NVIC table to point at our desired table.
-	 * Out of safety, stop the world around changing the VTOR reg.
-	 */
-	FURI_CRITICAL_ENTER();
-	SCB->VTOR = gblink->ivt_mirror_offs;
-	FURI_CRITICAL_EXIT();
-
-	/* Last, enable the interrupts and hope everything works. */
-	LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_3);
-}
-
-static void gblink_gross_exti_workaround_undo(struct gblink *gblink)
-{
-	/* First, disable the EXTI3 interrupt. This lets us do bad things without
-	 * fear of an IRQ hitting in the middle.
-	 */
-	LL_EXTI_DisableIT_0_31(LL_EXTI_LINE_3);
-
-	/* Set the correct input source, PH3/OK button, to EXTI3. It is important
-	 * to do this before calling furi_hal_gpio_init() on PB3. When that func
-	 * is called with no interrupt settings enabled, if the EXTI source
-	 * matches the pin, and the interrupt is enabled, interrupts will be
-	 * disabled. By manually setting the EXTI3 source here, it no longer
-	 * matches the PB3 pin, and our changing of IO settings on our GPIO pin
-	 * to no longer have interrupts will not affect the shared IRQ.
-	 */
-	LL_SYSCFG_SetEXTISource(LL_SYSCFG_EXTI_PORTH, LL_SYSCFG_EXTI_LINE3);
-
-	/* Set the correct rise/fall/event settings back */
-	if (gblink->exti3_rise_enable)
-		LL_EXTI_EnableRisingTrig_0_31(LL_EXTI_LINE_3);
-	else
-		LL_EXTI_DisableRisingTrig_0_31(LL_EXTI_LINE_3);
-
-	if (gblink->exti3_fall_enable)
-		LL_EXTI_EnableFallingTrig_0_31(LL_EXTI_LINE_3);
-	else
-		LL_EXTI_DisableFallingTrig_0_31(LL_EXTI_LINE_3);
-
-	if (gblink->exti3_event_enable)
-		LL_EXTI_EnableEvent_0_31(LL_EXTI_LINE_3);
-	else
-		LL_EXTI_DisableEvent_0_31(LL_EXTI_LINE_3);
-
-	/* "Release" the GPIO by putting it back in a known idle state. */
-	furi_hal_gpio_init_simple(gblink->clk, GpioModeAnalog);
-
-	/* Set the IVT back to the normal, in-flash table. Stopping the world
-	 * while we do so.
-	 * NOTE: This just assumes the VTOR is always at 0x0 by default, if this
-	 * ever changes in the Flipper OS, then that will be a problem.
-	 */
-	FURI_CRITICAL_ENTER();
-	SCB->VTOR = 0x0;
-	FURI_CRITICAL_EXIT();
-
-	/* Re-enable the interrupt, OK button should work again. */
-	LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_3);
-
-	/* Free the alloc()ed mirror space */
-	free(gblink->ivt_mirror);
-}
-
-void gblink_clk_source_set(void *handle, int source)
-{
-	furi_assert(handle);
-	struct gblink *gblink = handle;
-
-	gblink->source = source;
-	gblink->shift = 0;
-}
-
-void gblink_speed_set(void *handle, gblink_speed speed)
-{
-	furi_assert(handle);
-	struct gblink *gblink = handle;
-
-	gblink->speed = speed;
-}
-
-/* default is set to 500 us */
-void gblink_timeout_set(void *handle, uint32_t us)
-{
-	furi_assert(handle);
-	struct gblink *gblink = handle;
-
-	gblink->bitclk_timeout_us = us;
-}
-
-void gblink_transfer(void *handle, uint8_t val)
-{
-	furi_assert(handle);
-	struct gblink *gblink = handle;
-
-	/* This checks the value of gblink->shift which can change in the ISR.
-	 * Because of that, disable interrupts when checking gblink->shift and
-	 * setting gblink->out_buf_valid
-	 * If shift is 0, we're between bytes and can safely set the out byte.
-	 * If shift is nonzero, a byte is currently being transmitted. Set the
-	 * out_buf and set out_buf_valid. When the ISR is finished writing the
-	 * next byte it will check out_buf_valid and copy in out_buf.
-	 *
-	 * The correct/smart way of doing this would be a mutex rather than
-	 * stopping the world.
-	 *
-	 * Realistically, this should only ever be called from the transfer
-	 * complete callback. There are few situations outside of that which
-	 * would make sense.
-	 *
-	 * Note that, this currently has no checks for if there is data already
-	 * pending to be transmitted. Calling this back to back can cause data
-	 * loss!
-	 */
-	FURI_CRITICAL_ENTER();
-	if (gblink->shift == 0) {
-		gblink->out = val;
-		gblink->out_buf_valid = false;
-	} else {
-		gblink->out_buf = val;
-		gblink->out_buf_valid = true;
-	}
-	FURI_CRITICAL_EXIT();
-}
-
-void gblink_nobyte_set(void *handle, uint8_t val)
-{
-	struct gblink *gblink = handle;
-	gblink->nobyte = val;
-}
-
-void gblink_int_enable(void *handle)
-{
-	furi_assert(handle);
-	struct gblink *gblink = handle;
-
-	furi_hal_gpio_enable_int_callback(gblink->clk);
-}
-
-void gblink_int_disable(void *handle)
-{
-	furi_assert(handle);
-	struct gblink *gblink = handle;
-
-	furi_hal_gpio_disable_int_callback(gblink->clk);
-}
-
-void *gblink_alloc(struct gblink_def *gblink_def)
-{
-	struct gblink *gblink;
-
-	/* Allocate and zero struct */
-	gblink = malloc(sizeof(struct gblink));
-
-	/* Set struct values from function args */
-	gblink->serin = gblink_def->pins->serin;
-	gblink->serout = gblink_def->pins->serout;
-	gblink->clk = gblink_def->pins->clk;
-	gblink->sd = gblink_def->pins->sd;
-	gblink->source = gblink_def->source;
-	gblink->speed = GBLINK_SPD_8192HZ;
-
-	/* Set up timeout variables */
-	gblink->bitclk_timeout_us = 500;
-	gblink->time = DWT->CYCCNT;
-
-	/* Set up secondary callback */
-	gblink->callback = gblink_def->callback;
-	gblink->cb_context = gblink_def->cb_context;
-
-	/* Set up pins */
-	/* TODO: Set up a list of pins that are not safe to use with interrupts.
-	 * I do believe the main FURI GPIO struct has this data baked in so that
-	 * could be used. For now though, we're only checking for the MALVEKE
-	 * pinout which uses a clk pin that has its IRQ shared with the Okay
-	 * button.
-	 * See the work done in pokemon trade tool custom pinout selection for
-	 * an idea of how to check all that.
-	 */
-	/* TODO: Currently assumes external clock source only */
-	/* XXX: This might actually be open-drain on real GB hardware */
-	furi_hal_gpio_write(gblink->serout, false);
-	furi_hal_gpio_init(gblink->serout, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
-	furi_hal_gpio_write(gblink->serin, false);
-	furi_hal_gpio_init(gblink->serin, GpioModeInput, GpioPullUp, GpioSpeedVeryHigh);
-
-	/* Set up interrupt on clock */
-	if (gblink->clk == &gpio_ext_pb3) {
-		/* The clock pin is on a pin that is not safe to set an interrupt
-		 * on, so we do a gross workaround to get an interrupt enabled
-		 * on that pin in a way that can be undone safely later with
-		 * no impact to the shared IRQ.
-		 */
-		gblink_gross_exti_workaround(gblink);
-	} else {
-		furi_hal_gpio_init(gblink->clk, GpioModeInterruptRiseFall, GpioPullUp, GpioSpeedVeryHigh);
-		/* This may not be needed after NFC refactor */
-		furi_hal_gpio_remove_int_callback(gblink->clk);
-		furi_hal_gpio_add_int_callback(gblink->clk, gblink_clk_isr, gblink);
-	}
-
-	return gblink;
-}
-
-void gblink_free(void *handle)
-{
-	furi_assert(handle);
-	struct gblink *gblink = handle;
-
-	if (gblink->clk == &gpio_ext_pb3) {
-		/* This handles switching the IVT back and putting the EXTI
-		 * regs and pin regs in a valid state for normal use.
-		 */
-		gblink_gross_exti_workaround_undo(gblink);
-	} else {
-		/* Remove interrupt, set IO to sane state */
-		furi_hal_gpio_remove_int_callback(gblink->clk);
-	}
-	furi_hal_gpio_init_simple(gblink->serin, GpioModeAnalog);
-	furi_hal_gpio_init_simple(gblink->serout, GpioModeAnalog);
-	furi_hal_gpio_init_simple(gblink->clk, GpioModeAnalog);
-	free(gblink);
-}

+ 0 - 86
pokemon_trading/lib/flipper-gblink/gblink.h

@@ -1,86 +0,0 @@
-// SPDX-License-Identifier: BSD-2-Clause
-// Copyright (c) 2023 KBEmbedded
-
-#ifndef __GBLINK_H__
-#define __GBLINK_H__
-
-#pragma once
-
-#include <furi.h>
-#include <furi_hal.h>
-#include <stdint.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef enum {
-	/* Flipper drives the clock line */
-	/* Unsupported at this time */
-	GBLINK_INTERNAL_CLK,
-	/* Game Boy drives the clock line */
-	GBLINK_EXTERNAL_CLK,
-} gblink_clk_source;
-
-/* Currently unused */
-typedef enum {
-	GBLINK_MODE_GBC,
-	GBLINK_MODE_GBA,
-} gblink_mode;
-
-/* Should this just be a macro? */
-/* This pretty much only applies to GBC, OG GB is 8192 Hz only */
-/* This is only for TX */
-typedef enum {
-	GBLINK_SPD_8192HZ,
-	GBLINK_SPD_16384HZ,
-	GBLINK_SPD_262144HZ,
-	GBLINK_SPD_524288HZ,
-} gblink_speed;
-
-struct gblink_pins {
-        const GpioPin *serin;
-        const GpioPin *serout;
-        const GpioPin *clk;
-        const GpioPin *sd;
-};
-
-typedef enum {
-	PINOUT_ORIGINAL,
-	PINOUT_MALVEKE_EXT1,
-	PINOUT_COUNT,
-} gblink_pinout;
-
-extern const struct gblink_pins common_pinouts[PINOUT_COUNT];
-
-struct gblink_def {
-	struct gblink_pins *pins;
-	gblink_clk_source source;
-	gblink_mode mode;
-	void (*callback)(void* cb_context, uint8_t in);
-	void *cb_context;
-};
-
-void gblink_clk_source_set(void *handle, int clk_source);
-
-void gblink_speed_set(void *handle, gblink_speed speed);
-
-void gblink_timeout_set(void *handle, uint32_t us);
-
-void gblink_transfer(void *handle, uint8_t val);
-
-void gblink_nobyte_set(void *handle, uint8_t val);
-
-void gblink_int_enable(void *handle);
-
-void gblink_int_disable(void *handle);
-
-void *gblink_alloc(struct gblink_def *gblink_def);
-
-void gblink_free(void *handle);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif // __GBLINK_H__

+ 68 - 0
pokemon_trading/lib/flipper-gblink/gblink/clock_timer.c

@@ -0,0 +1,68 @@
+/*
+ * NOTE TO ANYONE USING THIS CODE
+ *
+ * The following is verbatim from the flipperzero-game-engine codebase and
+ * exists with a separate license, GPL-3.0 
+ *
+ * While the flipper-gblink is BSD-2, this project is always open-source.
+ *
+ * If you are using the flipper-gblink library in another project that is not
+ * open-source, then you should review the GPL-3.0 text to ensure you abide by
+ * the terms of the license before releasing a binary made with this code and
+ * not including the source alongside it.
+ *
+ */
+
+#include "clock_timer_i.h"
+#include <stdlib.h>
+
+#include <furi_hal_interrupt.h>
+#include <furi_hal_bus.h>
+#include <stm32wbxx_ll_tim.h>
+
+#define FURI_HAL_CLOCK_TIMER TIM2
+#define FURI_HAL_CLOCK_TIMER_BUS FuriHalBusTIM2
+#define FURI_HAL_CLOCK_TIMER_IRQ FuriHalInterruptIdTIM2
+
+typedef struct {
+    ClockTimerCallback callback;
+    void* context;
+} ClockTimer;
+
+static ClockTimer clock_timer = {
+    .callback = NULL,
+    .context = NULL,
+};
+
+static void clock_timer_isr(void* context) {
+    if(clock_timer.callback) {
+        clock_timer.callback(context);
+    }
+
+    LL_TIM_ClearFlag_UPDATE(FURI_HAL_CLOCK_TIMER);
+}
+
+void clock_timer_start(ClockTimerCallback callback, void* context, float period) {
+    clock_timer.callback = callback;
+    clock_timer.context = context;
+
+    furi_hal_bus_enable(FURI_HAL_CLOCK_TIMER_BUS);
+
+    // init timer to produce interrupts
+    LL_TIM_InitTypeDef TIM_InitStruct = {0};
+    TIM_InitStruct.Autoreload = (SystemCoreClock / period) - 1;
+    LL_TIM_Init(FURI_HAL_CLOCK_TIMER, &TIM_InitStruct);
+
+    furi_hal_interrupt_set_isr(FURI_HAL_CLOCK_TIMER_IRQ, clock_timer_isr, clock_timer.context);
+
+    LL_TIM_EnableIT_UPDATE(FURI_HAL_CLOCK_TIMER);
+    LL_TIM_EnableCounter(FURI_HAL_CLOCK_TIMER);
+}
+
+void clock_timer_stop(void) {
+    LL_TIM_DisableIT_UPDATE(FURI_HAL_CLOCK_TIMER);
+    LL_TIM_DisableCounter(FURI_HAL_CLOCK_TIMER);
+
+    furi_hal_bus_disable(FURI_HAL_CLOCK_TIMER_BUS);
+    furi_hal_interrupt_set_isr(FURI_HAL_CLOCK_TIMER_IRQ, NULL, NULL);
+}

+ 15 - 0
pokemon_trading/lib/flipper-gblink/gblink/clock_timer_i.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void (*ClockTimerCallback)(void* context);
+
+void clock_timer_start(ClockTimerCallback callback, void* context, float period);
+
+void clock_timer_stop(void);
+
+#ifdef __cplusplus
+}
+#endif

+ 202 - 0
pokemon_trading/lib/flipper-gblink/gblink/exti_workaround.c

@@ -0,0 +1,202 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// Copyright (c) 2023 KBEmbedded
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <stm32wbxx_ll_exti.h>
+#include <stm32wbxx_ll_system.h>
+
+#include <stdint.h>
+
+struct exti_workaround {
+	uint32_t* ivt_mirror;
+	uint32_t ivt_mirror_offs;
+	bool exti3_rise_enable;
+	bool exti3_fall_enable;
+	bool exti3_event_enable;
+	const GpioPin *clk;
+};
+
+/* NOTE WELL! This function is absurdly hacky and a stupid workaround to a
+ * stupid issue that doesn't really have any other solution in the current
+ * Flipper/FURI API. I'm over-commenting this so we know exactly what is going
+ * on if we ever have to re-visit this mess.
+ *
+ * This block of text below describes the overall idea, more specific comments
+ * in the function body.
+ *
+ * TODO: make this more generic for any other GPIOs that might conflict with
+ * exti interrupts. PA6, PB3, PC3, PB2? (NFC), PA13, PB6
+ * NOTE: This is only set up at the moment for PB3, hardcoded
+ *
+ * There are multiple problems that this workaround is handling. EXTI interrupts
+ * are shared among multiple pins. The FURI core maintains per-pin ISRs in a
+ * private struct that has no way to read, save, or otherwise be able to put
+ * back the ISR that would service a conflicting EXTI. e.g. PB3 and PH3
+ * (the OK button) both share EXTI3. Setting an interrupt on PB3 will clobber
+ * the FURI ISR callback/context pair as well as change EXTI3 to use PB3 as
+ * the interrupt source.
+ *
+ * To make an interrupt work correctly on PB3 and not break the OK button
+ * we need a way to set an interrupt for PB3 in a way that doesn't clobber the
+ * private FURI GPIO ISR handles and can let the interrupt for the OK button
+ * work again when we're done.
+ *
+ * The general concept of this workaround is to modify the IVT to create our
+ * own handler for EXTI3 interrupts. Doing this leaves the aforementioned private
+ * GPIO struct unmodified and disables the OK button from triggering an interrupt.
+ * The IVT is normally located at the lowest addresses of flash (which is located
+ * at 0x08000000 and mapped at runtime to 0x00000000); this means the IVT cannot
+ * be changed at runtime.
+ *
+ * To make this work, we use the Vector Table Offset Register (VTOR) in the
+ * System Control Block (SCB). The VTOR allows for changing the location of the
+ * IVT. We copy the IVT to a location in memory, and then do a dance to safely
+ * set up the GPIO interrupt to PB3, and swap in our IVT with the modified EXTI3
+ * handler.
+ *
+ * When undoing this, the process is not quite in reverse as we have to put back
+ * specific interrupt settings that we very likely would have clobbered but have
+ * the ability to save beforehand.
+ *
+ * Wrapping the steps in disabling the EXTI3 interrupt is probably not needed,
+ * but is a precaution since we are changing the interrupt sources in weird ways.
+ */
+/* Used to map our callback context in a way the handler can access */
+static void *exti3_cb_context;
+static void (*callback)(void *context);
+static void gblink_exti3_IRQHandler(void) {
+	if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_3)) {
+		callback(exti3_cb_context);
+		LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_3);
+	}
+}
+
+void *exti_workaround(const GpioPin *clk, void (*isr_callback)(void *context), void *context)
+{
+	struct exti_workaround *work = NULL;
+
+	/* This process makes a number of assumptions, including that the IVT
+	 * is located at 0x00000000, that the lowest flash page is mapped to
+	 * that base address, and that the VTOR points to 0x00000000.
+	 * There are runtime protections in place to prevent reading from the
+	 * first 1 MB of addresses. So we have to always assume that the lowest
+	 * page of flash is mapped to 0x00000000 and read the IVT from the that
+	 * page in flash directly.
+	 * The only check we can really do here is ensuring VTOR is 0 and that
+	 * Main memory is mapped to 0x00000000. If either of those are not true,
+	 * then we can't continue.
+	 */
+	furi_check(SCB->VTOR == 0x0);
+	furi_check(LL_SYSCFG_GetRemapMemory() == LL_SYSCFG_REMAP_FLASH);
+
+	/* Create a mirror of the existing IVT from CPU 1
+	 * The IVT on this platform has 79 entries; 63 maskable, 10 non-maskable,
+	 * 6 reserved. The maskable interrupts start at offset 16.
+	 * CMSIS documentation says that the boundary for IVT must be aligned to
+	 * the number of interrupts, rounded up to the nearest power of two, and
+	 * then multiplied by the word width of the CPU. 79 rounds up to 128
+	 * with a word width of 4, this is 512/0x200 bytes.
+	 * As there is no good way with FreeRTOS to request an alloc at an
+	 * aligned boundary, allocate the amount of data we need, plus 0x200
+	 * bytes, to guarantee that we can put the table in a location that is
+	 * properly aligned. Once we find a suitable base address, this offset
+	 * is saved for later.
+	 */
+	work = malloc(sizeof(struct exti_workaround));
+	work->ivt_mirror = malloc((79 * sizeof(uint32_t)) + 0x200);
+	work->ivt_mirror_offs = (uint32_t)work->ivt_mirror;
+	while (work->ivt_mirror_offs & 0x1FF)
+		work->ivt_mirror_offs++;
+	/* 0x08000000 is used instead of 0x00000000 because everything complains
+	 * using a NULL pointer.
+	 */
+	memcpy((uint32_t *)work->ivt_mirror_offs, ((uint32_t *)0x08000000), 79 * sizeof(uint32_t));
+
+	/* Point our IVT's EXTI3 interrupt to our desired interrupt handler.
+	 * Also copy the gblink struct to the global var that the interrupt
+	 * handler will use to make further calls.
+	 */
+	((uint32_t *)work->ivt_mirror_offs)[25] = (uint32_t)gblink_exti3_IRQHandler; // 16 NMI + offset of 9 for EXTI3
+	callback = isr_callback;
+	exti3_cb_context = context;
+
+	/* Disable the EXTI3 interrupt. This lets us do bad things without
+	 * fear of an IRQ hitting in the middle.
+	 */
+	LL_EXTI_DisableIT_0_31(LL_EXTI_LINE_3);
+
+	/* Save the existing rise/fall trigger settings. In theory, these should
+	 * really never change through the life of the flipper OS. But for safety
+	 * we always save them rather than just blindly restoring the same settings
+	 * back when we undo this later.
+	 */
+	work->exti3_rise_enable = LL_EXTI_IsEnabledRisingTrig_0_31(LL_EXTI_LINE_3);
+	work->exti3_fall_enable = LL_EXTI_IsEnabledFallingTrig_0_31(LL_EXTI_LINE_3);
+	work->exti3_event_enable = LL_EXTI_IsEnabledEvent_0_31(LL_EXTI_LINE_3);
+	work->clk = clk;
+
+	/* Update the NVIC table to point at our desired table.
+	 * Out of safety, stop the world around changing the VTOR reg.
+	 */
+	FURI_CRITICAL_ENTER();
+	SCB->VTOR = work->ivt_mirror_offs;
+	FURI_CRITICAL_EXIT();
+
+	/* Last, enable the interrupts and hope everything works. */
+	LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_3);
+
+	return work;
+}
+
+void exti_workaround_undo(void *handle)
+{
+	struct exti_workaround *work = handle;
+	/* First, disable the EXTI3 interrupt. This lets us do bad things without
+	 * fear of an IRQ hitting in the middle.
+	 */
+	LL_EXTI_DisableIT_0_31(LL_EXTI_LINE_3);
+
+	/* Set the correct input source, PH3/OK button, to EXTI3. It is important
+	 * to do this before calling furi_hal_gpio_init() on PB3. When that func
+	 * is called with no interrupt settings enabled, if the EXTI source
+	 * matches the pin, and the interrupt is enabled, interrupts will be
+	 * disabled. By manually setting the EXTI3 source here, it no longer
+	 * matches the PB3 pin, and our changing of IO settings on our GPIO pin
+	 * to no longer have interrupts will not affect the shared IRQ.
+	 */
+	LL_SYSCFG_SetEXTISource(LL_SYSCFG_EXTI_PORTH, LL_SYSCFG_EXTI_LINE3);
+
+	/* Set the correct rise/fall/event settings back */
+	if (work->exti3_rise_enable)
+		LL_EXTI_EnableRisingTrig_0_31(LL_EXTI_LINE_3);
+	else
+		LL_EXTI_DisableRisingTrig_0_31(LL_EXTI_LINE_3);
+
+	if (work->exti3_fall_enable)
+		LL_EXTI_EnableFallingTrig_0_31(LL_EXTI_LINE_3);
+	else
+		LL_EXTI_DisableFallingTrig_0_31(LL_EXTI_LINE_3);
+
+	if (work->exti3_event_enable)
+		LL_EXTI_EnableEvent_0_31(LL_EXTI_LINE_3);
+	else
+		LL_EXTI_DisableEvent_0_31(LL_EXTI_LINE_3);
+
+
+	/* Set the IVT back to the normal, in-flash table. Stopping the world
+	 * while we do so.
+	 * NOTE: This just assumes the VTOR is always at 0x0 by default, if this
+	 * ever changes in the Flipper OS, then that will be a problem.
+	 */
+	FURI_CRITICAL_ENTER();
+	SCB->VTOR = 0x0;
+	FURI_CRITICAL_EXIT();
+
+	/* Re-enable the interrupt, OK button should work again. */
+	LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_3);
+
+	/* Free the alloc()ed mirror space */
+	free(work->ivt_mirror);
+	free(work);
+}

+ 22 - 0
pokemon_trading/lib/flipper-gblink/gblink/exti_workaround_i.h

@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// Copyright (c) 2023 KBEmbedded
+
+#ifndef __EXTI_WORKAROUND_H__
+#define __EXTI_WORKAROUND_H__
+
+#include <furi.h>
+#include <furi_hal.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void *exti_workaround(const GpioPin *clk, void (*isr_callback)(void *context), void *context);
+
+void exti_workaround_undo(void *handle);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // __EXTI_WORKAROUND_H__

+ 593 - 0
pokemon_trading/lib/flipper-gblink/gblink/gblink.c

@@ -0,0 +1,593 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// Copyright (c) 2023 KBEmbedded
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <stm32wbxx_ll_exti.h>
+#include <stm32wbxx_ll_system.h>
+
+#include <stdint.h>
+
+#include <gblink/include/gblink.h>
+#include "exti_workaround_i.h"
+#include "clock_timer_i.h"
+
+const struct gblink_pins common_pinouts[PINOUT_COUNT] = {
+	/* Original */
+	{
+		&gpio_ext_pc3,
+		&gpio_ext_pb3,
+		&gpio_ext_pb2,
+		&gpio_ext_pa4,
+	},
+	/* MALVEKE EXT1 */
+	{
+		&gpio_ext_pa6,
+		&gpio_ext_pa7,
+		&gpio_ext_pb3,
+		&gpio_ext_pa4,
+	},
+};
+
+struct gblink {
+	const GpioPin *serin;
+	const GpioPin *serout;
+	const GpioPin *clk;
+	const GpioPin *sd;
+	gblink_mode mode;
+	void (*callback)(void* cb_context, uint8_t in);
+	void *cb_context;
+
+	/* These two semaphores serve similar but distinct purposes. */
+	/* The transfer semaphore is taken as soon as a transfer() request
+	 * has been started. This is used in the function to wait until the
+	 * transfer has been completed.
+	 */
+	FuriSemaphore *transfer_sem;
+	/* The out byte semaphore is used to indicate that a byte transfer
+	 * is in progress. This is used in the transfer function to not allow
+	 * a transfer request if we're in the middle of sending a byte.
+	 * The transfer semaphore is not used for that purpose since if the
+	 * Flipper is in EXT clk mode, once a transfer() is started, there
+	 * would be no way to both prevent transfer() from being called again
+	 * as well as cancelling/changing what we're wanting to send. Using
+	 * out byte semaphore means a transfer() can be called at any time,
+	 * waited on synchronously for a timeout, and then re-called at a
+	 * later time; while blocking that update if a byte is actually
+	 * in the middle of being transmitted.
+	 */
+	FuriSemaphore *out_byte_sem;
+
+	/* Used to lock out changing things after a certain point. Pinout,
+	 * mode, etc.
+	 * XXX: Might make more sense to use the mutex to protect a flag?
+	 * Maybe a semaphore? Though I think that is the wrong use.
+	 */
+	FuriMutex *start_mutex;
+
+	/* 
+	 * The following should probably have the world stopped around them
+	 * if not modified in an interrupt context.
+	 */
+	uint8_t in;
+	uint8_t out;
+	uint8_t shift;
+	uint8_t nobyte;
+
+	/* Should only be changed when not in middle of tx, will affect a lot */
+	gblink_clk_source source;
+
+	/* Can be changed at any time, will only take effect on the next
+	 * transfer.
+	 */
+	gblink_speed speed;
+
+
+	/* 
+	 * The following is based on observing Pokemon trade data
+	 *
+	 * Clocks idle between bytes is nominally 430 us long for burst data,
+	 * 15 ms for idle polling (e.g. waiting for menu selection), some oddball
+	 * 2 ms gaps that appears between one 0xFE byte from the Game Boy every trade;
+	 * clock period is nominally 122 us.
+	 *
+	 * Therefore, if we haven't seen a clock in 500 us, reset our bit counter.
+	 * Note that, this should never actually be a concern, but it is an additional
+	 * safeguard against desyncing.
+	 */
+	uint32_t time;
+	uint32_t bitclk_timeout_us;
+
+	void *exti_workaround_handle;
+};
+
+static inline bool gblink_transfer_in_progress(struct gblink *gblink)
+{
+	return !(furi_semaphore_get_count(gblink->out_byte_sem));
+}
+
+static void gblink_shift_in_isr(struct gblink *gblink)
+{
+	const uint32_t time_ticks = furi_hal_cortex_instructions_per_microsecond() * gblink->bitclk_timeout_us;
+
+	if (gblink->source == GBLINK_CLK_INT)
+		furi_hal_gpio_write(gblink->clk, 1);
+
+	/* If we exceeded the bit clock timeout, reset all counters */
+	if ((DWT->CYCCNT - gblink->time) > time_ticks) {
+		gblink->in = 0;
+		gblink->shift = 0;
+	}
+	gblink->time = DWT->CYCCNT;
+
+	gblink->in <<= 1;
+	gblink->in |= furi_hal_gpio_read(gblink->serin);
+	gblink->shift++;
+	/* If 8 bits transfered, reset shift counter, call registered
+	 * callback, re-set nobyte in output buffer.
+	 */
+	if (gblink->shift == 8) {
+	 	if (gblink->source == GBLINK_CLK_INT)
+			clock_timer_stop();
+
+		gblink->shift = 0;
+
+		/* 
+		 * Set up next out byte before calling the callback.
+		 * This is in case the callback itself sets a new out
+		 * byte which it will in most cases.
+		 *
+		 * The nobyte value is set in place as the next output byte,
+		 * in case the flipper does not set a real byte before the next
+		 * transfer starts.
+		 */
+		gblink->out = gblink->nobyte;
+		furi_semaphore_release(gblink->out_byte_sem);
+
+		/* 
+		 * Call the callback, if set, and then release the semaphore
+		 * in case a thread is waiting on TX to complete.
+		 */
+		if (gblink->callback)
+			gblink->callback(gblink->cb_context, gblink->in);
+
+		furi_semaphore_release(gblink->transfer_sem);
+	}
+}
+
+static void gblink_shift_out_isr(struct gblink *gblink)
+{
+	furi_semaphore_acquire(gblink->out_byte_sem, 0);
+	furi_hal_gpio_write(gblink->serout, !!(gblink->out & 0x80));
+	gblink->out <<= 1;
+
+	/* XXX: TODO: Check that this is the correct thing with open drain.
+	 * does 0 value actually drive the line low, or high?
+	 */
+	if (gblink->source == GBLINK_CLK_INT)
+		furi_hal_gpio_write(gblink->clk, 0);
+}
+
+static void gblink_clk_isr(void *context)
+{
+	furi_assert(context);
+	struct gblink *gblink = context;
+	bool out = false;
+
+	/* 
+	 * Whether we're shifting in or out is dependent on the clock source.
+	 * If external, and the clock line is high, that means a posedge just
+	 * occurred and we need to shift data in.
+	 *
+	 * If internal, and the clock line is high, that means we're about
+	 * to drive a negedge and need to shift data out.
+	 *
+	 * The actual in/out functions drive the clock state at the right times
+	 * if the clock is internal source.
+	 */
+	out = (furi_hal_gpio_read(gblink->clk) ==
+	      (gblink->source == GBLINK_CLK_INT));
+
+	if (out)
+		gblink_shift_out_isr(gblink);
+	else
+		gblink_shift_in_isr(gblink);
+}
+
+/* 
+ * Call to set up the clk pin modes to do the right thing based on if INT or
+ * EXT clock source is configured.
+ */
+static void gblink_clk_configure(struct gblink *gblink)
+{
+	if (gblink->source == GBLINK_CLK_EXT) {
+		furi_hal_gpio_init(gblink->clk, GpioModeInterruptRiseFall, GpioPullUp, GpioSpeedVeryHigh);
+		/* furi_hal_gpio_init, while it sets interrupt settings on the GPIO,
+		 * does not actually enable the EXTI interrupt.
+		 */
+		gblink_int_enable(gblink);
+	} else {
+		/* This will disable the EXTI interrupt for us */
+		furi_hal_gpio_init(gblink->clk, GpioModeOutputOpenDrain, GpioPullUp, GpioSpeedVeryHigh);
+	};
+}
+
+void gblink_clk_source_set(void *handle, gblink_clk_source source)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	if (source == gblink->source)
+		return;
+	
+	/*
+	 * NOTE:
+	 * I'm not sure the best way to handle this at the moment. In theory,
+	 * it should be safe to check that we're just not in the middle of a
+	 * transfer and not worry about getting stuck.
+	 * However, I'm not really sure how true that is, so for now this will
+	 * always change the source and reset the current byte transfer.
+	 * It is up to the callee to ensure that they are between bytes.
+	 *
+	 * One idea would be to get the semaphore, but wait the set timeout.
+	 * if that is exceeded or the semaphore is acquired, then its probably
+	 * safe to change the source and reset shift register.
+	 */
+
+	gblink->source = source;
+	gblink->shift = 0;
+
+	gblink_clk_configure(gblink);
+}
+
+void gblink_speed_set(void *handle, gblink_speed speed)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	/*
+	 * This does not need any protection, it will take effect at the start
+	 * of the next byte.
+	 */
+	gblink->speed = speed;
+}
+
+/* default is set to 500 us */
+void gblink_timeout_set(void *handle, uint32_t us)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	gblink->bitclk_timeout_us = us;
+}
+
+int gblink_pin_set(void *handle, gblink_bus_pins pin, const GpioPin *gpio)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	if (furi_mutex_acquire(gblink->start_mutex, 0) != FuriStatusOk)
+		return 1;
+
+	switch (pin) {
+	case PIN_SERIN:
+		gblink->serin = gpio;
+		break;
+	case PIN_SEROUT:
+		gblink->serout = gpio;
+		break;
+	case PIN_CLK:
+		gblink->clk = gpio;
+		break;
+	case PIN_SD:
+		gblink->sd = gpio;
+		break;
+	default:
+		furi_crash();
+		break;
+	}
+
+	furi_mutex_release(gblink->start_mutex);
+
+	return 0;
+}
+
+int gblink_pin_set_default(void *handle, gblink_pinouts pinout)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	if (furi_mutex_acquire(gblink->start_mutex, 0) != FuriStatusOk)
+		return 1;
+
+	gblink->serin = common_pinouts[pinout].serin;
+	gblink->serout = common_pinouts[pinout].serout;
+	gblink->clk = common_pinouts[pinout].clk;
+	gblink->sd = common_pinouts[pinout].sd;
+
+	furi_mutex_release(gblink->start_mutex);
+
+	return 0;
+}
+
+
+const GpioPin *gblink_pin_get(void *handle, gblink_bus_pins pin)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	switch (pin) {
+	case PIN_SERIN:
+		return gblink->serin;
+	case PIN_SEROUT:
+		return gblink->serout;
+	case PIN_CLK:
+		return gblink->clk;
+	case PIN_SD:
+		return gblink->sd;
+	default:
+		furi_crash();
+		break;
+	}
+
+	return NULL;
+}
+
+int gblink_callback_set(void *handle, void (*callback)(void* cb_context, uint8_t in), void *cb_context)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	if (furi_mutex_acquire(gblink->start_mutex, 0) != FuriStatusOk)
+		return 1;
+
+	gblink->callback = callback;
+	gblink->cb_context = cb_context;
+	furi_mutex_release(gblink->start_mutex);
+
+	return 0;
+}
+
+int gblink_mode_set(void *handle, gblink_mode mode)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	if (furi_mutex_acquire(gblink->start_mutex, 0) != FuriStatusOk)
+		return 1;
+
+	gblink->mode = mode;
+	furi_mutex_release(gblink->start_mutex);
+
+	return 0;
+}
+
+
+bool gblink_transfer(void *handle, uint8_t val)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+	bool ret = false;
+
+
+	/* Stop the world, this is to ensure we can safely set the next out byte */
+	/*
+	 * The reason for and therefore issue of setting the next byte has a few
+	 * points to keep in mind.
+	 *
+	 * First, with EXT clock source, the first hint of the external device
+	 * clocking in data is a negative edge where it would set data. This
+	 * means that the next out byte needs to be set before that.
+	 *
+	 * Second, since the interrupt on the neg clock edge loads the next
+	 * byte in to serout after grabbing the semaphore; we can stop the
+	 * world right now, and set the byte if there is no transfer in
+	 * progress. As soon as the world is resumed, the IRQ will fire, and
+	 * the correct, new, data byte will start to be shifted out.
+	 */
+	FURI_CRITICAL_ENTER();
+
+	/* If we're in the middle of a tranfer, don't let the byte be set. */
+	if (!gblink_transfer_in_progress(gblink)) {
+		gblink->out = val;
+		ret = true;
+
+		/*
+		 * Now that we're this far, this means the byte we set will be
+		 * transferred one way or another. Because of that, take the
+		 * transfer semaphore. This gets released once a full byte has
+		 * been transferred. This is for the TX wait function. We cannot
+		 * use the out_byte_sem as if the wait is called immediately
+		 * after the transfer, and no data has yet been shifted out,
+		 * the TX wait function would incorrectly return immediately.
+		 */
+		furi_semaphore_acquire(gblink->transfer_sem, 0);
+	}
+
+	FURI_CRITICAL_EXIT();
+
+	/* 
+	 * If the out byte was successfully set, and we're driving the clock,
+	 * turn on our timer for byte transfer.
+	 */
+	if (ret && gblink->source == GBLINK_CLK_INT)
+		clock_timer_start(gblink_clk_isr, gblink, gblink->speed);
+
+	return ret;
+
+}
+
+uint8_t gblink_transfer_tx_wait_complete(void *handle)
+{
+	struct gblink *gblink = handle;
+
+	/* XXX: TODO: Think about how to implement this in a way that we can
+	 * use the semaphore to see if there is a transfer waiting to happen,
+	 * but not in a way that would incorrectly show a transfer waiting. e.g.
+	 * if this takes the semaphore, then the semaphore is in the same state
+	 * as if a transfer was in progress. Should this put back the semaphore
+	 * after acquiring it? Is there a better way of handling it?
+	 */
+
+	furi_semaphore_acquire(gblink->transfer_sem, FuriWaitForever);
+
+	return gblink->in;
+}
+
+void gblink_nobyte_set(void *handle, uint8_t val)
+{
+	struct gblink *gblink = handle;
+
+	/*
+	 * This is safe to run at any time. It is only copied in after a byte
+	 * transfer is completed.
+	 */
+	gblink->nobyte = val;
+}
+
+void gblink_int_enable(void *handle)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	/*
+	 * NOTE: This is currently safe to run even with the exti workaround
+	 * in effect. It just enables the root EXTI interrupt source of the
+	 * given pin.
+	 */
+	furi_hal_gpio_enable_int_callback(gblink->clk);
+}
+
+void gblink_int_disable(void *handle)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	/*
+	 * NOTE: This is currently safe to run even with the exti workaround
+	 * in effect. It just disables the root EXTI interrupt source of the
+	 * given pin.
+	 */
+	furi_hal_gpio_disable_int_callback(gblink->clk);
+}
+
+void *gblink_alloc(void)
+{
+	struct gblink *gblink;
+
+	/* Allocate and zero struct */
+	gblink = malloc(sizeof(struct gblink));
+	//gblink->spec = malloc(sizeof(struct gblink_spec));
+
+	gblink->transfer_sem = furi_semaphore_alloc(1, 1);
+	gblink->out_byte_sem = furi_semaphore_alloc(1, 1);
+	gblink->start_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+
+	/* Set defaults */
+	gblink_pin_set_default(gblink, PINOUT_ORIGINAL);
+	gblink_mode_set(gblink, GBLINK_MODE_GBC);
+	gblink_clk_source_set(gblink, GBLINK_CLK_EXT);
+	gblink_speed_set(gblink, GBLINK_SPD_8192HZ);
+	gblink_timeout_set(gblink, 500);
+
+	/* Set current time to start timeout calculations */
+	gblink->time = DWT->CYCCNT;
+
+	return gblink;
+}
+
+void gblink_start(void *handle)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	/* XXX: Check callback is valid */
+
+	furi_mutex_acquire(gblink->start_mutex, FuriWaitForever);
+
+	/* Set up pins */
+	/* TODO: Set up a list of pins that are not safe to use with interrupts.
+	 * I do believe the main FURI GPIO struct has this data baked in so that
+	 * could be used. For now though, we're only checking for the MALVEKE
+	 * pinout which uses a clk pin that has its IRQ shared with the Okay
+	 * button.
+	 * See the work done in pokemon trade tool custom pinout selection for
+	 * an idea of how to check all that.
+	 */
+	furi_hal_gpio_write(gblink->serout, false);
+	furi_hal_gpio_init(gblink->serout, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
+	furi_hal_gpio_write(gblink->serin, false);
+	furi_hal_gpio_init(gblink->serin, GpioModeInput, GpioPullUp, GpioSpeedVeryHigh);
+
+	/* Set up interrupt on clock pin */
+	if (gblink->clk == &gpio_ext_pb3) {
+		/* The clock pin is on a pin that is not safe to set an interrupt
+		 * on, so we do a gross workaround to get an interrupt enabled
+		 * on that pin in a way that can be undone safely later with
+		 * no impact to the shared IRQ.
+		 */
+		gblink->exti_workaround_handle = exti_workaround(gblink->clk, gblink_clk_isr, gblink);
+	} else {
+		/* This may not be needed after NFC refactor */
+		furi_hal_gpio_remove_int_callback(gblink->clk);
+		furi_hal_gpio_add_int_callback(gblink->clk, gblink_clk_isr, gblink);
+	}
+
+	/* The above immediately enables the interrupt, we don't want
+	 * that just yet and we want configure to handle it.
+	 */
+	gblink_int_disable(gblink);
+
+	gblink_clk_configure(gblink);
+}
+
+void gblink_stop(void *handle)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	/* If we can acquire the mutex, that means start was never actually
+	 * called. Crash.
+	 * XXX: Probably a bit harsh to just crash, can it gracefully recover
+	 * without too much effort?
+	 */
+	if (furi_mutex_acquire(gblink->start_mutex, 0) == FuriStatusOk) {
+		furi_crash();
+		return;
+	}
+
+	if (gblink->clk == &gpio_ext_pb3) {
+		/* This handles switching the IVT back and putting the EXTI
+		 * regs and pin regs in a valid state for normal use.
+		 */
+		exti_workaround_undo(gblink->exti_workaround_handle);
+	} else {
+		/* Remove interrupt, set IO to sane state */
+		furi_hal_gpio_remove_int_callback(gblink->clk);
+	}
+	furi_hal_gpio_init_simple(gblink->serin, GpioModeAnalog);
+	furi_hal_gpio_init_simple(gblink->serout, GpioModeAnalog);
+	furi_hal_gpio_init_simple(gblink->clk, GpioModeAnalog);
+
+	furi_mutex_release(gblink->start_mutex);
+}
+
+void gblink_free(void *handle)
+{
+	furi_assert(handle);
+	struct gblink *gblink = handle;
+
+	/* If we cannot acquire the mutex, that means the link was never properly
+	 * stopped. Crash.
+	 * XXX: Can this be gracefully handled?
+	 */
+	if (furi_mutex_acquire(gblink->start_mutex, 0) != FuriStatusOk) {
+		furi_crash();
+		return;
+	}
+	furi_mutex_release(gblink->start_mutex);
+	furi_mutex_free(gblink->start_mutex);
+	furi_semaphore_free(gblink->transfer_sem);
+	furi_semaphore_free(gblink->out_byte_sem);
+	free(gblink);
+}

+ 115 - 0
pokemon_trading/lib/flipper-gblink/gblink/include/gblink.h

@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// Copyright (c) 2023 KBEmbedded
+
+#ifndef __GBLINK_H__
+#define __GBLINK_H__
+
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+	/* Flipper drives the clock line */
+	/* Unsupported at this time */
+	GBLINK_CLK_INT,
+	/* Game Boy drives the clock line */
+	GBLINK_CLK_EXT,
+} gblink_clk_source;
+
+/* Currently unused */
+typedef enum {
+	GBLINK_MODE_GBC,
+	GBLINK_MODE_GBA,
+} gblink_mode;
+
+/* Should this just be a macro? */
+/* This pretty much only applies to GBC, OG GB is 8192 Hz only */
+/* This is only for TX */
+typedef enum {
+	GBLINK_SPD_8192HZ = 4096,
+	GBLINK_SPD_16384HZ = 8192,
+	GBLINK_SPD_262144HZ = 16384,
+	GBLINK_SPD_524288HZ = 262144,
+} gblink_speed;
+
+struct gblink_pins {
+        const GpioPin *serin;
+        const GpioPin *serout;
+        const GpioPin *clk;
+        const GpioPin *sd;
+};
+
+typedef enum {
+	PINOUT_ORIGINAL,
+	PINOUT_MALVEKE_EXT1,
+	PINOUT_COUNT,
+} gblink_pinouts;
+
+typedef enum {
+	PIN_SERIN,
+	PIN_SEROUT,
+	PIN_CLK,
+	PIN_SD,
+	PIN_COUNT,
+} gblink_bus_pins;
+
+/*
+ * NOTE:
+ * This can be called at any time, it resets the current byte transfer information
+ */
+void gblink_clk_source_set(void *handle, gblink_clk_source clk_source);
+
+void gblink_speed_set(void *handle, gblink_speed speed);
+
+void gblink_timeout_set(void *handle, uint32_t us);
+
+bool gblink_transfer(void *handle, uint8_t val);
+
+/* Can only be run after alloc, before start */
+int gblink_pin_set_default(void *handle, gblink_pinouts pinout);
+
+int gblink_pin_set(void *handle, gblink_bus_pins pin, const GpioPin *gpio);
+
+const GpioPin *gblink_pin_get(void *handle, gblink_bus_pins pin);
+
+int gblink_callback_set(void *handle, void (*callback)(void* cb_context, uint8_t in), void *cb_context);
+
+int gblink_mode_set(void *handle, gblink_mode mode);
+
+/* 
+ * This can be used for INT or EXT clock modes. After a call to
+ * gblink_transfer this can be called at any time and will return only after
+ * a full byte is transferred and it will return the byte that was last shifted
+ * in from the link partner.
+ */
+uint8_t gblink_transfer_tx_wait_complete(void *handle);
+
+void gblink_nobyte_set(void *handle, uint8_t val);
+
+void gblink_int_enable(void *handle);
+
+void gblink_int_disable(void *handle);
+
+/* Sets up some defaults */
+void *gblink_alloc(void);
+
+void gblink_free(void *handle);
+
+void gblink_start(void *handle);
+
+void gblink_stop(void *handle);
+
+// void gblink_blink_led_on_byte(handle, color?)
+// get blink?
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // __GBLINK_H__

+ 56 - 0
pokemon_trading/lib/flipper-gblink/protocols/printer/include/printer_proto.h

@@ -0,0 +1,56 @@
+#ifndef PRINTER_PROTO_H
+#define PRINTER_PROTO_H
+
+#include <gblink/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

+ 10 - 0
pokemon_trading/lib/flipper-gblink/protocols/printer/include/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

+ 100 - 0
pokemon_trading/lib/flipper-gblink/protocols/printer/printer_i.h

@@ -0,0 +1,100 @@
+#ifndef PRINTER_I_H
+#define PRINTER_I_H
+
+#include <protocols/printer/include/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
pokemon_trading/lib/flipper-gblink/protocols/printer/printer_proto.c

@@ -0,0 +1,146 @@
+#include <furi.h>
+
+#include <gblink/include/gblink.h>
+#include <protocols/printer/include/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);
+}

+ 224 - 0
pokemon_trading/lib/flipper-gblink/protocols/printer/printer_receive.c

@@ -0,0 +1,224 @@
+#include <stdint.h>
+
+#include <furi.h>
+
+#include <gblink/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;
+}