Sfoglia il codice sorgente

update apps and add new apps

MX 2 anni fa
parent
commit
bdbed43345
82 ha cambiato i file con 6662 aggiunte e 1789 eliminazioni
  1. 3 1
      ReadMe.md
  2. BIN
      apps/GPIO/gpio_controller.fap
  3. BIN
      apps/GPIO/tas_playback.fap
  4. BIN
      apps/GPIO/ublox.fap
  5. BIN
      apps/NFC/seader.fap
  6. BIN
      apps/Sub-GHz/esubghz_chat.fap
  7. BIN
      apps/Sub-GHz/tpms.fap
  8. BIN
      apps/Tools/flipper_chronometer.fap
  9. 13 6
      non_catalog_apps/esubghz_chat/README.md
  10. BIN
      non_catalog_apps/esubghz_chat/assets/Cry_dolph_55x52.png
  11. BIN
      non_catalog_apps/esubghz_chat/assets/DolphinNice_96x59.png
  12. 105 19
      non_catalog_apps/esubghz_chat/crypto_wrapper.c
  13. 5 2
      non_catalog_apps/esubghz_chat/crypto_wrapper.h
  14. 33 10
      non_catalog_apps/esubghz_chat/esubghz_chat.c
  15. 10 0
      non_catalog_apps/esubghz_chat/esubghz_chat_i.h
  16. 20 25
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_chat_input.c
  17. 1 1
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_freq_input.c
  18. 3 2
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_hex_key_input.c
  19. 2 2
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_key_display.c
  20. 7 6
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_key_menu.c
  21. 92 22
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_key_read_popup.c
  22. 3 2
      non_catalog_apps/esubghz_chat/scenes/esubghz_chat_pass_input.c
  23. 40 0
      non_catalog_apps/flipper_chronometer/README.md
  24. 13 0
      non_catalog_apps/flipper_chronometer/application.fam
  25. BIN
      non_catalog_apps/flipper_chronometer/chronometer.png
  26. 184 0
      non_catalog_apps/flipper_chronometer/flipper_chronometer.c
  27. 143 0
      non_catalog_apps/gpio_controller/app_defines.h
  28. 1 1
      non_catalog_apps/gpio_controller/application.fam
  29. 491 165
      non_catalog_apps/gpio_controller/gpio_controller.c
  30. 0 69
      non_catalog_apps/gpio_controller/gpio_items.c
  31. 0 29
      non_catalog_apps/gpio_controller/gpio_items.h
  32. BIN
      non_catalog_apps/gpio_controller/images/analog_box.png
  33. BIN
      non_catalog_apps/gpio_controller/images/digi_one.png
  34. BIN
      non_catalog_apps/gpio_controller/images/digi_zero.png
  35. 113 43
      non_catalog_apps/seader/ccid.c
  36. 20 5
      non_catalog_apps/seader/ccid.h
  37. 2 2
      non_catalog_apps/seader/seader_worker.c
  38. 1 1
      non_catalog_apps/seader/seader_worker.h
  39. 21 0
      non_catalog_apps/tas_playback/LICENSE
  40. 11 0
      non_catalog_apps/tas_playback/README.md
  41. 767 0
      non_catalog_apps/tas_playback/WString.cpp
  42. 240 0
      non_catalog_apps/tas_playback/WString.h
  43. 15 0
      non_catalog_apps/tas_playback/application.fam
  44. 51 0
      non_catalog_apps/tas_playback/avr_functions.h
  45. 2 0
      non_catalog_apps/tas_playback/changelog.md
  46. BIN
      non_catalog_apps/tas_playback/images/Pin_back_arrow_10x8.png
  47. BIN
      non_catalog_apps/tas_playback/images/tas_playback.png
  48. 108 0
      non_catalog_apps/tas_playback/nonstd.c
  49. BIN
      non_catalog_apps/tas_playback/screenshots/file-select.png
  50. BIN
      non_catalog_apps/tas_playback/screenshots/running.png
  51. 494 0
      non_catalog_apps/tas_playback/tas_playback.cxx
  52. 264 0
      non_catalog_apps/tas_playback/teensy/crc_table.h
  53. 1295 0
      non_catalog_apps/tas_playback/teensy/teensy.ino
  54. 13 2
      non_catalog_apps/tpms_receiver/Readme.md
  55. 2 2
      non_catalog_apps/tpms_receiver/protocols/schrader_gg4.c
  56. 35 9
      non_catalog_apps/tpms_receiver/scenes/tpms_scene_about.c
  57. BIN
      non_catalog_apps/tpms_receiver/tpms.gif
  58. 5 1
      non_catalog_apps/ublox/README.md
  59. 1 0
      non_catalog_apps/ublox/application.fam
  60. 86 0
      non_catalog_apps/ublox/helpers/kml.c
  61. 22 0
      non_catalog_apps/ublox/helpers/kml.h
  62. 2 1
      non_catalog_apps/ublox/helpers/ublox_custom_event.h
  63. 36 24
      non_catalog_apps/ublox/helpers/ublox_types.h
  64. BIN
      non_catalog_apps/ublox/images/ublox_wiring.png
  65. BIN
      non_catalog_apps/ublox/images/ublox_wiring.xcf
  66. 18 5
      non_catalog_apps/ublox/scenes/ublox_scene_about.c
  67. 2 0
      non_catalog_apps/ublox/scenes/ublox_scene_config.h
  68. 82 108
      non_catalog_apps/ublox/scenes/ublox_scene_data_display.c
  69. 291 240
      non_catalog_apps/ublox/scenes/ublox_scene_data_display_config.c
  70. 85 0
      non_catalog_apps/ublox/scenes/ublox_scene_enter_file_name.c
  71. 53 35
      non_catalog_apps/ublox/scenes/ublox_scene_start.c
  72. 96 0
      non_catalog_apps/ublox/scenes/ublox_scene_sync_time.c
  73. 10 10
      non_catalog_apps/ublox/scenes/ublox_scene_wiring.c
  74. 115 70
      non_catalog_apps/ublox/ublox.c
  75. 104 109
      non_catalog_apps/ublox/ublox_device.c
  76. 80 65
      non_catalog_apps/ublox/ublox_device.h
  77. 41 24
      non_catalog_apps/ublox/ublox_i.h
  78. 593 413
      non_catalog_apps/ublox/ublox_worker.c
  79. 26 17
      non_catalog_apps/ublox/ublox_worker.h
  80. 9 7
      non_catalog_apps/ublox/ublox_worker_i.h
  81. 268 229
      non_catalog_apps/ublox/views/data_display_view.c
  82. 14 5
      non_catalog_apps/ublox/views/data_display_view.h

+ 3 - 1
ReadMe.md

@@ -20,7 +20,7 @@ Sources of "integrated/bundled" apps are added now in this repo too, to allow pu
 
 The Flipper and its community wouldn't be as rich as it is without your contributions and support. Thank you for all you have done.
 
-### Apps checked & updated at `29 Jul 05:07 GMT +3`
+### Apps checked & updated at `5 Aug 13:51 GMT +3`
 
 ## Games
 - [Pong (By nmrr)](https://github.com/nmrr/flipperzero-pong) - Modified by [SimplyMinimal](https://github.com/SimplyMinimal/FlipperZero-Pong)
@@ -102,6 +102,7 @@ The Flipper and its community wouldn't be as rich as it is without your contribu
 - [ESP32-C6 Gravity terminal (By chris-bc)](https://github.com/chris-bc/Flipper-Gravity)
 - [u-blox GPS (By liamhays)](https://github.com/liamhays/ublox)
 - [GPIO Controller (By Lokno)](https://github.com/Lokno/gpio_controller) -> `A visual tool to control the general purpose pins of the Flipper Zero`
+- [TAS playback (By rcombs)](https://github.com/rcombs/tas-playback) -> `This app plays back TAS files for retro video games. Connect the GPIO pins to the console's controller port and select a file to play back.`
 
 ## Tools / Misc / NFC / RFID / Infrared / etc..
 - [Calculator (By n-o-T-I-n-s-a-n-e)](https://github.com/n-o-T-I-n-s-a-n-e)
@@ -134,6 +135,7 @@ The Flipper and its community wouldn't be as rich as it is without your contribu
 - [Enhanced Sub-GHz Chat (By twisted-pear)](https://github.com/twisted-pear/esubghz_chat)
 - [TPMS Reader (By wosk)](https://github.com/wosk/flipperzero-tpms/tree/main)
 - [Multi Counter (By JadePossible)](https://github.com/JadePossible/Flipper-Multi-Counter)
+- [Chronometer (By nmrr)](https://github.com/nmrr/flipperzero-chronometer)
 
 --- 
 

BIN
apps/GPIO/gpio_controller.fap


BIN
apps/GPIO/tas_playback.fap


BIN
apps/GPIO/ublox.fap


BIN
apps/NFC/seader.fap


BIN
apps/Sub-GHz/esubghz_chat.fap


BIN
apps/Sub-GHz/tpms.fap


BIN
apps/Tools/flipper_chronometer.fap


+ 13 - 6
non_catalog_apps/esubghz_chat/README.md

@@ -5,8 +5,7 @@ feature that is available on the CLI. In addition it allows for basic
 encryption of messages.
 
 The plugin has been tested on the official firmware (version 0.87.0) and on
-Unleashed (version unlshd-059). Due to limitations of the official firmware,
-the behavior is slightly different there.
+Unleashed (version unlshd-059).
 
 Currently the use of an external antenna is not supported.
 
@@ -28,9 +27,8 @@ the key from another Flipper via NFC are supported.
 
 Finally the a message can be input. After the message is confirmed, the plugin
 will switch to the chat view, where sent and received messages are displayed.
-To view the chat view without entering a message, enter nothing (on Unleashed)
-or a single space (on OFW). To go back to entering a message press the back
-button.
+To view the chat view without entering a message, enter nothing. To go back to
+entering a message press the back button.
 
 In the chat view the keyboard can be locked by pressing and holding the OK
 button for a few seconds. To unlock the keyboard again quickly press the back
@@ -55,6 +53,14 @@ Messages are encrypted using 256 bit AES in GCM mode. Each message gets its own
 random IV. On reception the tag generated by GCM is verified and the message
 discarded if it doesn't match.
 
+A simple mechanism to prevent replay attacks is implemented. Each time the app
+enters an encrypted chat an ID is generated by SHA-256 hashing the Flipper's
+name with the current system ticks. Each message is prefixed with this ID and a
+counter. A receiving Flipper will check if the counter on a received message is
+higher than the last counter received with this ID. If it is not, the message
+is discarded and an error is displayed. ID and counter are included in the GCM
+tag's computation and are therefore authenticated with the used key.
+
 If a password is used, the key for the encryption is derived from the password
 by applying SHA-256 to the password once.
 
@@ -79,4 +85,5 @@ The implementations of AES and GCM are taken directly from
 https://github.com/mko-x/SharedAES-GCM. They were released to the public domain
 by Markus Kosmal.
 
-The app icon was made by [xMasterX](https://github.com/xMasterX).
+The app icon was made by [xMasterX](https://github.com/xMasterX). Other icons
+and graphics were taken from the Flipper Zero firmware.

BIN
non_catalog_apps/esubghz_chat/assets/Cry_dolph_55x52.png


BIN
non_catalog_apps/esubghz_chat/assets/DolphinNice_96x59.png


+ 105 - 19
non_catalog_apps/esubghz_chat/crypto_wrapper.c

@@ -1,4 +1,6 @@
 #include <furi_hal.h>
+#include <lib/mlib/m-dict.h>
+#include <toolbox/sha256.h>
 
 #ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
 #include "crypto/gcm.h"
@@ -6,13 +8,26 @@
 
 #include "crypto_wrapper.h"
 
+DICT_DEF2(ESubGhzChatReplayDict, uint64_t, uint32_t)
+
 struct ESugGhzChatCryptoCtx {
 	uint8_t key[KEY_BITS / 8];
 #ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
 	gcm_context gcm_ctx;
 #endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+	ESubGhzChatReplayDict_t replay_dict;
+	uint64_t run_id;
+	uint32_t counter;
 };
 
+struct ESubGhzChatCryptoMsg {
+	uint64_t run_id;
+	uint32_t counter;
+	uint8_t iv[IV_BYTES];
+	uint8_t tag[TAG_BYTES];
+	uint8_t data[0];
+} __attribute__ ((packed));
+
 void crypto_init(void)
 {
 #ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
@@ -33,6 +48,9 @@ ESubGhzChatCryptoCtx *crypto_ctx_alloc(void)
 
 	if (ret != NULL) {
 		memset(ret, 0, sizeof(ESubGhzChatCryptoCtx));
+		ESubGhzChatReplayDict_init(ret->replay_dict);
+		ret->run_id = 0;
+		ret->counter = 1;
 	}
 
 	return ret;
@@ -41,16 +59,45 @@ ESubGhzChatCryptoCtx *crypto_ctx_alloc(void)
 void crypto_ctx_free(ESubGhzChatCryptoCtx *ctx)
 {
 	crypto_ctx_clear(ctx);
+	ESubGhzChatReplayDict_clear(ctx->replay_dict);
 	free(ctx);
 }
 
 void crypto_ctx_clear(ESubGhzChatCryptoCtx *ctx)
 {
-	crypto_explicit_bzero(ctx, sizeof(ESubGhzChatCryptoCtx));
+	crypto_explicit_bzero(ctx->key, sizeof(ctx->key));
+#ifndef FURI_HAL_CRYPTO_ADVANCED_AVAIL
+	crypto_explicit_bzero(&(ctx->gcm_ctx), sizeof(ctx->gcm_ctx));
+#endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+	ESubGhzChatReplayDict_reset(ctx->replay_dict);
+	ctx->run_id = 0;
+	ctx->counter = 1;
 }
 
-bool crypto_ctx_set_key(ESubGhzChatCryptoCtx *ctx, const uint8_t *key)
+static uint64_t crypto_calc_run_id(FuriString *flipper_name, uint32_t tick)
 {
+	const char *fn = furi_string_get_cstr(flipper_name);
+	size_t fn_len = strlen(fn);
+
+	uint8_t h_in[fn_len + sizeof(uint32_t)];
+	memcpy(h_in, fn, fn_len);
+	memcpy(h_in + fn_len, &tick, sizeof(uint32_t));
+
+	uint8_t h_out[256];
+	sha256(h_in, fn_len + sizeof(uint32_t), h_out);
+
+	uint64_t run_id;
+	memcpy(&run_id, h_out, sizeof(uint64_t));
+
+	return run_id;
+}
+
+bool crypto_ctx_set_key(ESubGhzChatCryptoCtx *ctx, const uint8_t *key,
+		FuriString *flipper_name, uint32_t tick)
+{
+	ctx->run_id = crypto_calc_run_id(flipper_name, tick);
+	ctx->counter = 1;
+
 	memcpy(ctx->key, key, KEY_BITS / 8);
 #ifdef FURI_HAL_CRYPTO_ADVANCED_AVAIL
 	return true;
@@ -71,35 +118,74 @@ bool crypto_ctx_decrypt(ESubGhzChatCryptoCtx *ctx, uint8_t *in, size_t in_len,
 		return false;
 	}
 
+	struct ESubGhzChatCryptoMsg *msg = (struct ESubGhzChatCryptoMsg *) in;
+
+	// check if message is stale, if yes, discard
+	uint32_t *counter = ESubGhzChatReplayDict_get(ctx->replay_dict,
+			msg->run_id);
+	if (counter != NULL) {
+		if (*counter >= __ntohl(msg->counter)) {
+			return false;
+		}
+	}
+
+	// decrypt and auth message
 #ifdef FURI_HAL_CRYPTO_ADVANCED_AVAIL
-	return (furi_hal_crypto_gcm_decrypt_and_verify(ctx->key,
-			in, in + IV_BYTES, out,
+	bool ret = (furi_hal_crypto_gcm_decrypt_and_verify(ctx->key,
+			msg->iv,
+			(uint8_t *) msg, RUN_ID_BYTES + COUNTER_BYTES,
+			msg->data, out,
 			in_len - MSG_OVERHEAD,
-			in + in_len - TAG_BYTES) == FuriHalCryptoGCMStateOk);
+			msg->tag) == FuriHalCryptoGCMStateOk);
 #else /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
-	return (gcm_auth_decrypt(&(ctx->gcm_ctx),
-			in, IV_BYTES,
-			NULL, 0,
-			in + IV_BYTES, out, in_len - MSG_OVERHEAD,
-			in + in_len - TAG_BYTES, TAG_BYTES) == 0);
+	bool ret = (gcm_auth_decrypt(&(ctx->gcm_ctx),
+			msg->iv, IV_BYTES,
+			(uint8_t *) msg, RUN_ID_BYTES + COUNTER_BYTES,
+			msg->data, out,
+			in_len - MSG_OVERHEAD,
+			msg->tag, TAG_BYTES) == 0);
 #endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+
+	// if auth was successful update replay dict
+	if (ret) {
+		ESubGhzChatReplayDict_set_at(ctx->replay_dict, msg->run_id,
+				__ntohl(msg->counter));
+	}
+
+	return ret;
 }
 
 bool crypto_ctx_encrypt(ESubGhzChatCryptoCtx *ctx, uint8_t *in, size_t in_len,
 		uint8_t *out)
 {
-	furi_hal_random_fill_buf(out, IV_BYTES);
+	struct ESubGhzChatCryptoMsg *msg = (struct ESubGhzChatCryptoMsg *) out;
 
+	// fill message header
+	msg->run_id = ctx->run_id;
+	msg->counter = __htonl(ctx->counter);
+	furi_hal_random_fill_buf(msg->iv, IV_BYTES);
+
+	// encrypt message and store tag in header
 #ifdef FURI_HAL_CRYPTO_ADVANCED_AVAIL
-	return (furi_hal_crypto_gcm_encrypt_and_tag(ctx->key,
-			out, in, out + IV_BYTES,
+	bool ret = (furi_hal_crypto_gcm_encrypt_and_tag(ctx->key,
+			msg->iv,
+			(uint8_t *) msg, RUN_ID_BYTES + COUNTER_BYTES,
+			in, msg->data,
 			in_len,
-			out + IV_BYTES + in_len) == FuriHalCryptoGCMStateOk);
+			msg->tag) == FuriHalCryptoGCMStateOk);
 #else /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
-	return (gcm_crypt_and_tag(&(ctx->gcm_ctx), ENCRYPT,
-			out, IV_BYTES,
-			NULL, 0,
-			in, out + IV_BYTES, in_len,
-			out + IV_BYTES + in_len, TAG_BYTES) == 0);
+	bool ret = (gcm_crypt_and_tag(&(ctx->gcm_ctx), ENCRYPT,
+			msg->iv, IV_BYTES,
+			(uint8_t *) msg, RUN_ID_BYTES + COUNTER_BYTES,
+			in, msg->data,
+			in_len,
+			msg->tag, TAG_BYTES) == 0);
 #endif /* FURI_HAL_CRYPTO_ADVANCED_AVAIL */
+
+	// increase internal counter
+	if (ret) {
+		ctx->counter++;
+	}
+
+	return ret;
 }

+ 5 - 2
non_catalog_apps/esubghz_chat/crypto_wrapper.h

@@ -4,11 +4,13 @@
 extern "C" {
 #endif
 
+#define RUN_ID_BYTES (sizeof(uint64_t))
+#define COUNTER_BYTES (sizeof(uint32_t))
 #define KEY_BITS 256
 #define IV_BYTES 12
 #define TAG_BYTES 16
 
-#define MSG_OVERHEAD (IV_BYTES + TAG_BYTES)
+#define MSG_OVERHEAD (RUN_ID_BYTES + COUNTER_BYTES + IV_BYTES + TAG_BYTES)
 
 typedef struct ESugGhzChatCryptoCtx ESubGhzChatCryptoCtx;
 
@@ -22,7 +24,8 @@ void crypto_ctx_free(ESubGhzChatCryptoCtx *ctx);
 
 void crypto_ctx_clear(ESubGhzChatCryptoCtx *ctx);
 
-bool crypto_ctx_set_key(ESubGhzChatCryptoCtx *ctx, const uint8_t *key);
+bool crypto_ctx_set_key(ESubGhzChatCryptoCtx *ctx, const uint8_t *key,
+		FuriString *flipper_name, uint32_t tick);
 void crypto_ctx_get_key(ESubGhzChatCryptoCtx *ctx, uint8_t *key);
 
 bool crypto_ctx_decrypt(ESubGhzChatCryptoCtx *ctx, uint8_t *in, size_t in_len,

+ 33 - 10
non_catalog_apps/esubghz_chat/esubghz_chat.c

@@ -23,6 +23,36 @@ static void have_read_cb(void* context)
 	state->last_time_rx_data = furi_get_tick();
 }
 
+/* Sets the header for the chat input field depending on whether or not a
+ * message preview exists. */
+void set_chat_input_header(ESubGhzChatState *state)
+{
+	if (strlen(state->msg_preview) == 0) {
+		text_input_set_header_text(state->text_input, "Message");
+	} else {
+		text_input_set_header_text(state->text_input,
+				state->msg_preview);
+	}
+}
+
+/* Appends the latest message to the chat box and prepares the message preview.
+ */
+void append_msg(ESubGhzChatState *state, const char *msg)
+{
+	/* append message to text box */
+	furi_string_cat_printf(state->chat_box_store, "\n%s", msg);
+
+	/* prepare message preview */
+	strncpy(state->msg_preview, msg, MSG_PREVIEW_SIZE);
+	state->msg_preview[MSG_PREVIEW_SIZE] = 0;
+	set_chat_input_header(state);
+
+	/* reset text box contents and focus */
+	text_box_set_text(state->chat_box,
+			furi_string_get_cstr(state->chat_box_store));
+	text_box_set_focus(state->chat_box, TextBoxFocusEnd);
+}
+
 /* Decrypts a message for post_rx(). */
 static bool post_rx_decrypt(ESubGhzChatState *state, size_t rx_size)
 {
@@ -39,8 +69,7 @@ static bool post_rx_decrypt(ESubGhzChatState *state, size_t rx_size)
 	return ret;
 }
 
-/* Post RX handler, decrypts received messages, displays them in the text box
- * and sends a notification. */
+/* Post RX handler, decrypts received messages and calls append_msg(). */
 static void post_rx(ESubGhzChatState *state, size_t rx_size)
 {
 	furi_assert(state);
@@ -68,17 +97,11 @@ static void post_rx(ESubGhzChatState *state, size_t rx_size)
 		}
 	}
 
-	/* append message to text box */
-	furi_string_cat_printf(state->chat_box_store, "\n%s",
-			state->rx_str_buffer);
+	/* append message to text box and prepare message preview */
+	append_msg(state, state->rx_str_buffer);
 
 	/* send notification (make the flipper vibrate) */
 	notification_message(state->notification, &sequence_single_vibro);
-
-	/* reset text box contents and focus */
-	text_box_set_text(state->chat_box,
-			furi_string_get_cstr(state->chat_box_store));
-	text_box_set_focus(state->chat_box, TextBoxFocusEnd);
 }
 
 /* Reads the message from msg_input, encrypts it if necessary and then

+ 10 - 0
non_catalog_apps/esubghz_chat/esubghz_chat_i.h

@@ -24,10 +24,13 @@
 
 #define DEFAULT_FREQ 433920000
 
+#define KEY_READ_POPUP_MS 3000
+
 #define RX_TX_BUFFER_SIZE 1024
 
 #define CHAT_BOX_STORE_SIZE 4096
 #define TEXT_INPUT_STORE_SIZE 256
+#define MSG_PREVIEW_SIZE 32
 
 #define KEY_HEX_STR_SIZE ((KEY_BITS / 8) * 3)
 
@@ -61,6 +64,9 @@ typedef struct {
 	FuriString *name_prefix;
 	FuriString *msg_input;
 
+	// message preview
+	char msg_preview[MSG_PREVIEW_SIZE + 1];
+
 	// encryption
 	bool encrypted;
 	ESubGhzChatCryptoCtx *crypto_ctx;
@@ -91,6 +97,8 @@ typedef enum {
 	ESubGhzChatEvent_KeyMenuHexKey,
 	ESubGhzChatEvent_KeyMenuGenKey,
 	ESubGhzChatEvent_KeyMenuReadKeyFromNfc,
+	ESubGhzChatEvent_KeyReadPopupFailed,
+	ESubGhzChatEvent_KeyReadPopupSucceeded,
 	ESubGhzChatEvent_PassEntered,
 	ESubGhzChatEvent_HexKeyEntered,
 	ESubGhzChatEvent_MsgEntered,
@@ -109,5 +117,7 @@ typedef enum {
 	ESubGhzChatView_NfcPopup,
 } ESubGhzChatView;
 
+void set_chat_input_header(ESubGhzChatState *state);
+void append_msg(ESubGhzChatState *state, const char *msg);
 void tx_msg_input(ESubGhzChatState *state);
 void enter_chat(ESubGhzChatState *state);

+ 20 - 25
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_chat_input.c

@@ -6,30 +6,28 @@
  * then copied into the TX buffer. The contents of the TX buffer are then
  * transmitted. The sent message is appended to the text box and a MsgEntered
  * event is sent to the scene manager to switch to the text box view. */
-static void chat_input_cb(void *context)
+static bool chat_input_validator(const char *text, FuriString *error,
+		void *context)
 {
+	UNUSED(error);
+
 	furi_assert(context);
 	ESubGhzChatState* state = context;
 
 	/* no message, just switch to the text box view */
-#ifdef FW_ORIGIN_Official
-	if (strcmp(state->text_input_store, " ") == 0) {
-#else /* FW_ORIGIN_Official */
-	if (strlen(state->text_input_store) == 0) {
-#endif /* FW_ORIGIN_Official */
-		scene_manager_handle_custom_event(state->scene_manager,
+	if (strlen(text) == 0) {
+		view_dispatcher_send_custom_event(state->view_dispatcher,
 				ESubGhzChatEvent_MsgEntered);
-		return;
+		return true;
 	}
 
 	/* concatenate the name prefix and the actual message */
 	furi_string_set(state->msg_input, state->name_prefix);
 	furi_string_cat_str(state->msg_input, ": ");
-	furi_string_cat_str(state->msg_input, state->text_input_store);
+	furi_string_cat_str(state->msg_input, text);
 
-	/* append the message to the chat box */
-	furi_string_cat_printf(state->chat_box_store, "\n%s",
-		furi_string_get_cstr(state->msg_input));
+	/* append the message to the chat box and prepare message preview */
+	append_msg(state, furi_string_get_cstr(state->msg_input));
 
 	/* encrypt and transmit message */
 	tx_msg_input(state);
@@ -38,8 +36,10 @@ static void chat_input_cb(void *context)
 	furi_string_set_char(state->msg_input, 0, 0);
 
 	/* switch to text box view */
-	scene_manager_handle_custom_event(state->scene_manager,
+	view_dispatcher_send_custom_event(state->view_dispatcher,
 			ESubGhzChatEvent_MsgEntered);
+
+	return true;
 }
 
 /* Prepares the message input scene. */
@@ -52,25 +52,20 @@ void scene_on_enter_chat_input(void* context)
 
 	state->text_input_store[0] = 0;
 	text_input_reset(state->text_input);
+	/* use validator for scene change to get around minimum length
+	 * requirement */
 	text_input_set_result_callback(
 			state->text_input,
-			chat_input_cb,
-			state,
+			NULL,
+			NULL,
 			state->text_input_store,
 			sizeof(state->text_input_store),
 			true);
 	text_input_set_validator(
 			state->text_input,
-			NULL,
-			NULL);
-	text_input_set_header_text(
-			state->text_input,
-#ifdef FW_ORIGIN_Official
-			"Message (space for none)");
-#else /* FW_ORIGIN_Official */
-			"Message");
-	text_input_set_minimum_length(state->text_input, 0);
-#endif /* FW_ORIGIN_Official */
+			chat_input_validator,
+			state);
+	set_chat_input_header(state);
 
 	view_dispatcher_switch_to_view(state->view_dispatcher, ESubGhzChatView_Input);
 }

+ 1 - 1
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_freq_input.c

@@ -10,7 +10,7 @@ static void freq_input_cb(void *context)
 	furi_string_cat_printf(state->chat_box_store, "Frequency: %lu",
 			state->frequency);
 
-	scene_manager_handle_custom_event(state->scene_manager,
+	view_dispatcher_send_custom_event(state->view_dispatcher,
 			ESubGhzChatEvent_FreqEntered);
 }
 

+ 3 - 2
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_hex_key_input.c

@@ -9,7 +9,8 @@ static void hex_key_input_cb(void* context)
 
 	/* initiate the crypto context */
 	bool ret = crypto_ctx_set_key(state->crypto_ctx,
-			state->hex_key_input_store);
+			state->hex_key_input_store, state->name_prefix,
+			furi_get_tick());
 
 	/* cleanup */
 	crypto_explicit_bzero(state->hex_key_input_store,
@@ -24,7 +25,7 @@ static void hex_key_input_cb(void* context)
 
 	enter_chat(state);
 
-	scene_manager_handle_custom_event(state->scene_manager,
+	view_dispatcher_send_custom_event(state->view_dispatcher,
 			ESubGhzChatEvent_HexKeyEntered);
 }
 

+ 2 - 2
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_key_display.c

@@ -7,13 +7,13 @@ void key_display_result_cb(DialogExResult result, void* context)
 
 	switch(result) {
 	case DialogExResultLeft:
-		scene_manager_handle_custom_event(state->scene_manager,
+		view_dispatcher_send_custom_event(state->view_dispatcher,
 				ESubGhzChatEvent_KeyDisplayBack);
 		break;
 
 	case DialogExResultCenter:
 		if (state->encrypted) {
-			scene_manager_handle_custom_event(state->scene_manager,
+			view_dispatcher_send_custom_event(state->view_dispatcher,
 					ESubGhzChatEvent_KeyDisplayShare);
 		}
 		break;

+ 7 - 6
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_key_menu.c

@@ -20,17 +20,17 @@ static void key_menu_cb(void* context, uint32_t index)
 		state->encrypted = false;
 		enter_chat(state);
 
-		scene_manager_handle_custom_event(state->scene_manager,
+		view_dispatcher_send_custom_event(state->view_dispatcher,
 				ESubGhzChatEvent_KeyMenuNoEncryption);
 		break;
 
 	case ESubGhzChatKeyMenuItems_Password:
-		scene_manager_handle_custom_event(state->scene_manager,
+		view_dispatcher_send_custom_event(state->view_dispatcher,
 				ESubGhzChatEvent_KeyMenuPassword);
 		break;
 
 	case ESubGhzChatKeyMenuItems_HexKey:
-		scene_manager_handle_custom_event(state->scene_manager,
+		view_dispatcher_send_custom_event(state->view_dispatcher,
 				ESubGhzChatEvent_KeyMenuHexKey);
 		break;
 
@@ -39,7 +39,8 @@ static void key_menu_cb(void* context, uint32_t index)
 		furi_hal_random_fill_buf(key, KEY_BITS / 8);
 
 		/* initiate the crypto context */
-		bool ret = crypto_ctx_set_key(state->crypto_ctx, key);
+		bool ret = crypto_ctx_set_key(state->crypto_ctx, key,
+				state->name_prefix, furi_get_tick());
 
 		/* cleanup */
 		crypto_explicit_bzero(key, sizeof(key));
@@ -53,12 +54,12 @@ static void key_menu_cb(void* context, uint32_t index)
 		state->encrypted = true;
 		enter_chat(state);
 
-		scene_manager_handle_custom_event(state->scene_manager,
+		view_dispatcher_send_custom_event(state->view_dispatcher,
 				ESubGhzChatEvent_KeyMenuGenKey);
 		break;
 
 	case ESubGhzChatKeyMenuItems_ReadKeyFromNfc:
-		scene_manager_handle_custom_event(state->scene_manager,
+		view_dispatcher_send_custom_event(state->view_dispatcher,
 				ESubGhzChatEvent_KeyMenuReadKeyFromNfc);
 		break;
 

+ 92 - 22
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_key_read_popup.c

@@ -4,6 +4,8 @@ typedef enum {
 	KeyReadPopupState_Idle,
 	KeyReadPopupState_Detecting,
 	KeyReadPopupState_Reading,
+	KeyReadPopupState_Fail,
+	KeyReadPopupState_Success,
 } KeyReadPopupState;
 
 static bool read_worker_cb(NfcWorkerEvent event, void* context)
@@ -16,6 +18,26 @@ static bool read_worker_cb(NfcWorkerEvent event, void* context)
 	return true;
 }
 
+static void key_read_popup_timeout_cb(void* context)
+{
+	furi_assert(context);
+	ESubGhzChatState* state = context;
+
+	uint32_t cur_state = scene_manager_get_scene_state(
+			state->scene_manager, ESubGhzChatScene_KeyReadPopup);
+
+	/* done displaying our failure */
+	if (cur_state == KeyReadPopupState_Fail) {
+		view_dispatcher_send_custom_event(state->view_dispatcher,
+				ESubGhzChatEvent_KeyReadPopupFailed);
+	/* done displaying our success, enter chat */
+	} else if (cur_state == KeyReadPopupState_Success) {
+		enter_chat(state);
+		view_dispatcher_send_custom_event(state->view_dispatcher,
+				ESubGhzChatEvent_KeyReadPopupSucceeded);
+	}
+}
+
 static bool key_read_popup_handle_key_read(ESubGhzChatState *state)
 {
 	NfcDeviceData *dev_data = state->nfc_dev_data;
@@ -26,7 +48,8 @@ static bool key_read_popup_handle_key_read(ESubGhzChatState *state)
 
 	/* initiate the crypto context */
 	bool ret = crypto_ctx_set_key(state->crypto_ctx,
-			dev_data->mf_ul_data.data);
+			dev_data->mf_ul_data.data, state->name_prefix,
+			furi_get_tick());
 
 	/* cleanup */
 	crypto_explicit_bzero(dev_data->mf_ul_data.data, KEY_BITS / 8);
@@ -36,9 +59,8 @@ static bool key_read_popup_handle_key_read(ESubGhzChatState *state)
 		return false;
 	}
 
-	/* set encrypted flag and enter the chat */
+	/* set encrypted flag */
 	state->encrypted = true;
-	enter_chat(state);
 
 	return true;
 }
@@ -54,18 +76,62 @@ static void key_read_popup_set_state(ESubGhzChatState *state, KeyReadPopupState
 
 	if (new_state == KeyReadPopupState_Detecting) {
 		popup_reset(state->nfc_popup);
-		popup_set_text(state->nfc_popup, "Apply card to\nFlipper's "
-				"back", 97, 24, AlignCenter, AlignTop);
+		popup_disable_timeout(state->nfc_popup);
+		popup_set_text(state->nfc_popup, "Tap Flipper\n to sender", 97,
+				24, AlignCenter, AlignTop);
 		popup_set_icon(state->nfc_popup, 0, 8, &I_NFC_manual_60x50);
+		notification_message(state->notification,
+				&sequence_blink_start_cyan);
 	} else if (new_state == KeyReadPopupState_Reading) {
 		popup_reset(state->nfc_popup);
-		popup_set_header(state->nfc_popup, "Reading card\nDon't "
+		popup_disable_timeout(state->nfc_popup);
+		popup_set_header(state->nfc_popup, "Reading key\nDon't "
 				"move...", 85, 24, AlignCenter, AlignTop);
 		popup_set_icon(state->nfc_popup, 12, 23, &I_Loading_24);
+		notification_message(state->notification,
+				&sequence_blink_start_yellow);
+	} else if (new_state == KeyReadPopupState_Fail) {
+		nfc_worker_stop(state->nfc_worker);
+
+		popup_reset(state->nfc_popup);
+		popup_set_header(state->nfc_popup, "Failure!", 64, 2,
+				AlignCenter, AlignTop);
+		popup_set_text(state->nfc_popup, "Failed\nto read\nkey.", 78,
+				16, AlignLeft, AlignTop);
+		popup_set_icon(state->nfc_popup, 21, 13, &I_Cry_dolph_55x52);
+
+		popup_set_timeout(state->nfc_popup, KEY_READ_POPUP_MS);
+		popup_set_context(state->nfc_popup, state);
+		popup_set_callback(state->nfc_popup,
+				key_read_popup_timeout_cb);
+		popup_enable_timeout(state->nfc_popup);
+
+		notification_message(state->notification,
+				&sequence_blink_stop);
+	} else if (new_state == KeyReadPopupState_Success) {
+		nfc_worker_stop(state->nfc_worker);
+
+		popup_reset(state->nfc_popup);
+		popup_set_header(state->nfc_popup, "Key\nread!", 13, 22,
+				AlignLeft, AlignBottom);
+		popup_set_icon(state->nfc_popup, 32, 5, &I_DolphinNice_96x59);
+
+		popup_set_timeout(state->nfc_popup, KEY_READ_POPUP_MS);
+		popup_set_context(state->nfc_popup, state);
+		popup_set_callback(state->nfc_popup,
+				key_read_popup_timeout_cb);
+		popup_enable_timeout(state->nfc_popup);
+
+		notification_message(state->notification, &sequence_success);
+		notification_message(state->notification,
+				&sequence_blink_stop);
 	}
 
 	scene_manager_set_scene_state(state->scene_manager,
 			ESubGhzChatScene_KeyReadPopup, new_state);
+
+	view_dispatcher_switch_to_view(state->view_dispatcher,
+			ESubGhzChatView_NfcPopup);
 }
 
 /* Prepares the key share read scene. */
@@ -86,11 +152,6 @@ void scene_on_enter_key_read_popup(void* context)
 
 	nfc_worker_start(state->nfc_worker, NfcWorkerStateRead,
 			state->nfc_dev_data, read_worker_cb, state);
-
-	notification_message(state->notification, &sequence_blink_start_cyan);
-
-	view_dispatcher_switch_to_view(state->view_dispatcher,
-			ESubGhzChatView_NfcPopup);
 }
 
 /* Handles scene manager events for the key read popup scene. */
@@ -110,8 +171,6 @@ bool scene_on_event_key_read_popup(void* context, SceneManagerEvent event)
 		case NfcWorkerEventCardDetected:
 			key_read_popup_set_state(state,
 					KeyReadPopupState_Reading);
-			notification_message(state->notification,
-					&sequence_blink_start_yellow);
 			consumed = true;
 			break;
 
@@ -119,32 +178,43 @@ bool scene_on_event_key_read_popup(void* context, SceneManagerEvent event)
 		case NfcWorkerEventNoCardDetected:
 			key_read_popup_set_state(state,
 					KeyReadPopupState_Detecting);
-			notification_message(state->notification,
-					&sequence_blink_start_cyan);
 			consumed = true;
 			break;
 
 		/* key probably read */
 		case NfcWorkerEventReadMfUltralight:
 			if (key_read_popup_handle_key_read(state)) {
-				scene_manager_next_scene(state->scene_manager,
-						ESubGhzChatScene_ChatInput);
+				key_read_popup_set_state(state,
+						KeyReadPopupState_Success);
 			} else {
-				if (!scene_manager_previous_scene(
-							state->scene_manager)) {
-					view_dispatcher_stop(state->view_dispatcher);
-				}
+				key_read_popup_set_state(state,
+						KeyReadPopupState_Fail);
 			}
 			consumed = true;
 			break;
 
-		default:
+		/* close the popup and go back */
+		case ESubGhzChatEvent_KeyReadPopupFailed:
 			if (!scene_manager_previous_scene(
 						state->scene_manager)) {
 				view_dispatcher_stop(state->view_dispatcher);
 			}
 			consumed = true;
 			break;
+
+		/* success, go to chat input */
+		case ESubGhzChatEvent_KeyReadPopupSucceeded:
+			scene_manager_next_scene(state->scene_manager,
+					ESubGhzChatScene_ChatInput);
+			consumed = true;
+			break;
+
+		/* something else happend, treat as failure */
+		default:
+			key_read_popup_set_state(state,
+					KeyReadPopupState_Fail);
+			consumed = true;
+			break;
 		}
 
 		break;

+ 3 - 2
non_catalog_apps/esubghz_chat/scenes/esubghz_chat_pass_input.c

@@ -11,7 +11,7 @@ static void pass_input_cb(void *context)
 
 	enter_chat(state);
 
-	scene_manager_handle_custom_event(state->scene_manager,
+	view_dispatcher_send_custom_event(state->view_dispatcher,
 			ESubGhzChatEvent_PassEntered);
 }
 
@@ -38,7 +38,8 @@ static bool pass_input_validator(const char *text, FuriString *error,
 	sha256((unsigned char *) text, strlen(text), key);
 
 	/* initiate the crypto context */
-	bool ret = crypto_ctx_set_key(state->crypto_ctx, key);
+	bool ret = crypto_ctx_set_key(state->crypto_ctx, key,
+			state->name_prefix, furi_get_tick());
 
 	/* cleanup */
 	crypto_explicit_bzero(key, sizeof(key));

+ 40 - 0
non_catalog_apps/flipper_chronometer/README.md

@@ -0,0 +1,40 @@
+# flipperzero-chronometer
+⏱️⏱️ A chronometer application for the Flipper Zero ⏱️⏱️
+
+This chronometer is accurate to the millisecond. **TIM2** internal timer of the **STM32** MCU is used to generate a 64 MHz clock signal. This signal is used to count elapsed time.
+
+## Gallery
+
+<img src="https://github.com/nmrr/flipperzero-chronometer/blob/main/img/chrono2.png" width=25% height=25%> <img src="https://github.com/nmrr/flipperzero-chronometer/blob/main/img/chrono3.png" width=25% height=25%>
+
+**Note:** Chronometer stops after one hour
+
+## Build the program
+
+Assuming the toolchain is already installed, copy **flipper_chronometer** directory to **applications_user**
+
+Plug your **Flipper Zero** and build the chronometer:
+```
+./fbt launch_app APPSRC=applications_user/flipper_chronometer
+```
+
+The program will automatically be launched after compilation
+
+<img src="https://github.com/nmrr/flipperzero-chronometer/blob/main/img/chrono1.png" width=25% height=25%>
+
+**Button assignments**: 
+
+button  | function
+------------- | -------------
+**Ok** *[short press]*  | Start/stop the chronometer
+**Ok** *[long press]*  | Reset the chronometer
+**Back** *[long press]*  | Exit
+
+If you don't want to build this application, just simply copy **flipper_chronometer.fap** on your **Flipper Zero** 
+
+Build has been made with official toolchain, **API Mismatch** error may appear if you are using custom firmware. You can bypass this error but the program may crash.
+
+## Changelog
+
+* 2023-08-02
+  * Initial release

+ 13 - 0
non_catalog_apps/flipper_chronometer/application.fam

@@ -0,0 +1,13 @@
+App(
+    appid="flipper_chronometer",
+    name="Chronometer",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="flipper_chronometer_app",
+    cdefines=["APP_CHRONOMETER"],
+    requires=[
+        "gui",
+    ],
+    stack_size=1 * 1024,
+    fap_icon="chronometer.png",
+    fap_category="Tools",
+)

BIN
non_catalog_apps/flipper_chronometer/chronometer.png


+ 184 - 0
non_catalog_apps/flipper_chronometer/flipper_chronometer.c

@@ -0,0 +1,184 @@
+// CC0 1.0 Universal (CC0 1.0)
+// Public Domain Dedication
+// https://github.com/nmrr
+
+#include <stdio.h>
+#include <furi.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <notification/notification_messages.h>
+#include <furi_hal_pwm.h>
+#include <furi_hal_power.h>
+#include <locale/locale.h>
+#include <lib/toolbox/md5.h>
+
+#define SCREEN_SIZE_X 128
+#define SCREEN_SIZE_Y 64
+
+typedef enum {
+    EventTypeInput,
+    ClockEventTypeTick,
+    EventGPIO,
+} EventType;
+
+typedef struct {
+    EventType type;
+    InputEvent input;
+} EventApp;
+
+#define lineArraySize 128
+
+typedef struct {
+    FuriMutex* mutex;
+    uint32_t timer;
+    uint8_t minute;
+} mutexStruct;
+
+static void draw_callback(Canvas* canvas, void* ctx) 
+{
+    mutexStruct* mutexVal = ctx;
+    mutexStruct mutexDraw;
+    furi_mutex_acquire(mutexVal->mutex, FuriWaitForever);
+    memcpy(&mutexDraw, mutexVal, sizeof(mutexStruct));
+    furi_mutex_release(mutexVal->mutex);
+
+    char buffer[16];
+    snprintf(buffer, sizeof(buffer), "%02u:%02lu:%03lu", mutexDraw.minute, mutexDraw.timer / 64000000, (mutexDraw.timer % 64000000) / 64000);
+    canvas_set_font(canvas, FontBigNumbers);
+    canvas_draw_str_aligned(canvas, SCREEN_SIZE_X/2, SCREEN_SIZE_Y/2 + 5, AlignCenter, AlignBottom, buffer);
+}
+
+static void input_callback(InputEvent* input_event, void* ctx) 
+{
+    furi_assert(ctx);
+    FuriMessageQueue* event_queue = ctx;
+    EventApp event = {.type = EventTypeInput, .input = *input_event};
+    furi_message_queue_put(event_queue, &event, FuriWaitForever);
+}
+
+static void clock_tick(void* ctx) {
+    furi_assert(ctx);
+
+    FuriMessageQueue* queue = ctx;
+    EventApp event = {.type = ClockEventTypeTick};
+    furi_message_queue_put(queue, &event, 0);
+}
+
+int32_t flipper_chronometer_app() 
+{
+    // 64 MHz
+    furi_hal_bus_enable(FuriHalBusTIM2);
+    LL_TIM_SetCounterMode(TIM2, LL_TIM_COUNTERMODE_UP);
+    LL_TIM_SetClockDivision(TIM2, LL_TIM_CLOCKDIVISION_DIV1);
+    LL_TIM_SetPrescaler(TIM2, 0);
+    // return to 0 after 1 min
+    LL_TIM_SetAutoReload(TIM2, 3839999999);
+    LL_TIM_SetCounter(TIM2, 0);
+
+    EventApp event;
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(EventApp));
+
+    mutexStruct mutexVal;
+    mutexVal.minute = 0;
+    mutexVal.timer = 0;
+
+    uint32_t previousTimer = 0;
+
+    mutexVal.mutex= furi_mutex_alloc(FuriMutexTypeNormal);
+    if(!mutexVal.mutex) {
+        furi_message_queue_free(event_queue);
+        return 255;
+    }
+
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, draw_callback, &mutexVal.mutex);
+    view_port_input_callback_set(view_port, input_callback, event_queue);
+
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    FuriTimer* timer = furi_timer_alloc(clock_tick, FuriTimerTypePeriodic, event_queue);
+
+    uint8_t enableChrono = 0;
+
+    while(1) 
+    {
+        FuriStatus event_status = furi_message_queue_get(event_queue, &event, FuriWaitForever);
+        
+        uint8_t screenRefresh = 0;
+
+        if (event_status == FuriStatusOk)
+        {   
+            if(event.type == EventTypeInput) 
+            {
+                if(event.input.key == InputKeyBack && event.input.type == InputTypeLong) 
+                {
+                    break;
+                }
+                else if (event.input.key == InputKeyOk && event.input.type == InputTypeShort)
+                {
+                    if (enableChrono == 1)
+                    {
+                        LL_TIM_DisableCounter(TIM2);
+                        furi_timer_stop(timer);
+                        enableChrono = 0;
+                    }
+                    else
+                    {
+                        LL_TIM_EnableCounter(TIM2);
+                        furi_timer_start(timer, 43); // better to use prime number as timer for millisecond refresh effect
+                        enableChrono = 1;
+                    }
+
+                    screenRefresh = 1;
+                }
+                else if (enableChrono == 0 && event.input.key == InputKeyOk && event.input.type == InputTypeLong)
+                {
+                    LL_TIM_SetCounter(TIM2, 0);
+                    furi_mutex_acquire(mutexVal.mutex, FuriWaitForever);
+                    mutexVal.minute = 0;
+                    mutexVal.timer = 0;
+                    furi_mutex_release(mutexVal.mutex);
+
+                    screenRefresh = 1;
+                }
+            }
+            else if (event.type == ClockEventTypeTick)
+            {
+                screenRefresh = 1;
+            }
+        }
+
+        if (screenRefresh == 1)
+        {
+            furi_mutex_acquire(mutexVal.mutex, FuriWaitForever);
+            previousTimer = mutexVal.timer;
+            mutexVal.timer = TIM2->CNT;
+            if (mutexVal.timer < previousTimer)
+            {
+                if (mutexVal.minute < 59) mutexVal.minute++;
+                else 
+                {
+                    LL_TIM_DisableCounter(TIM2);
+                    mutexVal.timer = 3839999999;
+                    furi_timer_stop(timer);
+                    enableChrono = 0;
+                }
+            }
+            furi_mutex_release(mutexVal.mutex);
+
+            view_port_update(view_port);
+        }
+    }
+
+    LL_TIM_DisableCounter(TIM2);
+    furi_hal_bus_disable(FuriHalBusTIM2);
+    furi_message_queue_free(event_queue);
+    furi_mutex_free(mutexVal.mutex);
+    gui_remove_view_port(gui, view_port);
+    view_port_free(view_port);
+    furi_timer_free(timer);
+    furi_record_close(RECORD_GUI);
+
+    return 0;
+}

+ 143 - 0
non_catalog_apps/gpio_controller/app_defines.h

@@ -0,0 +1,143 @@
+#ifndef APP_DEFINES_H
+#define APP_DEFINES_H
+
+#define GPIO_PIN_COUNT 8
+#define ANIMATE_FRAME_TIME_MS 133
+#define FRAME_TIME 66.666666 
+
+typedef void (*DrawView)(Canvas* canvas, void* ctx);
+typedef void (*HandleInput)(InputEvent* event, void* ctx);
+
+typedef enum {
+    MAIN_VIEW,
+    CONFIG_MENU_VIEW
+}enum_view;
+
+typedef enum {
+    GPIO_MODE_INPUT,
+    GPIO_MODE_INPUT_PULLUP,
+    GPIO_MODE_OUTPUT,
+    GPIO_MODE_UNSET
+}GpioUserMode;
+
+typedef enum {
+    GPIO_VALUE_TRUE,
+    GPIO_VALUE_FALSE,
+    GPIO_VALUE_INPUT,
+    GPIO_VALUE_NONE
+}GpioUserValue;
+
+typedef enum {
+    CONFIG_MENU_MODE,
+    CONFIG_MENU_VALUE,
+    CONFIG_MENU_INPUT
+}ConfigMenuOptions;
+
+typedef struct {
+    GpioUserMode mode;
+    GpioUserValue value;
+    int gp_idx_input;
+    bool changed;
+    GpioUserMode prev_mode;
+}GPIOPinUserSelection;
+
+typedef struct {
+    int selected;
+    enum_view view;
+    int wiggle_frame;
+    size_t prev_frame_time;
+    size_t elapsed_time;
+    double result;
+    double freq_var;
+    double elapsed_var;
+    ConfigMenuOptions config_menu_selected;
+} ViewerState;
+
+//  5V  A7  A6  A4  B3  B2  C3 GND SET
+//
+//
+//  3V SWC GND SIO  TX  RX  C1  C0  1W GND
+
+typedef enum {
+    PIN_5V = 0,
+    PIN_A7,
+    PIN_A6,
+    PIN_A4,
+    PIN_B3,
+    PIN_B2,
+    PIN_C3,
+    GEARIC,
+    PIN_3V,
+    PIN_SWC,
+    PIN_SIO,
+    PIN_TX,
+    PIN_RX,
+    PIN_C1,
+    PIN_C0,
+    PIN_1W,
+    PIN_GND_08,
+    PIN_GND_11,
+    PIN_GND_18,
+    NONE
+}enum_view_element;
+
+typedef struct {
+    enum_view_element element;
+    enum_view_element opposite;
+    bool selectable;
+    bool editable;
+    bool top_row;
+    bool pull_out;
+    int gp_idx;
+    uint8_t x_pos;
+    uint8_t y_pos;
+    const char* name;
+    Icon* icon;
+    Icon* selected_icon;
+}ViewElement;
+
+typedef struct {
+    uint8_t element_idx;
+    const GpioPin* pin;
+    GpioMode mode;
+    GpioPull pull;
+    GpioSpeed speed;
+    double value;
+    const char* name;
+    bool unset;
+    bool found;
+    bool input;
+    GPIOPinUserSelection user;
+}GPIOPin;
+
+// GPIO enums from firmware/targets/f7/furi_hal/furi_hal_gpio.h
+
+// /**
+//  * Gpio modes
+//  */
+// typedef enum {
+//     *GpioModeInput,
+//     *GpioModeOutputPushPull,
+//     GpioModeOutputOpenDrain,
+//     GpioModeAltFunctionPushPull,
+//     GpioModeAltFunctionOpenDrain,
+//     *GpioModeAnalog,
+//     GpioModeInterruptRise,
+//     GpioModeInterruptFall,
+//     GpioModeInterruptRiseFall,
+//     GpioModeEventRise,
+//     GpioModeEventFall,
+//     GpioModeEventRiseFall,
+// } GpioMode;
+
+// /**
+//  * Gpio pull modes
+//  */
+// typedef enum {
+//     GpioPullNo,
+//     GpioPullUp,
+//     GpioPullDown,
+// } GpioPull;
+
+
+#endif 

+ 1 - 1
non_catalog_apps/gpio_controller/application.fam

@@ -7,5 +7,5 @@ App(
     stack_size=1 * 1024,
     fap_category="GPIO",
     fap_icon="icon10px.png",
-    fap_icon_assets="images",
+    fap_icon_assets="images"
 )

+ 491 - 165
non_catalog_apps/gpio_controller/gpio_controller.c

@@ -1,6 +1,10 @@
 #include <furi.h>
 #include <furi_hal.h>
 
+#include <storage/storage.h>
+#include <toolbox/stream/stream.h>
+#include <toolbox/stream/file_stream.h>
+
 #include <gui/gui.h>
 #include <input/input.h>
 
@@ -8,160 +12,506 @@
  * Just set fap_icon_assets in application.fam and #include {APPID}_icons.h */
 #include "gpio_controller_icons.h"
 
-#include "gpio_items.h"
-
-typedef struct {
-    int selected;
-    GPIOItems* gpio_items;
-} ViewerState;
-
-static ViewerState vstate = {.selected = 0};
-
-typedef enum {
-    PIN_5V = 0,
-    PIN_A7,
-    PIN_A6,
-    PIN_A4,
-    PIN_B3,
-    PIN_B2,
-    PIN_C3,
-    GEARIC,
-    PIN_3V,
-    PIN_SWC,
-    PIN_SIO,
-    PIN_TX,
-    PIN_RX,
-    PIN_C1,
-    PIN_C0,
-    PIN_1W,
-    PIN_GND_08,
-    PIN_GND_11,
-    PIN_GND_18,
-    NONE
-}ViewElement;
-
- //  5V  A7  A6  A4  B3  B2  C3 GND SET
- //
- //
- //  3V SWC GND SIO  TX  RX  C1  C0  1W GND
-
-// next element when pressing up or down from a given element
-// ground pins cannot be selected
-static uint8_t y_mapping[] = { 
-    PIN_3V,  // <- PIN_5V
-    PIN_SWC, // <- PIN_A7
-    NONE,    // <- PIN_A6
-    PIN_SIO, // <- PIN_A4
-    PIN_TX,  // <- PIN_B3
-    PIN_RX,  // <- PIN_B2
-    PIN_C1,  // <- PIN_C3
-    PIN_1W,  // <- GEARIC
-    PIN_5V,  // <- PIN_3V
-    PIN_A7,  // <- PIN_SWC
-    PIN_A4,  // <- PIN_SIO
-    PIN_B3,  // <- PIN_TX
-    PIN_B2,  // <- PIN_RX
-    PIN_C3,  // <- PIN_C1
-    NONE,    // <- PIN_C0
-    GEARIC   // <- PIN_1W
-};
+#include "app_defines.h"
+
+static void draw_main_view(Canvas* canvas, void* ctx);
+static void draw_config_menu_view(Canvas* canvas, void* ctx);
 
-static uint8_t x_pos[] = {
-      0, // PIN_5V
-     14, // PIN_A7
-     28, // PIN_A6
-     42, // PIN_A4
-     56, // PIN_B3
-     70, // PIN_B2
-     84, // PIN_C3
-    112, // GEARIC
-      0, // PIN_3V
-     14, // PIN_SWC
-     42, // PIN_SIO
-     56, // PIN_TX
-     70, // PIN_RX
-     84, // PIN_C1
-     98, // PIN_C0
-    112, // PIN_1W
-     98, // PIN_GND_08
-     28, // PIN_GND_11
-    126  // PIN_GND_18
+static DrawView draw_view_funcs[] = {
+    draw_main_view,
+    draw_config_menu_view
 };
 
-static int gp_pins[] = {
-     -1, // PIN_5V
-      0, // PIN_A7
-      1, // PIN_A6
-      2, // PIN_A4
-      3, // PIN_B3
-      4, // PIN_B2
-      5, // PIN_C3
-     -1, // GEARIC
-     -1, // PIN_3V
-     -1, // PIN_SWC
-     -1, // PIN_SIO
-     -1, // PIN_TX
-     -1, // PIN_RX
-      6, // PIN_C1
-      7, // PIN_C0
-     -1, // PIN_1W
+static void handle_main_input(InputEvent* event, void* ctx);
+static void handle_config_menu_input(InputEvent* event, void* ctx);
+
+static HandleInput input_handlers[] = {
+    handle_main_input,
+    handle_config_menu_input
 };
 
-static Icon * icons[] = {
-    (Icon*)&I_5v_pin,   // PIN_5V
-    (Icon*)&I_a7_pin,   // PIN_A7
-    (Icon*)&I_a6_pin,   // PIN_A6
-    (Icon*)&I_a4_pin,   // PIN_A4
-    (Icon*)&I_b3_pin,   // PIN_B3
-    (Icon*)&I_b2_pin,   // PIN_B2
-    (Icon*)&I_c3_pin,   // PIN_C3
-    (Icon*)&I_gear_unhighlighted, // GEARIC
-    (Icon*)&I_3v_pin,   // PIN_3V
-    (Icon*)&I_swc_pin,  // PIN_SWC
-    (Icon*)&I_sio_pin,  // PIN_SIO
-    (Icon*)&I_tx_pin,   // PIN_TX
-    (Icon*)&I_rx_pin,   // PIN_RX
-    (Icon*)&I_c1_pin,   // PIN_C1
-    (Icon*)&I_c0_pin,   // PIN_C0
-    (Icon*)&I_1w_pin    // PIN_1W
+static ViewerState vstate;
+
+static int wiggle[] = {-1,1,-1,1};
+static uint32_t wiggle_frame_count = 4;
+
+static ViewElement elements[] = {
+    {PIN_5V,     PIN_3V,  true, false,  true,  true, -1,   0,   0, "5V" ,               (Icon*)&I_5v_pin, NULL},
+    {PIN_A7,     PIN_SWC, true, false,  true,  true, -1,  14,   0, "PA7",               (Icon*)&I_a7_pin, NULL},
+    {PIN_A6,     NONE,    true, false,  true,  true, -1,  28,   0, "PA6",               (Icon*)&I_a6_pin, NULL},
+    {PIN_A4,     PIN_SIO, true, false,  true,  true, -1,  42,   0, "PA4",               (Icon*)&I_a4_pin, NULL},
+    {PIN_B3,     PIN_TX,  true, false,  true,  true, -1,  56,   0, "PB3",               (Icon*)&I_b3_pin, NULL},
+    {PIN_B2,     PIN_RX,  true, false,  true,  true, -1,  70,   0, "PB2",               (Icon*)&I_b2_pin, NULL},
+    {PIN_C3,     PIN_C1,  true, false,  true,  true, -1,  84,   0, "PC3",               (Icon*)&I_c3_pin, NULL}, 
+    {GEARIC,     PIN_1W,  true,  true,  true, false, -1, 112,   0, "Settings",          (Icon*)&I_gear_unhighlighted, (Icon*)&I_gear_highlighted},
+    {PIN_3V,     PIN_5V,  true, false, false,  true, -1,   0,  48, "3.3V",              (Icon*)&I_3v_pin, NULL},
+    {PIN_SWC,    PIN_A7,  true, false, false,  true, -1,  14,  48, "Serial Wire Clock", (Icon*)&I_swc_pin, NULL},
+    {PIN_SIO,    PIN_A4,  true, false, false,  true, -1,  42,  48, "Serial IO",         (Icon*)&I_sio_pin, NULL},
+    {PIN_TX,     PIN_B3,  true, false, false,  true, -1,  56,  48, "UART - Transmit",   (Icon*)&I_tx_pin, NULL},
+    {PIN_RX,     PIN_B2,  true, false, false,  true, -1,  70,  48, "UART - Receive",    (Icon*)&I_rx_pin, NULL},
+    {PIN_C1,     PIN_C3,  true, false, false,  true, -1,  84,  48, "PC1",               (Icon*)&I_c1_pin, NULL},
+    {PIN_C0,     NONE,    true, false, false,  true, -1,  98,  48, "PC0",               (Icon*)&I_c0_pin, NULL},
+    {PIN_1W,     GEARIC,  true,  true, false,  true, -1, 112,  48, "1-Wire",            (Icon*)&I_1w_pin, NULL},
+    {PIN_GND_08, NONE,   false, false,  true, false, -1,  98,  -1, "GND (Ground)",      (Icon*)&I_gnd_pin, NULL},
+    {PIN_GND_11, NONE,   false, false, false, false, -1,  28,  48, "GND (Ground)",      (Icon*)&I_gnd_pin, NULL},
+    {PIN_GND_18, NONE,   false, false, false, false, -1, 126,  48, "GND (Ground)",      (Icon*)&I_gnd_pin, NULL},
 };
 
-static uint8_t bot_row_y = 48;
+static GPIOPin gpio_pin_config[GPIO_PIN_COUNT];
 
-// Screen is 128x64 px
-static void app_draw_callback(Canvas* canvas, void* ctx) {
+static int element_count = NONE; // The NONE enum will a value equal to the number of elements defined in enum_view_element
+
+size_t strnlen(const char *str, size_t maxlen) {
+    size_t len = 0;
+    while (len < maxlen && str[len] != '\0') {
+        len++;
+    }
+    return len;
+}
+
+static void init_state()
+{
+    vstate.selected = PIN_A7;
+    vstate.wiggle_frame=-1;
+    vstate.view = MAIN_VIEW;
+}
+
+static void init_gpio()
+{
+    int count = 0;
+    for(size_t i = 0; i < gpio_pins_count; i++) {
+        if(!gpio_pins[i].debug) {
+            for(int j = 0; j < element_count; j++) {
+                if( strcmp(elements[j].name,gpio_pins[i].name) == 0 )
+                {
+                        gpio_pin_config[count].element_idx     = j;
+                        gpio_pin_config[count].pin             = gpio_pins[i].pin;
+                        gpio_pin_config[count].mode            = GpioModeOutputPushPull;
+                        gpio_pin_config[count].pull            = GpioPullNo;
+                        gpio_pin_config[count].speed           = GpioSpeedVeryHigh;
+                        gpio_pin_config[count].value           = 0;
+                        gpio_pin_config[count].name            = gpio_pins[i].name;
+                        gpio_pin_config[count].unset           = true;
+                        gpio_pin_config[count].found           = true;
+                        gpio_pin_config[count].input           = false;
+
+                        gpio_pin_config[count].user.mode = GPIO_MODE_UNSET;
+                        gpio_pin_config[count].user.value = GPIO_VALUE_FALSE;
+                        gpio_pin_config[count].user.gp_idx_input = -1;
+                        gpio_pin_config[count].user.changed = false;
+
+                        elements[j].gp_idx   = i;
+                        elements[j].editable = true;
+
+                        count++;
+                }
+            }
+        }
+    }
+
+    vstate.result = 0;
+}
+
+static void update_gpio()
+{
+    // read from gpio pins
+    for(int i = 0; i < GPIO_PIN_COUNT; i++) {
+        GPIOPin* gpc = &gpio_pin_config[i];
+        if( !gpc->unset )
+        {
+            if( gpc->mode == GpioModeInput ) {
+                gpc->value = furi_hal_gpio_read(gpc->pin) ? 1 : 0;
+            }
+        }
+    }
+}
+
+#define TOGGLECOLOR(state, canvas, setting, selected_col, deselected_col) \
+    canvas_set_color(canvas, (state == setting) ? selected_col : deselected_col)
+
+
+const char* gpio_user_mode_strs[] = {"INPUT","INPUT_PULLUP","OUTPUT","UNSET"};
+const char* gpio_user_value_strs[] = {"TRUE","FALSE","INPUT"};
+
+static void draw_config_menu_view(Canvas* canvas, void* ctx)
+{
+    UNUSED(ctx);
+
+    int gp_idx = elements[vstate.selected].gp_idx;
+    GPIOPin* gpc = &gpio_pin_config[gp_idx];
+
+    UNUSED(gpc);
+
+    canvas_set_font(canvas, FontSecondary);
+
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_rframe(canvas, 1, 1, 126, 62, 0);
+    
+    TOGGLECOLOR(vstate.config_menu_selected, canvas, CONFIG_MENU_MODE, ColorBlack, ColorWhite);
+    canvas_draw_box(canvas, 2, 2, 124, 15);
+
+    TOGGLECOLOR(vstate.config_menu_selected, canvas, CONFIG_MENU_MODE, ColorWhite, ColorBlack);
+    canvas_draw_str(canvas, 6, 12, "Mode");
+
+    if( gpc->user.mode > 0 ) canvas_draw_str(canvas, 34, 12, "<");
+
+    canvas_draw_str(canvas, 45, 12, gpio_user_mode_strs[gpc->user.mode]);
+
+    if( gpc->user.mode < GPIO_MODE_UNSET ) canvas_draw_str(canvas, 120, 12, ">");
+
+    if( gpc->user.mode == GPIO_MODE_OUTPUT )
+    {
+        TOGGLECOLOR(vstate.config_menu_selected, canvas, CONFIG_MENU_VALUE, ColorBlack, ColorWhite);
+        canvas_draw_box(canvas, 2, 16, 124, 15);
+
+        TOGGLECOLOR(vstate.config_menu_selected, canvas, CONFIG_MENU_VALUE, ColorWhite, ColorBlack);
+        canvas_draw_str(canvas, 6, 12 + 16, "Value");
+
+        if( gpc->user.value > 0 ) canvas_draw_str(canvas, 34, 12 + 16, "<");
+
+        canvas_draw_str(canvas, 45, 12 + 16, gpio_user_value_strs[gpc->user.value]);
+
+        if( gpc->user.value < GPIO_VALUE_INPUT ) canvas_draw_str(canvas, 120, 12 + 16, ">");    
+    }
+
+}
+
+// TODO: Determine the lowest frame delta we can get away with. 
+// TODO: Redraw only what changes.
+//       - clear previous (drawn) selected pin
+//       - clear newly selected pin
+
+static void draw_main_view(Canvas* canvas, void* ctx)
+{
     UNUSED(ctx);
 
     canvas_clear(canvas);
 
-    // draw ground pins
-    canvas_draw_icon(canvas, x_pos[PIN_GND_08], -1, &I_gnd_pin);
-    canvas_draw_icon(canvas, x_pos[PIN_GND_11], bot_row_y, &I_gnd_pin);
-    canvas_draw_icon(canvas, x_pos[PIN_GND_18], bot_row_y, &I_gnd_pin);
+    size_t current_frame_time = furi_get_tick();
+    size_t delta_cycles = (current_frame_time > vstate.prev_frame_time ? current_frame_time - vstate.prev_frame_time : 0);
+    size_t delta_time_ms = delta_cycles * 1000 / furi_kernel_get_tick_frequency();
+
+    // delay until desired delta time and recalculate
+    if( delta_time_ms < FRAME_TIME )
+    {
+        furi_delay_ms(FRAME_TIME-delta_time_ms);
+        current_frame_time = furi_get_tick();
+        delta_cycles = (current_frame_time > vstate.prev_frame_time ? current_frame_time - vstate.prev_frame_time : 0);
+        delta_time_ms = delta_cycles * 1000 / furi_kernel_get_tick_frequency();
+    }
+    
+    vstate.elapsed_time += delta_time_ms;
+    vstate.prev_frame_time = current_frame_time;
+
+    canvas_set_font(canvas, FontSecondary);
+
+    char hex_string[3];
+
+    // draw values
+    for(int i = 0; i < GPIO_PIN_COUNT; i++) {
+        if( !gpio_pin_config[i].unset )
+        {
+            ViewElement e = elements[gpio_pin_config[i].element_idx];
+
+            // draw wire
+            if(e.top_row)
+            {
+                canvas_draw_line(canvas, e.x_pos + 6, e.y_pos + 16, e.x_pos + 6, e.y_pos + 16 + 8);
+            }
+            else
+            {
+                canvas_draw_line(canvas, e.x_pos + 6, e.y_pos, e.x_pos + 6, e.y_pos - 8);
+            }
+            
+            if(gpio_pin_config[i].mode == GpioModeAnalog)
+            {
+                snprintf(hex_string, sizeof(hex_string), "%02X", (int)gpio_pin_config[i].value);
+                if(e.top_row)
+                {
+                    canvas_draw_icon(canvas, e.x_pos - 1, e.y_pos + 20, &I_analog_box);
+                    canvas_draw_str(canvas, e.x_pos + 1, e.y_pos + 22 + 7, hex_string);
+
+                }   
+                else
+                {
+                    canvas_draw_icon(canvas, e.x_pos - 1, e.y_pos - 15, &I_analog_box);
+                    canvas_draw_str(canvas, e.x_pos + 1, e.y_pos - 6, hex_string);
+                }
+            }
+            else
+            {
+                const Icon* icon = (int)gpio_pin_config[i].value ? &I_digi_one : &I_digi_zero;
+                if(e.top_row)
+                {
+                    canvas_draw_icon(canvas, e.x_pos + 2, e.y_pos + 20, icon);
+                }   
+                else
+                {
+                    canvas_draw_icon(canvas, e.x_pos + 2, e.y_pos - 13, icon);
+                }
+                
+            }
+        }
+    }
+
+    for(int i = 0; i < element_count; i++)
+    {
+        ViewElement e = elements[i];
+        int x = e.x_pos;
+        int y = e.y_pos + (e.top_row && e.pull_out ? -3 : 0);
+        Icon* icon = e.icon;
+
+        if( vstate.selected == i )
+        {
+            if( e.pull_out )
+            {
+                y += e.top_row ? 3 : -3;
+            }
+            if( e.selected_icon != NULL )
+            {
+                icon = e.selected_icon;
+            }
+
+            if(vstate.wiggle_frame >= 0)
+            {
+                x += wiggle[vstate.wiggle_frame];
 
-    // draw gear
-    canvas_draw_icon(canvas, x_pos[GEARIC], 0, (vstate.selected == GEARIC ? &I_gear_highlighted : &I_gear_unhighlighted));
+                if(vstate.elapsed_time >= ANIMATE_FRAME_TIME_MS)
+                {
+                    vstate.wiggle_frame++;
+                    if ((unsigned int)(vstate.wiggle_frame) >= wiggle_frame_count)
+                    {
+                        vstate.wiggle_frame = -1;
+                    }
+                    vstate.elapsed_time = 0;
+                }
+            }
+        }
+
+        canvas_draw_icon(canvas, x, y, icon);
+    }
+
+    // draw arrows
+    for(int i = 0; i < GPIO_PIN_COUNT; i++) {
+        if( !gpio_pin_config[i].unset )
+        {
+            ViewElement e = elements[gpio_pin_config[i].element_idx];
 
-    // draw top row of pins
-    for( int i = 0; i < GEARIC; i++ )
+            bool selected = vstate.selected == gpio_pin_config[i].element_idx;
+
+            // draw arrow
+            if(e.top_row)
+            {   
+                int offset = selected ? 3 : 0;
+                const Icon* arrow_icon = gpio_pin_config[i].input ? &I_arrow_up : &I_arrow_down;
+                canvas_draw_icon(canvas, e.x_pos + 3, e.y_pos + 8 + offset, arrow_icon);
+            }   
+            else
+            {
+                int offset = selected ? 0 : 3;
+                const Icon* arrow_icon = gpio_pin_config[i].input ? &I_arrow_down : &I_arrow_up;
+                canvas_draw_icon(canvas, e.x_pos + 3, e.y_pos + -1 + offset, arrow_icon);
+            }
+        }
+    }
+
+    
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 0, 42, elements[vstate.selected].name);
+}
+
+static void handle_main_input(InputEvent* event, void* ctx) {
+    if( vstate.wiggle_frame < 0 )
     {
-        int y = vstate.selected == i ? 0 : -3;
-        canvas_draw_icon(canvas, x_pos[i], y, icons[i]);
+        furi_assert(ctx);
+        FuriMessageQueue* event_queue = ctx;
+
+        // place in queue to handle backing out of app
+        furi_message_queue_put(event_queue, event, FuriWaitForever);
+
+        if( (event->type == InputTypePress || event->type == InputTypeRelease) && event->key == InputKeyOk )
+        {
+            if( event->type == InputTypePress && elements[vstate.selected].gp_idx < 0 )
+            {
+                vstate.wiggle_frame = 0;
+                vstate.elapsed_time = 0;
+            }
+            else if( elements[vstate.selected].gp_idx >= 0 && (event->type == InputTypePress || event->type == InputTypeRelease) )
+            {
+                int gp_idx = elements[vstate.selected].gp_idx;
+                gpio_pin_config[gp_idx].user.prev_mode = gpio_pin_config[gp_idx].user.mode;
+
+                vstate.view = CONFIG_MENU_VIEW;
+                vstate.config_menu_selected = CONFIG_MENU_MODE;
+            }
+        }
+        else if(event->type == InputTypePress || event->type == InputTypeRepeat) {
+            switch(event->key) {
+            case InputKeyLeft:
+                vstate.selected--;
+                if(vstate.selected == GEARIC) vstate.selected = PIN_1W;
+                else if(vstate.selected < 0) vstate.selected = GEARIC;
+                break;
+            case InputKeyRight:
+                if(vstate.selected <= GEARIC)
+                {
+                    vstate.selected++;
+                    vstate.selected = vstate.selected > GEARIC ? PIN_5V : vstate.selected;
+                }
+                else
+                {
+                    vstate.selected++;
+                    vstate.selected = vstate.selected > PIN_1W ? PIN_3V : vstate.selected;
+                }
+                break;
+            case InputKeyUp:
+            case InputKeyDown:
+                if (elements[vstate.selected].opposite != NONE) vstate.selected = elements[vstate.selected].opposite;
+                break;
+            default:
+                break;
+            }
+        }
     }
+}
 
-    // draw bottom row of pins
-    for( int i = PIN_3V; i <= PIN_1W; i++ )
+static void set_GPIO_pin_via_user(int gp_idx)
+{
+    GPIOPin* gpc = &gpio_pin_config[gp_idx];
+
+    if(gpc->user.changed)
     {
-        int y = bot_row_y - (vstate.selected == i ? 3 : 0);
-        canvas_draw_icon(canvas, x_pos[i], y, icons[i]);
+        // update attributes
+        switch(gpc->user.mode)
+        {
+        case GPIO_MODE_INPUT:
+            gpc->mode = GpioModeInput;
+            gpc->pull = GpioPullNo;
+            gpc->input = true;
+            break;
+        case GPIO_MODE_INPUT_PULLUP:
+            gpc->mode = GpioModeInput;
+            gpc->pull = GpioPullUp;
+            gpc->input = true;
+            break;
+        case GPIO_MODE_OUTPUT:
+            gpc->mode = GpioModeOutputPushPull;
+            gpc->pull = GpioPullNo;
+            gpc->input = false;
+            break;
+        default:
+            break;
+        }
+
+        switch(gpc->user.value)
+        {
+        case GPIO_VALUE_TRUE:
+            gpc->value = (double)1.0;
+            break;
+        case GPIO_VALUE_FALSE:
+        case GPIO_VALUE_INPUT:
+        case GPIO_VALUE_NONE:
+            gpc->value = (double)0.0;
+            break;
+        default:
+            break;
+        }
+
+        furi_hal_gpio_write(gpc->pin, gpc->value != (double)0.0 ? true : false);
+        if( gpc->user.mode != gpc->user.prev_mode) {
+            furi_hal_gpio_init(gpc->pin, gpc->mode, gpc->pull, gpc->speed);
+            gpc->unset = false;
+        }
+        
+        gpc->user.changed = false;
     }
 }
 
-static void app_input_callback(InputEvent* input_event, void* ctx) {
-    furi_assert(ctx);
+static void handle_config_menu_input(InputEvent* event, void* ctx) {
+    UNUSED(ctx);
+
+    int gp_idx = elements[vstate.selected].gp_idx;
+    GPIOPin* gpc = &gpio_pin_config[gp_idx];
+
+    if(event->type == InputTypePress || event->type == InputTypeRepeat) {
+        switch(event->key) {
+        case InputKeyLeft:
+            switch(vstate.config_menu_selected)
+            {
+            case CONFIG_MENU_MODE:
+                if(gpc->user.mode > 0) {
+                    gpc->user.mode--;
+                    gpc->user.changed = true;
+                }
+                break;
+            case CONFIG_MENU_VALUE:
+                if(gpc->user.value > 0) {
+                    gpc->user.value--;
+                    gpc->user.changed = true;
+                }
+                break;
+            case CONFIG_MENU_INPUT:
+                break;
+            default:
+                break;
+            }
+            break;
+        case InputKeyRight:
+            switch(vstate.config_menu_selected)
+            {
+            case CONFIG_MENU_MODE:
+                if(gpc->user.mode < GPIO_MODE_UNSET) {
+                    gpc->user.mode++;
+                    gpc->user.changed = true;
+                }
+                break;
+            case CONFIG_MENU_VALUE:
+                if(gpc->user.value < GPIO_VALUE_FALSE) {
+                    gpc->user.value++;
+                    gpc->user.changed = true;
+                }
+                break;
+            case CONFIG_MENU_INPUT:
+                break;
+            default:
+                break;
+            }
+            break;
+        case InputKeyUp:
+            if(gpc->user.mode == GPIO_MODE_OUTPUT )
+            { 
+                if( vstate.config_menu_selected == 0 ) vstate.config_menu_selected = CONFIG_MENU_VALUE;
+                else vstate.config_menu_selected--;
+            }
+            break;
+        case InputKeyDown:
+            if(gpc->user.mode == GPIO_MODE_OUTPUT )
+            {
+                if( vstate.config_menu_selected == CONFIG_MENU_VALUE ) vstate.config_menu_selected = 0;
+                else vstate.config_menu_selected++;
+            }
+            break;
+        case InputKeyBack:
+            
+            // Set new pin configuration
+            set_GPIO_pin_via_user(gp_idx);
+
+            vstate.view = MAIN_VIEW;
+
+            break;
+        default:
+            break;
+        }
+    }
+}
 
-    FuriMessageQueue* event_queue = ctx;
-    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
+static void app_draw_callback(Canvas* canvas, void* ctx) {
+    draw_view_funcs[vstate.view](canvas,ctx);
+}
+
+static void app_input_callback(InputEvent* input_event, void* ctx) {
+    input_handlers[vstate.view](input_event,ctx);
 }
 
 int32_t gpio_controller_main(void* p) {
@@ -179,35 +529,18 @@ int32_t gpio_controller_main(void* p) {
 
     InputEvent event;
 
-    vstate.gpio_items = gpio_items_alloc();
-    gpio_items_configure_all_pins(vstate.gpio_items, GpioModeOutputPushPull);
+    init_state();
+    init_gpio();
+
+    vstate.prev_frame_time = furi_get_tick();
+    vstate.elapsed_time = 0;
 
     bool running = true;
     while(running) {
         if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
-            if((event.type == InputTypePress) || (event.type == InputTypeRepeat)) {
+
+            if(event.type == InputTypePress || event.type == InputTypeRepeat) {
                 switch(event.key) {
-                case InputKeyLeft:
-                    vstate.selected--;
-                    if(vstate.selected == GEARIC) vstate.selected = PIN_1W;
-                    else if(vstate.selected < 0) vstate.selected = GEARIC;
-                    break;
-                case InputKeyRight:
-                    if(vstate.selected <= GEARIC)
-                    {
-                        vstate.selected++;
-                        vstate.selected = vstate.selected > GEARIC ? PIN_5V : vstate.selected;
-                    }
-                    else
-                    {
-                        vstate.selected++;
-                        vstate.selected = vstate.selected > PIN_1W ? PIN_3V : vstate.selected;
-                    }
-                    break;
-                case InputKeyUp:
-                case InputKeyDown:
-                    if (y_mapping[vstate.selected] != NONE) vstate.selected = y_mapping[vstate.selected];
-                    break;
                 case InputKeyBack:
                     running = false;
                     break;
@@ -216,18 +549,11 @@ int32_t gpio_controller_main(void* p) {
                 }
             }
         }
-        else if(event.key == InputKeyOk)
-        {
-            if( gp_pins[vstate.selected] >= 0 && (event.type == InputTypePress || event.type == InputTypeRelease) )
-            {
-                gpio_items_set_pin(vstate.gpio_items, gp_pins[vstate.selected], event.type == InputTypePress);
-            }
-        }
+
+        update_gpio();
         view_port_update(view_port);
     }
 
-    gpio_items_free(vstate.gpio_items);
-
     view_port_enabled_set(view_port, false);
     gui_remove_view_port(gui, view_port);
     view_port_free(view_port);

+ 0 - 69
non_catalog_apps/gpio_controller/gpio_items.c

@@ -1,69 +0,0 @@
-#include "gpio_items.h"
-
-#include <furi_hal_resources.h>
-
-struct GPIOItems {
-    GpioPinRecord* pins;
-    size_t count;
-};
-
-GPIOItems* gpio_items_alloc() {
-    GPIOItems* items = malloc(sizeof(GPIOItems));
-
-    items->count = 0;
-    for(size_t i = 0; i < gpio_pins_count; i++) {
-        if(!gpio_pins[i].debug) {
-            items->count++;
-        }
-    }
-
-    items->pins = malloc(sizeof(GpioPinRecord) * items->count);
-    for(size_t i = 0; i < items->count; i++) {
-        if(!gpio_pins[i].debug) {
-            items->pins[i].pin = gpio_pins[i].pin;
-            items->pins[i].name = gpio_pins[i].name;
-        }
-    }
-    return items;
-}
-
-void gpio_items_free(GPIOItems* items) {
-    free(items->pins);
-    free(items);
-}
-
-uint8_t gpio_items_get_count(GPIOItems* items) {
-    return items->count;
-}
-
-void gpio_items_configure_pin(GPIOItems* items, uint8_t index, GpioMode mode) {
-    furi_assert(index < items->count);
-    furi_hal_gpio_write(items->pins[index].pin, false);
-    furi_hal_gpio_init(items->pins[index].pin, mode, GpioPullNo, GpioSpeedVeryHigh);
-}
-
-void gpio_items_configure_all_pins(GPIOItems* items, GpioMode mode) {
-    for(uint8_t i = 0; i < items->count; i++) {
-        gpio_items_configure_pin(items, i, mode);
-    }
-}
-
-void gpio_items_set_pin(GPIOItems* items, uint8_t index, bool level) {
-    furi_assert(index < items->count);
-    furi_hal_gpio_write(items->pins[index].pin, level);
-}
-
-void gpio_items_set_all_pins(GPIOItems* items, bool level) {
-    for(uint8_t i = 0; i < items->count; i++) {
-        gpio_items_set_pin(items, i, level);
-    }
-}
-
-const char* gpio_items_get_pin_name(GPIOItems* items, uint8_t index) {
-    furi_assert(index < items->count + 1);
-    if(index == items->count) {
-        return "ALL";
-    } else {
-        return items->pins[index].name;
-    }
-}

+ 0 - 29
non_catalog_apps/gpio_controller/gpio_items.h

@@ -1,29 +0,0 @@
-#pragma once
-
-#include <furi_hal_gpio.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef struct GPIOItems GPIOItems;
-
-GPIOItems* gpio_items_alloc();
-
-void gpio_items_free(GPIOItems* items);
-
-uint8_t gpio_items_get_count(GPIOItems* items);
-
-void gpio_items_configure_pin(GPIOItems* items, uint8_t index, GpioMode mode);
-
-void gpio_items_configure_all_pins(GPIOItems* items, GpioMode mode);
-
-void gpio_items_set_pin(GPIOItems* items, uint8_t index, bool level);
-
-void gpio_items_set_all_pins(GPIOItems* items, bool level);
-
-const char* gpio_items_get_pin_name(GPIOItems* items, uint8_t index);
-
-#ifdef __cplusplus
-}
-#endif

BIN
non_catalog_apps/gpio_controller/images/analog_box.png


BIN
non_catalog_apps/gpio_controller/images/digi_one.png


BIN
non_catalog_apps/gpio_controller/images/digi_zero.png


+ 113 - 43
non_catalog_apps/seader/ccid.c

@@ -7,15 +7,16 @@ const uint8_t SAM_ATR[] =
     {0x3b, 0x95, 0x96, 0x80, 0xb1, 0xfe, 0x55, 0x1f, 0xc7, 0x47, 0x72, 0x61, 0x63, 0x65, 0x13};
 const uint8_t SAM_ATR2[] = {0x3b, 0x90, 0x96, 0x91, 0x81, 0xb1, 0xfe, 0x55, 0x1f, 0xc7, 0xd4};
 
-bool powered = false;
-uint8_t slot = 0;
-uint8_t sequence = 0;
+bool powered[2] = {false, false};
+uint8_t sam_slot = 0;
+uint8_t sequence[2] = {0, 0};
 uint8_t retries = 3;
-uint8_t getSequence() {
-    if(sequence > 254) {
-        sequence = 0;
+
+uint8_t getSequence(uint8_t slot) {
+    if(sequence[slot] > 254) {
+        sequence[slot] = 0;
     }
-    return sequence++;
+    return sequence[slot]++;
 }
 
 size_t seader_ccid_add_lrc(uint8_t* data, size_t len) {
@@ -27,19 +28,20 @@ size_t seader_ccid_add_lrc(uint8_t* data, size_t len) {
     return len + 1;
 }
 
-void seader_ccid_IccPowerOn(SeaderUartBridge* seader_uart) {
-    if(powered) {
+void seader_ccid_IccPowerOn(SeaderUartBridge* seader_uart, uint8_t slot) {
+    if(powered[slot]) {
         return;
     }
-    powered = true;
-    FURI_LOG_D(TAG, "Sending Power On");
+    powered[slot] = true;
+
+    FURI_LOG_D(TAG, "Sending Power On (%d)", slot);
     memset(seader_uart->tx_buf, 0, SEADER_UART_RX_BUF_SIZE);
     seader_uart->tx_buf[0] = SYNC;
     seader_uart->tx_buf[1] = CTRL;
     seader_uart->tx_buf[2 + 0] = CCID_MESSAGE_TYPE_PC_to_RDR_IccPowerOn;
 
     seader_uart->tx_buf[2 + 5] = slot;
-    seader_uart->tx_buf[2 + 6] = getSequence();
+    seader_uart->tx_buf[2 + 6] = getSequence(slot);
     seader_uart->tx_buf[2 + 7] = 2; //power
 
     seader_uart->tx_len = seader_ccid_add_lrc(seader_uart->tx_buf, 2 + 10);
@@ -48,18 +50,19 @@ void seader_ccid_IccPowerOn(SeaderUartBridge* seader_uart) {
 
 void seader_ccid_check_for_sam(SeaderUartBridge* seader_uart) {
     hasSAM = false; // If someone is calling this, reset sam state
-    powered = false;
-    seader_ccid_GetSlotStatus(seader_uart);
+    powered[0] = false;
+    powered[1] = false;
+    seader_ccid_GetSlotStatus(seader_uart, 0);
 }
 
-void seader_ccid_GetSlotStatus(SeaderUartBridge* seader_uart) {
-    FURI_LOG_D(TAG, "seader_ccid_GetSlotStatus");
+void seader_ccid_GetSlotStatus(SeaderUartBridge* seader_uart, uint8_t slot) {
+    FURI_LOG_D(TAG, "seader_ccid_GetSlotStatus(%d)", slot);
     memset(seader_uart->tx_buf, 0, SEADER_UART_RX_BUF_SIZE);
     seader_uart->tx_buf[0] = SYNC;
     seader_uart->tx_buf[1] = CTRL;
     seader_uart->tx_buf[2 + 0] = CCID_MESSAGE_TYPE_PC_to_RDR_GetSlotStatus;
     seader_uart->tx_buf[2 + 5] = slot;
-    seader_uart->tx_buf[2 + 6] = getSequence();
+    seader_uart->tx_buf[2 + 6] = getSequence(slot);
 
     seader_uart->tx_len = seader_ccid_add_lrc(seader_uart->tx_buf, 2 + 10);
     furi_thread_flags_set(furi_thread_get_id(seader_uart->tx_thread), WorkerEvtSamRx);
@@ -72,8 +75,8 @@ void seader_ccid_SetParameters(SeaderUartBridge* seader_uart) {
     seader_uart->tx_buf[1] = CTRL;
     seader_uart->tx_buf[2 + 0] = CCID_MESSAGE_TYPE_PC_to_RDR_SetParameters;
     seader_uart->tx_buf[2 + 1] = 0;
-    seader_uart->tx_buf[2 + 5] = slot;
-    seader_uart->tx_buf[2 + 6] = getSequence();
+    seader_uart->tx_buf[2 + 5] = sam_slot;
+    seader_uart->tx_buf[2 + 6] = getSequence(sam_slot);
     seader_uart->tx_buf[2 + 7] = T1;
     seader_uart->tx_buf[2 + 8] = 0;
     seader_uart->tx_buf[2 + 9] = 0;
@@ -89,8 +92,8 @@ void seader_ccid_GetParameters(SeaderUartBridge* seader_uart) {
     seader_uart->tx_buf[1] = CTRL;
     seader_uart->tx_buf[2 + 0] = CCID_MESSAGE_TYPE_PC_to_RDR_GetParameters;
     seader_uart->tx_buf[2 + 1] = 0;
-    seader_uart->tx_buf[2 + 5] = slot;
-    seader_uart->tx_buf[2 + 6] = getSequence();
+    seader_uart->tx_buf[2 + 5] = sam_slot;
+    seader_uart->tx_buf[2 + 6] = getSequence(sam_slot);
     seader_uart->tx_buf[2 + 7] = 0;
     seader_uart->tx_buf[2 + 8] = 0;
     seader_uart->tx_buf[2 + 9] = 0;
@@ -101,13 +104,21 @@ void seader_ccid_GetParameters(SeaderUartBridge* seader_uart) {
 }
 
 void seader_ccid_XfrBlock(SeaderUartBridge* seader_uart, uint8_t* data, size_t len) {
+    seader_ccid_XfrBlockToSlot(seader_uart, sam_slot, data, len);
+}
+
+void seader_ccid_XfrBlockToSlot(
+    SeaderUartBridge* seader_uart,
+    uint8_t slot,
+    uint8_t* data,
+    size_t len) {
     memset(seader_uart->tx_buf, 0, SEADER_UART_RX_BUF_SIZE);
     seader_uart->tx_buf[0] = SYNC;
     seader_uart->tx_buf[1] = CTRL;
     seader_uart->tx_buf[2 + 0] = CCID_MESSAGE_TYPE_PC_to_RDR_XfrBlock;
     seader_uart->tx_buf[2 + 1] = len;
     seader_uart->tx_buf[2 + 5] = slot;
-    seader_uart->tx_buf[2 + 6] = getSequence();
+    seader_uart->tx_buf[2 + 6] = getSequence(slot);
     seader_uart->tx_buf[2 + 7] = 5;
     seader_uart->tx_buf[2 + 8] = 0;
     seader_uart->tx_buf[2 + 9] = 0;
@@ -128,35 +139,62 @@ size_t seader_ccid_process(SeaderWorker* seader_worker, uint8_t* cmd, size_t cmd
     for(uint8_t i = 0; i < cmd_len; i++) {
         snprintf(display + (i * 2), sizeof(display), "%02x", cmd[i]);
     }
-    FURI_LOG_D(TAG, "CCID %d: %s", cmd_len, display);
+    // FURI_LOG_D(TAG, "UART %d: %s", cmd_len, display);
 
     if(cmd_len == 2) {
         if(cmd[0] == CCID_MESSAGE_TYPE_RDR_to_PC_NotifySlotChange) {
-            switch(cmd[1]) {
-            case CARD_OUT:
-                FURI_LOG_D(TAG, "Card removed");
-                powered = false;
-                hasSAM = false;
-                retries = 3;
+            switch(cmd[1] & SLOT_0_MASK) {
+            case 0:
+            case 1:
+                // No change, no-op
                 break;
             case CARD_IN_1:
-                FURI_LOG_D(TAG, "Card Inserted (1)");
+                FURI_LOG_D(TAG, "Card Inserted (0)");
+                if(hasSAM && sam_slot == 0) {
+                    break;
+                }
                 retries = 0;
-                slot = 0;
-                sequence = 0;
-                seader_ccid_IccPowerOn(seader_uart);
+                sequence[0] = 0;
+                seader_ccid_IccPowerOn(seader_uart, 0);
+                break;
+            case CARD_OUT_1:
+                FURI_LOG_D(TAG, "Card Removed (0)");
+                if(hasSAM && sam_slot == 0) {
+                    powered[0] = false;
+                    hasSAM = false;
+                    retries = 3;
+                }
+                break;
+            default:
+                FURI_LOG_D(TAG, "Unknown slot 0 card event");
+            };
+
+            switch(cmd[1] & SLOT_1_MASK) {
+            case 0:
+            case 1:
+                // No change, no-op
                 break;
             case CARD_IN_2:
-                FURI_LOG_D(TAG, "Card Inserted (2)");
+                FURI_LOG_D(TAG, "Card Inserted (1)");
+                if(hasSAM && sam_slot == 1) {
+                    break;
+                }
                 retries = 0;
-                slot = 1;
-                sequence = 0;
-                seader_ccid_IccPowerOn(seader_uart);
+                sequence[1] = 0;
+                seader_ccid_IccPowerOn(seader_uart, 1);
                 break;
-            case CARD_IN_BOTH:
-                FURI_LOG_W(TAG, "Loading 2 cards not supported");
+            case CARD_OUT_2:
+                FURI_LOG_D(TAG, "Card Removed (1)");
+                if(hasSAM && sam_slot == 1) {
+                    powered[1] = false;
+                    hasSAM = false;
+                    retries = 3;
+                }
                 break;
+            default:
+                FURI_LOG_D(TAG, "Unknown slot 1 card event");
             };
+
             return 2;
         }
     }
@@ -180,27 +218,53 @@ size_t seader_ccid_process(SeaderWorker* seader_worker, uint8_t* cmd, size_t cmd
         uint8_t* ccid = cmd + 2;
         message.bMessageType = ccid[0];
         message.dwLength = *((uint32_t*)(ccid + 1));
+        message.bSlot = ccid[5];
+        message.bSeq = ccid[6];
         message.bStatus = ccid[7];
         message.bError = ccid[8];
         message.payload = ccid + 10;
 
+        memset(display, 0, sizeof(display));
+        for(uint8_t i = 0; i < message.dwLength; i++) {
+            snprintf(display + (i * 2), sizeof(display), "%02x", message.payload[i]);
+        }
+
         if(cmd_len < 2 + 10 + message.dwLength + 1) {
             return message.consumed;
         }
         message.consumed += 2 + 10 + message.dwLength + 1;
 
+        if(message.dwLength == 0) {
+            FURI_LOG_D(
+                TAG,
+                "CCID [%d|%d] type: %02x, status: %02x, error: %02x",
+                message.bSlot,
+                message.bSeq,
+                message.bMessageType,
+                message.bStatus,
+                message.bError);
+        } else {
+            FURI_LOG_D(
+                TAG,
+                "CCID [%d|%d] %ld: %s",
+                message.bSlot,
+                message.bSeq,
+                message.dwLength,
+                display);
+        }
+
         //0306 81 00000000 0000 0200 01 87
         //0306 81 00000000 0000 0100 01 84
         if(message.bMessageType == CCID_MESSAGE_TYPE_RDR_to_PC_SlotStatus) {
             uint8_t status = (message.bStatus & BMICCSTATUS_MASK);
             if(status == 0 || status == 1) {
-                seader_ccid_IccPowerOn(seader_uart);
+                seader_ccid_IccPowerOn(seader_uart, message.bSlot);
                 return message.consumed;
             } else if(status == 2) {
                 FURI_LOG_W(TAG, "No ICC is present [retries %d]", retries);
                 if(retries-- > 1 && hasSAM == false) {
                     furi_delay_ms(100);
-                    seader_ccid_GetSlotStatus(seader_uart);
+                    seader_ccid_GetSlotStatus(seader_uart, retries % 2);
                 } else {
                     if(seader_worker->callback) {
                         seader_worker->callback(
@@ -227,7 +291,7 @@ size_t seader_ccid_process(SeaderWorker* seader_worker, uint8_t* cmd, size_t cmd
             return message.consumed;
         }
         if(message.bError != 0) {
-            FURI_LOG_W(TAG, "CCID error");
+            FURI_LOG_W(TAG, "CCID error %02x", message.bError);
             message.consumed = cmd_len;
             if(seader_worker->callback) {
                 seader_worker->callback(SeaderWorkerEventSamMissing, seader_worker->context);
@@ -237,11 +301,16 @@ size_t seader_ccid_process(SeaderWorker* seader_worker, uint8_t* cmd, size_t cmd
 
         if(message.bMessageType == CCID_MESSAGE_TYPE_RDR_to_PC_DataBlock) {
             if(hasSAM) {
-                seader_worker_process_message(seader_worker, &message);
+                if(message.bSlot == sam_slot) {
+                    seader_worker_process_sam_message(seader_worker, &message);
+                } else {
+                    FURI_LOG_D(TAG, "Discarding message on non-sam slot");
+                }
             } else {
                 if(memcmp(SAM_ATR, message.payload, sizeof(SAM_ATR)) == 0) {
                     FURI_LOG_I(TAG, "SAM ATR!");
                     hasSAM = true;
+                    sam_slot = message.bSlot;
                     seader_worker_send_version(seader_worker);
                     if(seader_worker->callback) {
                         seader_worker->callback(
@@ -250,6 +319,7 @@ size_t seader_ccid_process(SeaderWorker* seader_worker, uint8_t* cmd, size_t cmd
                 } else if(memcmp(SAM_ATR2, message.payload, sizeof(SAM_ATR2)) == 0) {
                     FURI_LOG_I(TAG, "SAM ATR2!");
                     hasSAM = true;
+                    sam_slot = message.bSlot;
                     seader_worker_send_version(seader_worker);
                     if(seader_worker->callback) {
                         seader_worker->callback(

+ 20 - 5
non_catalog_apps/seader/ccid.h

@@ -14,10 +14,20 @@
 #define NAK (0x15)
 
 #define BMICCSTATUS_MASK 0x03
-#define CARD_OUT 0x02
+/*
+ * Bit 0 = Slot 0 current state
+ * Bit 1 = Slot 0 changed status
+ * Bit 2 = Slot 1 current state
+ * Bit 3 = Slot 1 changed status
+ */
+
+// TODO: rename/renumber
+#define SLOT_0_MASK 0x03
+#define CARD_OUT_1 0x02
 #define CARD_IN_1 0x03
-#define CARD_IN_2 0x06
-#define CARD_IN_BOTH 0x07
+#define SLOT_1_MASK 0x0C
+#define CARD_IN_2 0x04
+#define CARD_OUT_2 0x0C
 
 /*
  *  * BULK_OUT messages from PC to Reader
@@ -83,9 +93,14 @@ struct CCID_Message {
 };
 
 void seader_ccid_check_for_sam(SeaderUartBridge* seader_uart);
-void seader_ccid_IccPowerOn(SeaderUartBridge* seader_uart);
-void seader_ccid_GetSlotStatus(SeaderUartBridge* seader_uart);
+void seader_ccid_IccPowerOn(SeaderUartBridge* seader_uart, uint8_t slot);
+void seader_ccid_GetSlotStatus(SeaderUartBridge* seader_uart, uint8_t slot);
 void seader_ccid_SetParameters(SeaderUartBridge* seader_uart);
 void seader_ccid_GetParameters(SeaderUartBridge* seader_uart);
 void seader_ccid_XfrBlock(SeaderUartBridge* seader_uart, uint8_t* data, size_t len);
+void seader_ccid_XfrBlockToSlot(
+    SeaderUartBridge* seader_uart,
+    uint8_t slot,
+    uint8_t* data,
+    size_t len);
 size_t seader_ccid_process(SeaderWorker* seader_worker, uint8_t* cmd, size_t cmd_len);

+ 2 - 2
non_catalog_apps/seader/seader_worker.c

@@ -278,7 +278,7 @@ void seader_send_payload(
         (&asn_DEF_Payload)
             ->op->print_struct(&asn_DEF_Payload, payload, 1, seader_asn_to_string, payloadDebug);
         if(strlen(payloadDebug) > 0) {
-            FURI_LOG_D(TAG, "Sending payload: %s", payloadDebug);
+            FURI_LOG_D(TAG, "Sending payload[%d %d %d]: %s", to, from, replyTo, payloadDebug);
         }
     }
 #endif
@@ -914,7 +914,7 @@ ReturnCode seader_picopass_card_read(SeaderWorker* seader_worker) {
     return err;
 }
 
-void seader_worker_process_message(SeaderWorker* seader_worker, CCID_Message* message) {
+void seader_worker_process_sam_message(SeaderWorker* seader_worker, CCID_Message* message) {
     if(seader_process_apdu(seader_worker, message->payload, message->dwLength)) {
         // no-op
     } else {

+ 1 - 1
non_catalog_apps/seader/seader_worker.h

@@ -50,5 +50,5 @@ void seader_worker_start(
     void* context);
 
 void seader_worker_stop(SeaderWorker* seader_worker);
-void seader_worker_process_message(SeaderWorker* seader_worker, CCID_Message* message);
+void seader_worker_process_sam_message(SeaderWorker* seader_worker, CCID_Message* message);
 void seader_worker_send_version(SeaderWorker* seader_worker);

+ 21 - 0
non_catalog_apps/tas_playback/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 rcombs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 11 - 0
non_catalog_apps/tas_playback/README.md

@@ -0,0 +1,11 @@
+# TAS playback, now on Flipper!
+
+This app plays back TAS files for retro video games. Connect the GPIO pins to the console's controller port and select a file to play back.
+
+Currently only the Nintendo 64 is supported.
+
+## Consoles
+
+N64: Mupen64 .m64 files
+- Ground (black wire, right pin on console) to any GND pin
+- Data (green wire, center pin) to 16/C0

+ 767 - 0
non_catalog_apps/tas_playback/WString.cpp

@@ -0,0 +1,767 @@
+/*
+  WString.cpp - String library for Wiring & Arduino
+  ...mostly rewritten by Paul Stoffregen...
+  Copyright (c) 2009-10 Hernando Barragan.  All rights reserved.
+  Copyright 2011, Paul Stoffregen, paul@pjrc.com
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include <furi.h>
+#include "avr_functions.h"
+#include "WString.h"
+
+/*********************************************/
+/*  Constructors                             */
+/*********************************************/
+
+String::String(const char *cstr)
+{
+	init();
+	if (cstr) copy(cstr, strlen(cstr));
+}
+
+String::String(const __FlashStringHelper *pgmstr)
+{
+	init();
+	*this = pgmstr;
+}
+
+String::String(const String &value)
+{
+	init();
+	*this = value;
+}
+
+#if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__)
+String::String(String &&rval)
+{
+	init();
+	move(rval);
+}
+String::String(StringSumHelper &&rval)
+{
+	init();
+	move(rval);
+}
+#endif
+
+String::String(char c)
+{
+	init();
+	*this = c;
+}
+
+String::String(unsigned char c)
+{
+	init();
+	char buf[4];
+	ultoa(c, buf, 10);
+	*this = buf;
+}
+
+String::String(const int value, unsigned char base)
+{
+	init();
+	char buf[34];
+	ltoa(value, buf, base);
+	*this = buf;
+}
+
+String::String(unsigned int value, unsigned char base)
+{
+	init();
+	char buf[33];
+  	ultoa(value, buf, base);
+	*this = buf;
+}
+
+String::String(long value, unsigned char base)
+{
+	init();
+	char buf[34];
+	ltoa(value, buf, base);
+	*this = buf;
+}
+
+String::String(unsigned long value, unsigned char base)
+{
+	init();
+	char buf[33];
+	ultoa(value, buf, base);
+	*this = buf;
+}
+
+String::String(long long value, unsigned char base)
+{
+	init();
+	char buf[66];
+	lltoa(value, buf, base);
+	*this = buf;
+}
+
+String::String(unsigned long long value, unsigned char base)
+{
+	init();
+	char buf[65];
+	ulltoa(value, buf, base);
+	*this = buf;
+}
+
+String::String(float num, unsigned char digits)
+{
+	init();
+	char buf[40];
+	*this = dtostrf(num, digits + 2, digits, buf);
+}
+
+String::~String()
+{
+	free(buffer);
+}
+
+/*********************************************/
+/*  Memory Management                        */
+/*********************************************/
+
+inline void String::init(void)
+{
+	buffer = NULL;
+	capacity = 0;
+	len = 0;
+	flags = 0;
+}
+
+unsigned char String::reserve(unsigned int size)
+{
+	if (capacity >= size) return 1;
+	if (changeBuffer(size)) {
+		if (len == 0) buffer[0] = 0;
+		return 1;
+	}
+	return 0;
+}
+
+unsigned char String::changeBuffer(unsigned int maxStrLen)
+{
+	char *newbuffer = (char *)realloc(buffer, maxStrLen + 1);
+	if (newbuffer) {
+		buffer = newbuffer;
+		capacity = maxStrLen;
+		return 1;
+	}
+	return 0;
+}
+
+/*********************************************/
+/*  Copy and Move                            */
+/*********************************************/
+
+String & String::copy(const char *cstr, unsigned int length)
+{
+	if (length == 0) {
+		if (buffer) buffer[0] = 0;
+		len = 0;
+		return *this;
+	}
+	if (!reserve(length)) {
+		if (buffer) {
+			free(buffer);
+			buffer = NULL;
+		}
+		len = capacity = 0;
+		return *this;
+	}
+	len = length;
+	strcpy(buffer, cstr);
+	return *this;
+}
+
+void String::move(String &rhs)
+{
+	if (&rhs == this) return;
+	if (buffer) free(buffer);
+	buffer = rhs.buffer;
+	capacity = rhs.capacity;
+	len = rhs.len;
+	rhs.buffer = NULL;
+	rhs.capacity = 0;
+	rhs.len = 0;
+}
+
+String & String::operator = (const String &rhs)
+{
+	if (this == &rhs) return *this;
+	return copy(rhs.buffer, rhs.len);
+}
+
+#if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__)
+String & String::operator = (String &&rval)
+{
+	if (this != &rval) move(rval);
+	return *this;
+}
+
+String & String::operator = (StringSumHelper &&rval)
+{
+	if (this != &rval) move(rval);
+	return *this;
+}
+#endif
+
+String & String::operator = (const char *cstr)
+{
+	if (cstr) {
+		copy(cstr, strlen(cstr));
+	} else {
+		len = 0;
+	}
+	return *this;
+}
+
+String & String::operator = (const __FlashStringHelper *pgmstr)
+{
+	copy(pgmstr);
+	return *this;
+}
+
+String & String::operator = (char c)
+{
+	char buf[2];
+	buf[0] = c;
+	buf[1] = 0;
+	return copy(buf, 1);
+}
+
+/*********************************************/
+/*  Append                                   */
+/*********************************************/
+
+String & String::append(const String &s)
+{
+	return append(s.buffer, s.len);
+}
+
+String & String::append(const char *cstr, unsigned int length)
+{
+	unsigned int newlen = len + length;
+	bool self = false;
+	unsigned int buffer_offset;
+	if ( (cstr >= buffer) && (cstr < (buffer+len) ) ) {
+		self = true;
+		buffer_offset = (unsigned int)(cstr-buffer);
+	}
+	if (length == 0 || !reserve(newlen)) return *this;
+	if ( self ) {
+		memcpy(buffer + len, buffer+buffer_offset, length);
+		buffer[newlen] = 0;
+		}
+	else
+		strcpy(buffer + len, cstr);
+	len = newlen;
+	return *this;
+}
+
+String & String::append(const char *cstr)
+{
+	if (cstr) append(cstr, strlen(cstr));
+	return *this;
+}
+
+String & String::append(char c)
+{
+	char buf[2];
+	buf[0] = c;
+	buf[1] = 0;
+	append(buf, 1);
+	return *this;
+}
+
+String & String::append(int num)
+{
+	char buf[12];
+	ltoa((long)num, buf, 10);
+	append(buf, strlen(buf));
+	return *this;
+}
+
+String & String::append(unsigned int num)
+{
+	char buf[11];
+	ultoa((unsigned long)num, buf, 10);
+	append(buf, strlen(buf));
+	return *this;
+}
+
+String & String::append(long num)
+{
+	char buf[12];
+	ltoa(num, buf, 10);
+	append(buf, strlen(buf));
+	return *this;
+}
+
+String & String::append(unsigned long num)
+{
+	char buf[11];
+	ultoa(num, buf, 10);
+	append(buf, strlen(buf));
+	return *this;
+}
+
+String & String::append(long long num)
+{
+	char buf[22];
+	lltoa(num, buf, 10);
+	append(buf, strlen(buf));
+	return *this;
+}
+
+String & String::append(unsigned long long num)
+{
+	char buf[21];
+	ulltoa(num, buf, 10);
+	append(buf, strlen(buf));
+	return *this;
+}
+
+String & String::append(float num)
+{
+	char buf[30];
+	dtostrf(num, 4, 2, buf);
+	append(buf, strlen(buf));
+	return *this;
+}
+
+
+/*********************************************/
+/*  Concatenate                              */
+/*********************************************/
+
+
+StringSumHelper & operator + (const StringSumHelper &lhs, const String &rhs)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append(rhs.buffer, rhs.len);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, const char *cstr)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	if (cstr) a.append(cstr, strlen(cstr));
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, const __FlashStringHelper *pgmstr)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append(pgmstr);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, char c)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append(c);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, unsigned char c)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append(c);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, int num)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append((long)num);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, unsigned int num)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append((unsigned long)num);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, long num)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append(num);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, unsigned long num)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append(num);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, long long num)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append(num);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, unsigned long long num)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append(num);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, float num)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append(num);
+	return a;
+}
+
+StringSumHelper & operator + (const StringSumHelper &lhs, double num)
+{
+	StringSumHelper &a = const_cast<StringSumHelper&>(lhs);
+	a.append(num);
+	return a;
+}
+
+/*********************************************/
+/*  Comparison                               */
+/*********************************************/
+
+int String::compareTo(const String &s) const
+{
+	if (!buffer || !s.buffer) {
+		if (s.buffer && s.len > 0) return 0 - *(unsigned char *)s.buffer;
+		if (buffer && len > 0) return *(unsigned char *)buffer;
+		return 0;
+	}
+	return strcmp(buffer, s.buffer);
+}
+
+unsigned char String::equals(const String &s2) const
+{
+	return (len == s2.len && compareTo(s2) == 0);
+}
+
+unsigned char String::equals(const char *cstr) const
+{
+	if (len == 0) return (cstr == NULL || *cstr == 0);
+	if (cstr == NULL) return buffer[0] == 0;
+	return strcmp(buffer, cstr) == 0;
+}
+
+unsigned char String::operator<(const String &rhs) const
+{
+	return compareTo(rhs) < 0;
+}
+
+unsigned char String::operator>(const String &rhs) const
+{
+	return compareTo(rhs) > 0;
+}
+
+unsigned char String::operator<=(const String &rhs) const
+{
+	return compareTo(rhs) <= 0;
+}
+
+unsigned char String::operator>=(const String &rhs) const
+{
+	return compareTo(rhs) >= 0;
+}
+
+/*
+unsigned char String::equalsIgnoreCase( const String &s2 ) const
+{
+	if (this == &s2) return 1;
+	if (len != s2.len) return 0;
+	if (len == 0) return 1;
+	const char *p1 = buffer;
+	const char *p2 = s2.buffer;
+	while (*p1) {
+		if (tolower(*p1++) != tolower(*p2++)) return 0;
+	}
+	return 1;
+}
+*/
+
+unsigned char String::startsWith( const String &s2 ) const
+{
+	if (len < s2.len) return 0;
+	return startsWith(s2, 0);
+}
+
+unsigned char String::startsWith( const String &s2, unsigned int offset ) const
+{
+	if (offset > len - s2.len || !buffer || !s2.buffer) return 0;
+	return strncmp( &buffer[offset], s2.buffer, s2.len ) == 0;
+}
+
+unsigned char String::endsWith( const String &s2 ) const
+{
+	if ( len < s2.len || !buffer || !s2.buffer) return 0;
+	return strcmp(&buffer[len - s2.len], s2.buffer) == 0;
+}
+
+/*********************************************/
+/*  Character Access                         */
+/*********************************************/
+
+const char String::zerotermination = 0;
+
+char String::charAt(unsigned int loc) const
+{
+	return operator[](loc);
+}
+
+void String::setCharAt(unsigned int loc, char c)
+{
+	if (loc < len) buffer[loc] = c;
+}
+
+char & String::operator[](unsigned int index)
+{
+	static char dummy_writable_char;
+	if (index >= len || !buffer) {
+		dummy_writable_char = 0;
+		return dummy_writable_char;
+	}
+	return buffer[index];
+}
+
+char String::operator[]( unsigned int index ) const
+{
+	if (index >= len || !buffer) return 0;
+	return buffer[index];
+}
+
+void String::getBytes(unsigned char *buf, unsigned int bufsize, unsigned int index) const
+{
+	if (!bufsize || !buf) return;
+	if (index >= len) {
+		buf[0] = 0;
+		return;
+	}
+	unsigned int n = bufsize - 1;
+	if (n > len - index) n = len - index;
+	strncpy((char *)buf, buffer + index, n);
+	buf[n] = 0;
+}
+
+/*********************************************/
+/*  Search                                   */
+/*********************************************/
+
+int String::indexOf(char c) const
+{
+	return indexOf(c, 0);
+}
+
+int String::indexOf( char ch, unsigned int fromIndex ) const
+{
+	if (fromIndex >= len) return -1;
+	const char* temp = strchr(buffer + fromIndex, ch);
+	if (temp == NULL) return -1;
+	return temp - buffer;
+}
+
+int String::indexOf(const String &s2) const
+{
+	return indexOf(s2, 0);
+}
+
+int String::indexOf(const String &s2, unsigned int fromIndex) const
+{
+	if (fromIndex >= len) return -1;
+	const char *found = strstr(buffer + fromIndex, s2.buffer);
+	if (found == NULL) return -1;
+	return found - buffer;
+}
+
+int String::lastIndexOf( char theChar ) const
+{
+	return lastIndexOf(theChar, len - 1);
+}
+
+int String::lastIndexOf(char ch, unsigned int fromIndex) const
+{
+	if (fromIndex >= len) return -1;
+	char tempchar = buffer[fromIndex + 1];
+	buffer[fromIndex + 1] = '\0';
+	char* temp = strrchr( buffer, ch );
+	buffer[fromIndex + 1] = tempchar;
+	if (temp == NULL) return -1;
+	return temp - buffer;
+}
+
+int String::lastIndexOf(const String &s2) const
+{
+	return lastIndexOf(s2, len - s2.len);
+}
+
+int String::lastIndexOf(const String &s2, unsigned int fromIndex) const
+{
+  	if (s2.len == 0 || len == 0 || s2.len > len) return -1;
+	if (fromIndex >= len) fromIndex = len - 1;
+	int found = -1;
+	for (char *p = buffer; p <= buffer + fromIndex; p++) {
+		p = strstr(p, s2.buffer);
+		if (!p) break;
+		if ((unsigned int)(p - buffer) <= fromIndex) found = p - buffer;
+	}
+	return found;
+}
+
+String String::substring( unsigned int left ) const
+{
+	return substring(left, len);
+}
+
+String String::substring(unsigned int left, unsigned int right) const
+{
+	if (left > right) {
+		unsigned int temp = right;
+		right = left;
+		left = temp;
+	}
+	String out;
+	if (left > len) return out;
+	if (right > len) right = len;
+	char temp = buffer[right];  // save the replaced character
+	buffer[right] = '\0';
+	out = buffer + left;  // pointer arithmetic
+	buffer[right] = temp;  //restore character
+	return out;
+}
+
+/*********************************************/
+/*  Modification                             */
+/*********************************************/
+
+String & String::replace(char find, char replace)
+{
+	if (!buffer) return *this;
+	for (char *p = buffer; *p; p++) {
+		if (*p == find) *p = replace;
+	}
+	return *this;
+}
+
+String & String::replace(const String& find, const String& replace)
+{
+	if (len == 0 || find.len == 0) return *this;
+	int diff = replace.len - find.len;
+	char *readFrom = buffer;
+	char *foundAt;
+	if (diff == 0) {
+		while ((foundAt = strstr(readFrom, find.buffer)) != NULL) {
+			memcpy(foundAt, replace.buffer, replace.len);
+			readFrom = foundAt + replace.len;
+		}
+	} else if (diff < 0) {
+		char *writeTo = buffer;
+		while ((foundAt = strstr(readFrom, find.buffer)) != NULL) {
+			unsigned int n = foundAt - readFrom;
+			memcpy(writeTo, readFrom, n);
+			writeTo += n;
+			memcpy(writeTo, replace.buffer, replace.len);
+			writeTo += replace.len;
+			readFrom = foundAt + find.len;
+			len += diff;
+		}
+		strcpy(writeTo, readFrom);
+	} else {
+		unsigned int size = len; // compute size needed for result
+		while ((foundAt = strstr(readFrom, find.buffer)) != NULL) {
+			readFrom = foundAt + find.len;
+			size += diff;
+		}
+		if (size == len) return *this;
+		if (size > capacity && !changeBuffer(size)) return *this;
+		int index = len - 1;
+		while (index >= 0 && (index = lastIndexOf(find, index)) >= 0) {
+			readFrom = buffer + index + find.len;
+			memmove(readFrom + diff, readFrom, len - (readFrom - buffer));
+			len += diff;
+			buffer[len] = 0;
+			memcpy(buffer + index, replace.buffer, replace.len);
+			index--;
+		}
+	}
+	return *this;
+}
+
+String & String::remove(unsigned int index)
+{
+	if (index < len) {
+		len = index;
+		buffer[len] = 0;
+	}
+	return *this;
+}
+
+String & String::remove(unsigned int index, unsigned int count)
+{
+	if (index < len && count > 0) {
+  		if (index + count > len) count = len - index;
+		len = len - count;
+		memmove(buffer + index, buffer + index + count, len - index);
+		buffer[len] = 0;
+	}
+	return *this;
+}
+
+/*
+String & String::toLowerCase(void)
+{
+	if (!buffer) return *this;
+	for (char *p = buffer; *p; p++) {
+		*p = tolower(*p);
+	}
+	return *this;
+}
+
+String & String::toUpperCase(void)
+{
+	if (!buffer) return *this;
+	for (char *p = buffer; *p; p++) {
+		*p = toupper(*p);
+	}
+	return *this;
+}
+*/
+
+#define isspace(x) (x == ' ' || x == '\n' || x == '\t')
+
+String & String::trim(void)
+{
+	if (!buffer || len == 0) return *this;
+	char *begin = buffer;
+	while (isspace(*begin)) begin++;
+	char *end = buffer + len - 1;
+	while (isspace(*end) && end >= begin) end--;
+	len = end + 1 - begin;
+	if (begin > buffer) memcpy(buffer, begin, len);
+	buffer[len] = 0;
+	return *this;
+}
+
+

+ 240 - 0
non_catalog_apps/tas_playback/WString.h

@@ -0,0 +1,240 @@
+/*
+  WString.h - String library for Wiring & Arduino
+  ...mostly rewritten by Paul Stoffregen...
+  Copyright (c) 2009-10 Hernando Barragan.  All right reserved.
+  Copyright 2011, Paul Stoffregen, paul@pjrc.com
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef String_class_h
+#define String_class_h
+#ifdef __cplusplus
+
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+// When compiling programs with this class, the following gcc parameters
+// dramatically increase performance and memory (RAM) efficiency, typically
+// with little or no increase in code size.
+//     -felide-constructors
+//     -std=c++0x
+
+// Brian Cook's "no overhead" Flash String type (message on Dec 14, 2010)
+// modified by Mikal Hart for his FlashString library
+class __FlashStringHelper;
+
+// An inherited class for holding the result of a concatenation.  These
+// result objects are assumed to be writable by subsequent concatenations.
+class StringSumHelper;
+
+// The string class
+class String
+{
+public:
+	// constructors
+	String(const char *cstr = (const char *)NULL);
+	String(const __FlashStringHelper *pgmstr);
+	String(const String &str);
+	#if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__)
+	String(String &&rval);
+	String(StringSumHelper &&rval);
+	#endif
+	String(char c);
+	String(unsigned char c);
+	String(int, unsigned char base=10);
+	String(unsigned int, unsigned char base=10);
+	String(long, unsigned char base=10);
+	String(unsigned long, unsigned char base=10);
+	String(long long, unsigned char base=10);
+	String(unsigned long long, unsigned char base=10);
+        String(float num, unsigned char digits=2);
+	String(double num, unsigned char digits=2) : String((float)num, digits) {}
+	~String(void);
+
+	// memory management
+	unsigned char reserve(unsigned int size);
+	inline unsigned int length(void) const {return len;}
+
+	// copy and move
+	String & copy(const char *cstr, unsigned int length);
+	String & copy(const __FlashStringHelper *s) { return copy((const char *)s, strlen((const char *)s)); }
+	void move(String &rhs);
+	String & operator = (const String &rhs);
+	String & operator = (const char *cstr);
+	String & operator = (const __FlashStringHelper *pgmstr);
+	#if __cplusplus >= 201103L || defined(__GXX_EXPERIMENTAL_CXX0X__)
+	String & operator = (String &&rval);
+	String & operator = (StringSumHelper &&rval);
+	#endif
+	String & operator = (char c);
+
+	// append
+	String & append(const String &str);
+	String & append(const char *cstr);
+	String & append(const __FlashStringHelper *s)	{return append((const char *)s, strlen((const char *)s)); }
+	String & append(char c);
+	String & append(unsigned char c)		{return append((int)c);}
+	String & append(int num);
+	String & append(unsigned int num);
+	String & append(long num);
+	String & append(unsigned long num);
+	String & append(long long num);
+	String & append(unsigned long long num);
+	String & append(float num);
+	String & append(double num)			{return append((float)num);}
+	String & operator += (const String &rhs)	{return append(rhs);}
+	String & operator += (const char *cstr)		{return append(cstr);}
+	String & operator += (const __FlashStringHelper *pgmstr) {return append(pgmstr);}
+	String & operator += (char c)			{return append(c);}
+	String & operator += (unsigned char c)		{return append((int)c);}
+	String & operator += (int num)			{return append(num);}
+	String & operator += (unsigned int num)		{return append(num);}
+	String & operator += (long num)			{return append(num);}
+	String & operator += (unsigned long num)	{return append(num);}
+	String & operator += (long long num)		{return append(num);}
+	String & operator += (unsigned long long num)	{return append(num);}
+	String & operator += (float num)		{return append(num);}
+	String & operator += (double num)		{return append(num);}
+
+	// concatenate
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, const String &rhs);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, const char *cstr);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, const __FlashStringHelper *pgmstr);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, char c);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, unsigned char c);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, int num);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, unsigned int num);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, long num);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, unsigned long num);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, long long num);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, unsigned long long num);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, float num);
+	friend StringSumHelper & operator + (const StringSumHelper &lhs, double num);
+	String & concat(const String &str)		{return append(str);}
+	String & concat(const char *cstr)		{return append(cstr);}
+	String & concat(const __FlashStringHelper *pgmstr) {return append(pgmstr);}
+	String & concat(char c)				{return append(c);}
+	String & concat(unsigned char c)		{return append((int)c);}
+	String & concat(int num)			{return append(num);}
+	String & concat(unsigned int num)		{return append(num);}
+	String & concat(long num)			{return append(num);}
+	String & concat(unsigned long num)		{return append(num);}
+	String & concat(long long num)			{return append(num);}
+	String & concat(unsigned long long num)		{return append(num);}
+	String & concat(float num)			{return append(num);}
+	String & concat(double num)			{return append(num);}
+
+	// comparison
+	int compareTo(const String &s) const;
+	unsigned char equals(const String &s) const;
+	unsigned char equals(const char *cstr) const;
+	//unsigned char equals(const __FlashStringHelper *pgmstr) const;
+	unsigned char operator == (const String &rhs) const {return equals(rhs);}
+	unsigned char operator == (const char *cstr) const {return equals(cstr);}
+	unsigned char operator != (const String &rhs) const {return !equals(rhs);}
+	unsigned char operator != (const char *cstr) const {return !equals(cstr);}
+	unsigned char operator <  (const String &rhs) const;
+	unsigned char operator >  (const String &rhs) const;
+	unsigned char operator <= (const String &rhs) const;
+	unsigned char operator >= (const String &rhs) const;
+	unsigned char equalsIgnoreCase(const String &s) const;
+	unsigned char startsWith( const String &prefix) const;
+	unsigned char startsWith(const String &prefix, unsigned int offset) const;
+	unsigned char endsWith(const String &suffix) const;
+
+	// character acccess
+	char charAt(unsigned int index) const;
+	void setCharAt(unsigned int index, char c);
+	char operator [] (unsigned int index) const;
+	char& operator [] (unsigned int index);
+	void getBytes(unsigned char *buf, unsigned int bufsize, unsigned int index=0) const;
+	void toCharArray(char *buf, unsigned int bufsize, unsigned int index=0) const
+		{getBytes((unsigned char *)buf, bufsize, index);}
+	const char * c_str() const {
+		if (!buffer) return &zerotermination; // https://forum.pjrc.com/threads/63842
+		return buffer;
+	}
+	char * begin() {
+		if (!buffer) reserve(20);
+		return buffer;
+	}
+	char * end() { return begin() + length(); }
+	const char * begin() const { return c_str(); }
+	const char * end() const { return c_str() + length(); }
+
+	// search
+	int indexOf( char ch ) const;
+	int indexOf( char ch, unsigned int fromIndex ) const;
+	int indexOf( const String &str ) const;
+	int indexOf( const String &str, unsigned int fromIndex ) const;
+	int lastIndexOf( char ch ) const;
+	int lastIndexOf( char ch, unsigned int fromIndex ) const;
+	int lastIndexOf( const String &str ) const;
+	int lastIndexOf( const String &str, unsigned int fromIndex ) const;
+	String substring( unsigned int beginIndex ) const;
+	String substring( unsigned int beginIndex, unsigned int endIndex ) const;
+
+	// modification
+	String & replace(char find, char replace);
+	String & replace(const String& find, const String& replace);
+	String & remove(unsigned int index);
+	String & remove(unsigned int index, unsigned int count);
+	String & toLowerCase(void);
+	String & toUpperCase(void);
+	String & trim(void);
+
+	// parsing/conversion
+	long toInt(void) const;
+	float toFloat(void) const;
+
+protected:
+	char *buffer;	        // the actual char array
+	unsigned int capacity;  // the array length minus one (for the '\0')
+	unsigned int len;       // the String length (not counting the '\0')
+	unsigned char flags;    // unused, for future features
+protected:
+	void init(void);
+	unsigned char changeBuffer(unsigned int maxStrLen);
+	String & append(const char *cstr, unsigned int length);
+private:
+	// allow for "if (s)" without the complications of an operator bool().
+	// for more information http://www.artima.com/cppsource/safebool.html
+	typedef void (String::*StringIfHelperType)() const;
+	void StringIfHelper() const {}
+	static const char zerotermination;
+public:
+	operator StringIfHelperType() const { return buffer ? &String::StringIfHelper : 0; }
+};
+
+class StringSumHelper : public String
+{
+public:
+	StringSumHelper(const String &s) : String(s) {}
+	StringSumHelper(const char *p) : String(p) {}
+	StringSumHelper(const __FlashStringHelper *pgmstr) : String(pgmstr) {}
+	StringSumHelper(char c) : String(c) {}
+	StringSumHelper(unsigned char c) : String(c) {}
+	StringSumHelper(int num) : String(num, 10) {}
+	StringSumHelper(unsigned int num) : String(num, 10) {}
+	StringSumHelper(long num) : String(num, 10) {}
+	StringSumHelper(unsigned long num) : String(num, 10) {}
+	StringSumHelper(long long num) : String(num, 10) {}
+	StringSumHelper(unsigned long long num) : String(num, 10) {}
+};
+
+#endif  // __cplusplus
+#endif  // String_class_h

+ 15 - 0
non_catalog_apps/tas_playback/application.fam

@@ -0,0 +1,15 @@
+App(
+    appid="tas_playback",  # Must be unique
+    name="TAS playback",  # Displayed in menus
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="tas_playback_app",
+    requires=["gui", "cli", "storage", "dialogs"],
+    stack_size=4 * 1024,
+    fap_category="GPIO",
+    fap_version=(0, 1),  # (major, minor)
+    fap_icon="images/tas_playback.png",  # 10x10 1-bit PNG
+    fap_description="Playback TAS files for Nintendo consoles",
+    fap_author="rcombs",
+    fap_weburl="https://github.com/rcombs/tas-playback",
+    fap_icon_assets="images",  # Image assets to compile for this application
+)

+ 51 - 0
non_catalog_apps/tas_playback/avr_functions.h

@@ -0,0 +1,51 @@
+/* Teensyduino Core Library
+ * http://www.pjrc.com/teensy/
+ * Copyright (c) 2017 PJRC.COM, LLC.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * 2. If the Software is incorporated into a build system that allows
+ * selection among a list of target devices, then similar target
+ * devices manufactured by PJRC.COM must be included in the list of
+ * target devices and selectable in the same manner.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef _avr_functions_h_
+#define _avr_functions_h_
+
+#include <inttypes.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+char * ultoa(unsigned long val, char *buf, int radix);
+char * ltoa(long val, char *buf, int radix);
+char * ulltoa(unsigned long long val, char *buf, int radix);
+char * lltoa(long long val, char *buf, int radix);
+
+char * dtostrf(float val, int width, unsigned int precision, char *buf);
+
+
+#ifdef __cplusplus
+}
+#endif
+#endif

+ 2 - 0
non_catalog_apps/tas_playback/changelog.md

@@ -0,0 +1,2 @@
+0.1:
+- Initial Flipper Zero port

BIN
non_catalog_apps/tas_playback/images/Pin_back_arrow_10x8.png


BIN
non_catalog_apps/tas_playback/images/tas_playback.png


+ 108 - 0
non_catalog_apps/tas_playback/nonstd.c

@@ -0,0 +1,108 @@
+/* Teensyduino Core Library
+ * http://www.pjrc.com/teensy/
+ * Copyright (c) 2017 PJRC.COM, LLC.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * 2. If the Software is incorporated into a build system that allows
+ * selection among a list of target devices, then similar target
+ * devices manufactured by PJRC.COM must be included in the list of
+ * target devices and selectable in the same manner.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <furi.h>
+#include "avr_functions.h"
+
+
+char * ultoa(unsigned long val, char *buf, int radix)
+{
+	unsigned digit;
+	int i=0, j;
+	char t;
+
+	while (1) {
+		digit = val % radix;
+		buf[i] = ((digit < 10) ? '0' + digit : 'A' + digit - 10);
+		val /= radix;
+		if (val == 0) break;
+		i++;
+	}
+	buf[i + 1] = 0;
+	for (j=0; j < i; j++, i--) {
+		t = buf[j];
+		buf[j] = buf[i];
+		buf[i] = t;
+	}
+	return buf;
+}
+
+char * ltoa(long val, char *buf, int radix)
+{
+	if (val >= 0) {
+		return ultoa(val, buf, radix);
+	} else {
+		buf[0] = '-';
+		ultoa(-val, buf + 1, radix);
+		return buf;
+	}
+}
+
+char * ulltoa(unsigned long long val, char *buf, int radix)
+{
+	unsigned digit;
+	int i=0, j;
+	char t;
+
+	while (1) {
+		digit = val % radix;
+		buf[i] = ((digit < 10) ? '0' + digit : 'A' + digit - 10);
+		val /= radix;
+		if (val == 0) break;
+		i++;
+	}
+	buf[i + 1] = 0;
+	for (j=0; j < i; j++, i--) {
+		t = buf[j];
+		buf[j] = buf[i];
+		buf[i] = t;
+	}
+	return buf;
+}
+
+char * lltoa(long long val, char *buf, int radix)
+{
+	if (val >= 0) {
+		return ulltoa(val, buf, radix);
+	} else {
+		buf[0] = '-';
+		ulltoa(-val, buf + 1, radix);
+		return buf;
+	}
+}
+
+#define DTOA_UPPER 0x04
+
+char * dtostrf(float val, int width, unsigned int precision, char *buf)
+{
+  sprintf(buf, "%*.*f", width, (int)precision, (double)val);
+  return buf;
+}
+

BIN
non_catalog_apps/tas_playback/screenshots/file-select.png


BIN
non_catalog_apps/tas_playback/screenshots/running.png


+ 494 - 0
non_catalog_apps/tas_playback/tas_playback.cxx

@@ -0,0 +1,494 @@
+#include <furi.h>
+#include <cli/cli.h>
+#include <cli/cli_vcp.h>
+#include <dialogs/dialogs.h>
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include <storage/storage.h>
+#include <furi_hal_resources.h>
+#include <furi_hal_light.h>
+#include <furi_hal_console.h>
+#include <math.h>
+
+/* generated by fbt from .png files in images folder */
+#include <tas_playback_icons.h>
+
+#include "WString.h"
+
+typedef enum {
+    O_READ = FSAM_READ,
+    O_WRITE = FSAM_WRITE,
+    O_CREAT = ((uint16_t)FSOM_OPEN_ALWAYS << 8),
+    O_APPEND = ((uint16_t)FSOM_OPEN_APPEND << 8),
+    O_EXCL = ((uint16_t)FSOM_CREATE_NEW << 8),
+    O_TRUNC = ((uint16_t)FSOM_CREATE_ALWAYS << 8),
+} FileOpenMode;
+
+#define SD_CARD_ERROR_ACMD41 0x16
+
+class SdFs {
+public:
+    bool begin(int) {
+        m_storage = (Storage*)furi_record_open(RECORD_STORAGE);
+
+        return m_storage != nullptr;
+    }
+
+    bool mkdir(const char* path) {
+        return storage_simply_mkdir(m_storage, path);
+    }
+
+    ~SdFs() {
+        if(m_storage) furi_record_close(RECORD_STORAGE);
+    }
+
+    uint8_t sdErrorCode() const {
+        return 0;
+    }
+    uint8_t sdErrorData() const {
+        return 0;
+    }
+
+private:
+    Storage* m_storage = nullptr;
+
+    friend class SdFile;
+};
+
+template <class T>
+static inline void printSdErrorSymbol(T*, uint8_t) {
+}
+
+class SdFile {
+public:
+    ~SdFile() {
+        if(m_file) storage_file_free(m_file);
+    }
+
+    void close() {
+        if(m_file) storage_file_close(m_file);
+    }
+
+    bool open(SdFs* from, const char* path, uint16_t mode) {
+        m_fs = from;
+
+        initFile();
+
+        m_path = path;
+
+        return storage_file_open(
+            m_file, path, FS_AccessMode((uint16_t)mode & 0xFF), FS_OpenMode((uint16_t)mode >> 8));
+    }
+
+    bool open(SdFs* from, const char* path, FileOpenMode mode) {
+        return open(from, path, (uint16_t)mode);
+    }
+
+    void getName(char* dest, uint16_t size) {
+        m_path.toCharArray(dest, size);
+    }
+
+    unsigned read(void* buff, int bytes_to_read) {
+        return storage_file_read(m_file, buff, bytes_to_read);
+    }
+
+    unsigned write(const void* buff, int bytes_to_read) {
+        return storage_file_write(m_file, buff, bytes_to_read);
+    }
+
+    bool available() {
+        return !storage_file_eof(m_file);
+    }
+
+    uint64_t fileSize() {
+        return storage_file_size(m_file);
+    }
+
+    uint64_t curPosition() {
+        return storage_file_tell(m_file);
+    }
+
+    bool seekSet(uint32_t pos) {
+        return storage_file_seek(m_file, pos, true);
+    }
+
+    bool isOpen() {
+        return m_file && storage_file_is_open(m_file);
+    }
+
+    bool openNext(SdFile* from) {
+        char name[64];
+
+        bool ret = storage_dir_read(from->m_file, NULL, name, sizeof(name));
+        if(!ret) return false;
+
+        String path = from->m_path;
+        path += "/";
+        path += name;
+
+        return open(from->m_fs, path.c_str(), O_READ);
+    }
+
+    bool rewind() {
+        return false;
+        //    return storage_dir_rewind(m_file);
+    }
+
+private:
+    void initFile() {
+        close();
+
+        if(!m_file) m_file = storage_file_alloc(m_fs->m_storage);
+    }
+
+    File* m_file = nullptr;
+    SdFs* m_fs = nullptr;
+    String m_path;
+};
+
+#define F(x) x
+
+#define noInterrupts() uint32_t intLevel = ulPortRaiseBASEPRI()
+#define interrupts() vPortSetBASEPRI(intLevel)
+
+#define F_CPU /*SystemCoreClock*/ 64000000
+
+typedef enum { HEX = 16 } BaseType;
+
+class SerialObj {
+public:
+    template <class... T>
+    void print(const T&... v) {
+        String out(v...);
+        furi_hal_console_tx((uint8_t*)out.c_str(), out.length());
+    }
+
+    void println() {
+        furi_hal_console_tx((uint8_t*)"\r\n", 2);
+    }
+    template <class... T>
+    void println(const T&... v) {
+        String out(v...);
+        furi_hal_console_tx((uint8_t*)out.c_str(), out.length());
+        println();
+    }
+
+    template <class... T>
+    void write(const T&... v) {
+        print(v...);
+    }
+
+    int read() {
+        return -1;
+        /*
+    uint8_t buf;
+    if (!cli_read_timeout(m_cli, &buf, 1, 0))
+      return -1;
+    return buf;*/
+    }
+
+    bool begin() {
+        /*
+    m_cli = (Cli*)furi_record_open(RECORD_CLI);
+
+//    if (m_cli)
+  //    cli_session_open(m_cli, &cli_vcp);
+    */
+
+        return (bool)*this;
+    }
+
+    ~SerialObj() {
+        if(m_cli) furi_record_close(RECORD_CLI);
+    }
+
+    explicit operator bool() {
+        return true; // m_cli && cli_is_connected(m_cli);
+    }
+
+    void flush() {
+    }
+
+private:
+    Cli* m_cli = nullptr;
+};
+
+#define SERIAL_BAUD_RATE
+
+#define BUILTIN_SDCARD 0
+
+#define NO_CONSOLE_GPIO
+
+#define ARM_DWT_CYCCNT DWT->CYCCNT
+
+SerialObj Serial;
+
+typedef void (*ArduinoISR)();
+
+void gpioCallback(void* ctx) {
+    ArduinoISR cb = (ArduinoISR)ctx;
+    cb();
+}
+
+#define LOW false
+#define HIGH true
+
+typedef enum {
+    INPUT = GpioModeInput,
+    OUTPUT = GpioModeOutputOpenDrain,
+    INPUT_PULLUP = (uint16_t)GpioModeInput | ((uint16_t)GpioPullUp << 8),
+    INPUT_PULLDOWN = (uint16_t)GpioModeInput | ((uint16_t)GpioPullDown << 8),
+    FALLING = (uint16_t)GpioModeInterruptFall | ((uint16_t)GpioPullUp << 8),
+} PinMode;
+
+void pinMode(const GpioPin* pin, PinMode mode) {
+    furi_hal_gpio_init(
+        pin, GpioMode((uint16_t)mode & 0xFF), GpioPull((uint16_t)mode >> 8), GpioSpeedVeryHigh);
+}
+
+void attachInterrupt(const GpioPin* pin, ArduinoISR cb, PinMode mode) {
+    pinMode(pin, mode);
+    furi_hal_gpio_remove_int_callback(pin);
+    furi_hal_gpio_add_int_callback(pin, gpioCallback, (void*)cb);
+}
+
+static inline void digitalWrite(const GpioPin* pin, bool state) {
+    furi_hal_gpio_write(pin, state);
+}
+
+static inline bool digitalRead(const GpioPin* pin) {
+    return furi_hal_gpio_read(pin);
+}
+
+#define N64_PIN &gpio_ext_pc0
+#define N64_PIN_PREFIX PC0
+#define CONCAT1(a, b) a##b
+#define CONCAT(a, b) CONCAT1(a, b)
+#define N64_PORT CONCAT(N64_PIN_PREFIX, _GPIO_Port)
+#define N64_Pin CONCAT(N64_PIN_PREFIX, _Pin)
+#define N64_IRQ EXTI0_IRQn
+#define N64_PIN_LINE LL_SYSCFG_EXTI_LINE0
+
+#define DISABLE_N64_INTERRUPT() LL_EXTI_DisableFallingTrig_0_31(N64_PIN_LINE)
+#define ENABLE_N64_INTERRUPT() LL_EXTI_EnableFallingTrig_0_31(N64_PIN_LINE)
+
+#define N64_HIGH LL_GPIO_SetPinMode((N64_PIN)->port, (N64_PIN)->pin, LL_GPIO_MODE_INPUT)
+#define N64_LOW LL_GPIO_SetPinMode((N64_PIN)->port, (N64_PIN)->pin, LL_GPIO_MODE_OUTPUT)
+#define N64_QUERY (N64_PORT->IDR & N64_Pin)
+
+#define LED_HIGH (void)0; //furi_hal_light_set(LightGreen, 0xFF)
+#define LED_LOW (void)0; //furi_hal_light_set(LightGreen, 0x00)
+
+template <class T1, class T2>
+auto min(T1 x, T2 y) {
+    return x < y ? x : y;
+}
+
+//#define USE_COMPARE_ABSOLUTE
+
+FuriMessageQueue* msg_queue;
+
+Gui* gui = nullptr;
+ViewPort* view_port = nullptr;
+
+typedef struct {
+    enum {
+        Input,
+        ISRLog,
+    } msg_type;
+    union {
+        InputEvent input;
+    };
+} QueueMessage;
+
+void isrLogCb() {
+    QueueMessage msg = {QueueMessage::ISRLog, {}};
+    furi_message_queue_put(msg_queue, &msg, 0);
+}
+
+#define ISR_LOG_CB isrLogCb
+
+unsigned char frameDat[4] = {0};
+
+void frameCb(const unsigned char* dat, size_t count) {
+    memcpy(frameDat, dat, count);
+}
+
+#define FRAME_CB frameCb
+
+#include "teensy/teensy.ino"
+
+static void input_callback(InputEvent* input_event, void* ctx) {
+    (void)ctx;
+    QueueMessage msg = {QueueMessage::Input, {.input = *input_event}};
+    furi_message_queue_put(msg_queue, &msg, 0);
+}
+
+static void render_callback(Canvas* canvas, void* ctx) {
+    (void)ctx;
+
+    elements_text_box(canvas, 0, 0, 128, 10, AlignCenter, AlignTop, filename.c_str(), true);
+
+#define INVERT "\e!"
+
+    static char inputStr[128] = "";
+
+#define GET_BIT(byte, bit) (frameDat[byte] & (1 << bit))
+#define INV_IF(x) ((x) ? INVERT : "")
+#define FMT_BTN(byte, bit) INV_IF(GET_BIT(byte, bit)), INV_IF(GET_BIT(byte, bit))
+#define PLUS_MIN_ZERO(x) (((x) == 0) ? '0' : (((x) > 127) ? '-' : '+'))
+#define UN_SIGN(x) (((x) <= 127) ? x : (-((int)(x)-256)))
+#define FMT_JOY(byte)                                                                    \
+    INV_IF(frameDat[byte] != 0), PLUS_MIN_ZERO(frameDat[byte]), UN_SIGN(frameDat[byte]), \
+        INV_IF(frameDat[byte] != 0)
+
+    if(console == N64)
+        snprintf(
+            inputStr,
+            sizeof(inputStr),
+            "\e*\e!X\e!:%s%c%02x%s \e!Y\e!:%s%c%02x%s %s<%s%s>%s%s^%s%sv%s\n%sA%s%sB%s%sZ%s%sS%s%sL%s%sR%s C(%s<%s%s>%s%s^%s%sv%s)",
+            FMT_JOY(2),
+            FMT_JOY(3),
+            FMT_BTN(0, 1),
+            FMT_BTN(0, 0),
+            FMT_BTN(0, 3),
+            FMT_BTN(0, 2),
+            FMT_BTN(0, 7),
+            FMT_BTN(0, 6),
+            FMT_BTN(0, 5),
+            FMT_BTN(0, 4),
+            FMT_BTN(1, 5),
+            FMT_BTN(1, 4),
+            FMT_BTN(1, 1),
+            FMT_BTN(1, 0),
+            FMT_BTN(1, 3),
+            FMT_BTN(1, 2));
+
+    elements_text_box(canvas, 0, 15, 128, 32, AlignCenter, AlignTop, inputStr, false);
+
+    int denom = numFrames ? numFrames : 1;
+    float progress = (float)curFrame / denom;
+    char progStr[32];
+    snprintf(
+        progStr, sizeof(progStr), "%07lu/%07lu:%03i%%", curFrame, numFrames, (int)(progress * 100));
+
+    uint8_t progress_y = 41;
+    uint8_t progress_h = 12;
+
+    uint8_t progress_length = roundf(progress * (126));
+    canvas_set_color(canvas, ColorWhite);
+    canvas_draw_box(canvas, 1, progress_y + 1, 126, progress_h - 2);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_rframe(canvas, 0, progress_y, 128, progress_h, 3);
+    canvas_draw_box(canvas, 1, progress_y + 1, progress_length, progress_h - 2);
+
+    canvas_set_color(canvas, ColorXOR);
+    canvas_set_font(canvas, FontKeyboard);
+    canvas_draw_str_aligned(canvas, 64, progress_y + 2, AlignCenter, AlignTop, progStr);
+
+    canvas_draw_icon(canvas, 0, 54, &I_Pin_back_arrow_10x8);
+    canvas_set_font(canvas, FontSecondary);
+    elements_multiline_text_aligned(canvas, 13, 62, AlignLeft, AlignBottom, "Hold to exit");
+}
+
+extern "C" int32_t tas_playback_app(const char* p) {
+    FURI_LOG_I("TAS", "Hello world");
+    FURI_LOG_I("TAS", "I'm tas_playback!");
+
+    uint32_t oldPrio = NVIC_GetPriority(N64_IRQ);
+    NVIC_SetPriority(N64_IRQ, 0);
+
+    LL_GPIO_SetPinOutputType((N64_PIN)->port, (N64_PIN)->pin, LL_GPIO_OUTPUT_PUSHPULL);
+
+    setup();
+
+    FURI_LOG_I("TAS", "SETUP DONE");
+
+    msg_queue = furi_message_queue_alloc(8, sizeof(QueueMessage));
+
+    gui = (Gui*)furi_record_open(RECORD_GUI);
+
+    view_port = view_port_alloc();
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    view_port_draw_callback_set(view_port, render_callback, NULL);
+    view_port_input_callback_set(view_port, input_callback, NULL);
+
+    FuriString* file_path = furi_string_alloc();
+
+    FURI_LOG_I("TAS", "STARTING LOOP");
+
+    do {
+        if(p && strlen(p)) {
+            FURI_LOG_I("TAS", "GOT ARG");
+            furi_string_set_str(file_path, (const char*)p);
+        } else {
+            FURI_LOG_I("TAS", "NO ARG");
+
+            Storage* storage = (Storage*)furi_record_open(RECORD_STORAGE);
+            storage_common_migrate(storage, EXT_PATH("tas"), STORAGE_APP_DATA_PATH_PREFIX);
+            furi_record_close(RECORD_STORAGE);
+
+            FURI_LOG_I("TAS", "MIGRATED");
+
+            furi_string_set_str(file_path, STORAGE_APP_DATA_PATH_PREFIX);
+
+            DialogsFileBrowserOptions browser_options;
+            dialog_file_browser_set_basic_options(&browser_options, "m64", &I_tas_playback);
+            browser_options.hide_ext = false;
+            browser_options.base_path = STORAGE_APP_DATA_PATH_PREFIX;
+
+            FURI_LOG_I("TAS", "SETUP OPTIONS");
+
+            DialogsApp* dialogs = (DialogsApp*)furi_record_open(RECORD_DIALOGS);
+            bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options);
+
+            furi_record_close(RECORD_DIALOGS);
+
+            if(!res) {
+                FURI_LOG_E("TAS", "No file selected");
+                break;
+            }
+        }
+
+        if(!openTAS(furi_string_get_cstr(file_path))) continue;
+
+        FURI_LOG_E("TAS", "Got file");
+
+        QueueMessage msg;
+        while(furi_message_queue_get(msg_queue, &msg, FuriWaitForever) == FuriStatusOk) {
+            if(msg.msg_type == QueueMessage::Input) {
+                if(msg.input.type == InputTypeLong) {
+                    if(msg.input.key == InputKeyBack) break;
+                } else if(msg.input.type == InputTypeShort) {
+                    if(msg.input.key == InputKeyUp) {
+                    } else if(msg.input.key == InputKeyDown) {
+                    }
+                }
+            }
+
+            view_port_update(view_port);
+
+            loop();
+        }
+
+        view_port_update(view_port);
+
+        if(p && strlen(p)) break; // Exit instead of going to browser if launched with arg
+    } while(1);
+
+    furi_string_free(file_path);
+
+    gui_remove_view_port(gui, view_port);
+    furi_record_close(RECORD_GUI);
+    view_port_free(view_port);
+
+    furi_message_queue_free(msg_queue);
+
+    furi_hal_gpio_init(N64_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+    furi_hal_gpio_remove_int_callback(N64_PIN);
+
+    NVIC_SetPriority(N64_IRQ, oldPrio);
+
+    return 0;
+}

+ 264 - 0
non_catalog_apps/tas_playback/teensy/crc_table.h

@@ -0,0 +1,264 @@
+/**
+ * This CRC table for repeating bytes is take from
+ * the cube64 project
+ *  http://cia.vc/stats/project/navi-misc/cube64
+ */
+unsigned char crc_repeating_table[] = {
+          0xFF, // 0x00
+    0x14, // 0x01
+    0xAC, // 0x02
+    0x47, // 0x03
+    0x59, // 0x04
+    0xB2, // 0x05
+    0x0A, // 0x06
+    0xE1, // 0x07
+    0x36, // 0x08
+    0xDD, // 0x09
+    0x65, // 0x0A
+    0x8E, // 0x0B
+    0x90, // 0x0C
+    0x7B, // 0x0D
+    0xC3, // 0x0E
+    0x28, // 0x0F
+    0xE8, // 0x10
+    0x03, // 0x11
+    0xBB, // 0x12
+    0x50, // 0x13
+    0x4E, // 0x14
+    0xA5, // 0x15
+    0x1D, // 0x16
+    0xF6, // 0x17
+    0x21, // 0x18
+    0xCA, // 0x19
+    0x72, // 0x1A
+    0x99, // 0x1B
+    0x87, // 0x1C
+    0x6C, // 0x1D
+    0xD4, // 0x1E
+    0x3F, // 0x1F
+    0xD1, // 0x20
+    0x3A, // 0x21
+    0x82, // 0x22
+    0x69, // 0x23
+    0x77, // 0x24
+    0x9C, // 0x25
+    0x24, // 0x26
+    0xCF, // 0x27
+    0x18, // 0x28
+    0xF3, // 0x29
+    0x4B, // 0x2A
+    0xA0, // 0x2B
+    0xBE, // 0x2C
+    0x55, // 0x2D
+    0xED, // 0x2E
+    0x06, // 0x2F
+    0xC6, // 0x30
+    0x2D, // 0x31
+    0x95, // 0x32
+    0x7E, // 0x33
+    0x60, // 0x34
+    0x8B, // 0x35
+    0x33, // 0x36
+    0xD8, // 0x37
+    0x0F, // 0x38
+    0xE4, // 0x39
+    0x5C, // 0x3A
+    0xB7, // 0x3B
+    0xA9, // 0x3C
+    0x42, // 0x3D
+    0xFA, // 0x3E
+    0x11, // 0x3F
+    0xA3, // 0x40
+    0x48, // 0x41
+    0xF0, // 0x42
+    0x1B, // 0x43
+    0x05, // 0x44
+    0xEE, // 0x45
+    0x56, // 0x46
+    0xBD, // 0x47
+    0x6A, // 0x48
+    0x81, // 0x49
+    0x39, // 0x4A
+    0xD2, // 0x4B
+    0xCC, // 0x4C
+    0x27, // 0x4D
+    0x9F, // 0x4E
+    0x74, // 0x4F
+    0xB4, // 0x50
+    0x5F, // 0x51
+    0xE7, // 0x52
+    0x0C, // 0x53
+    0x12, // 0x54
+    0xF9, // 0x55
+    0x41, // 0x56
+    0xAA, // 0x57
+    0x7D, // 0x58
+    0x96, // 0x59
+    0x2E, // 0x5A
+    0xC5, // 0x5B
+    0xDB, // 0x5C
+    0x30, // 0x5D
+    0x88, // 0x5E
+    0x63, // 0x5F
+    0x8D, // 0x60
+    0x66, // 0x61
+    0xDE, // 0x62
+    0x35, // 0x63
+    0x2B, // 0x64
+    0xC0, // 0x65
+    0x78, // 0x66
+    0x93, // 0x67
+    0x44, // 0x68
+    0xAF, // 0x69
+    0x17, // 0x6A
+    0xFC, // 0x6B
+    0xE2, // 0x6C
+    0x09, // 0x6D
+    0xB1, // 0x6E
+    0x5A, // 0x6F
+    0x9A, // 0x70
+    0x71, // 0x71
+    0xC9, // 0x72
+    0x22, // 0x73
+    0x3C, // 0x74
+    0xD7, // 0x75
+    0x6F, // 0x76
+    0x84, // 0x77
+    0x53, // 0x78
+    0xB8, // 0x79
+    0x00, // 0x7A
+    0xEB, // 0x7B
+    0xF5, // 0x7C
+    0x1E, // 0x7D
+    0xA6, // 0x7E
+    0x4D, // 0x7F
+    0x47, // 0x80
+    0xAC, // 0x81
+    0x14, // 0x82
+    0xFF, // 0x83
+    0xE1, // 0x84
+    0x0A, // 0x85
+    0xB2, // 0x86
+    0x59, // 0x87
+    0x8E, // 0x88
+    0x65, // 0x89
+    0xDD, // 0x8A
+    0x36, // 0x8B
+    0x28, // 0x8C
+    0xC3, // 0x8D
+    0x7B, // 0x8E
+    0x90, // 0x8F
+    0x50, // 0x90
+    0xBB, // 0x91
+    0x03, // 0x92
+    0xE8, // 0x93
+    0xF6, // 0x94
+    0x1D, // 0x95
+    0xA5, // 0x96
+    0x4E, // 0x97
+    0x99, // 0x98
+    0x72, // 0x99
+    0xCA, // 0x9A
+    0x21, // 0x9B
+    0x3F, // 0x9C
+    0xD4, // 0x9D
+    0x6C, // 0x9E
+    0x87, // 0x9F
+    0x69, // 0xA0
+    0x82, // 0xA1
+    0x3A, // 0xA2
+    0xD1, // 0xA3
+    0xCF, // 0xA4
+    0x24, // 0xA5
+    0x9C, // 0xA6
+    0x77, // 0xA7
+    0xA0, // 0xA8
+    0x4B, // 0xA9
+    0xF3, // 0xAA
+    0x18, // 0xAB
+    0x06, // 0xAC
+    0xED, // 0xAD
+    0x55, // 0xAE
+    0xBE, // 0xAF
+    0x7E, // 0xB0
+    0x95, // 0xB1
+    0x2D, // 0xB2
+    0xC6, // 0xB3
+    0xD8, // 0xB4
+    0x33, // 0xB5
+    0x8B, // 0xB6
+    0x60, // 0xB7
+    0xB7, // 0xB8
+    0x5C, // 0xB9
+    0xE4, // 0xBA
+    0x0F, // 0xBB
+    0x11, // 0xBC
+    0xFA, // 0xBD
+    0x42, // 0xBE
+    0xA9, // 0xBF
+    0x1B, // 0xC0
+    0xF0, // 0xC1
+    0x48, // 0xC2
+    0xA3, // 0xC3
+    0xBD, // 0xC4
+    0x56, // 0xC5
+    0xEE, // 0xC6
+    0x05, // 0xC7
+    0xD2, // 0xC8
+    0x39, // 0xC9
+    0x81, // 0xCA
+    0x6A, // 0xCB
+    0x74, // 0xCC
+    0x9F, // 0xCD
+    0x27, // 0xCE
+    0xCC, // 0xCF
+    0x0C, // 0xD0
+    0xE7, // 0xD1
+    0x5F, // 0xD2
+    0xB4, // 0xD3
+    0xAA, // 0xD4
+    0x41, // 0xD5
+    0xF9, // 0xD6
+    0x12, // 0xD7
+    0xC5, // 0xD8
+    0x2E, // 0xD9
+    0x96, // 0xDA
+    0x7D, // 0xDB
+    0x63, // 0xDC
+    0x88, // 0xDD
+    0x30, // 0xDE
+    0xDB, // 0xDF
+    0x35, // 0xE0
+    0xDE, // 0xE1
+    0x66, // 0xE2
+    0x8D, // 0xE3
+    0x93, // 0xE4
+    0x78, // 0xE5
+    0xC0, // 0xE6
+    0x2B, // 0xE7
+    0xFC, // 0xE8
+    0x17, // 0xE9
+    0xAF, // 0xEA
+    0x44, // 0xEB
+    0x5A, // 0xEC
+    0xB1, // 0xED
+    0x09, // 0xEE
+    0xE2, // 0xEF
+    0x22, // 0xF0
+    0xC9, // 0xF1
+    0x71, // 0xF2
+    0x9A, // 0xF3
+    0x84, // 0xF4
+    0x6F, // 0xF5
+    0xD7, // 0xF6
+    0x3C, // 0xF7
+    0xEB, // 0xF8
+    0x00, // 0xF9
+    0xB8, // 0xFA
+    0x53, // 0xFB
+    0x4D, // 0xFC
+    0xA6, // 0xFD
+    0x1E, // 0xFE
+    0xF5 // 0xFF
+};
+

+ 1295 - 0
non_catalog_apps/tas_playback/teensy/teensy.ino

@@ -0,0 +1,1295 @@
+/*
+ * Copyright (c) 2009 Andrew Brown
+ * Copyright (c) 2018 rcombs
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "crc_table.h"
+
+#ifdef TEENSYDUINO
+#include <BufferedPrint.h>
+#include <FreeStack.h>
+#include <RingBuf.h>
+#include <SdFat.h>
+#include <sdios.h>
+#include <SdFatConfig.h>
+#include <MinimumSerial.h>
+#include <DMAChannel.h>
+#include <EEPROM.h>
+
+#ifndef PIT_LTMR64H
+#define PIT_LTMR64H             (*(volatile uint32_t *)0x400370E0) // PIT Upper Lifetime Timer Register
+#define PIT_LTMR64L             (*(volatile uint32_t *)0x400370E4) // PIT Lower Lifetime Timer Register
+#endif
+
+#define SD_CONFIG SdSpiConfig(SDCARD_SS_PIN, SHARED_SPI, SD_SCK_MHZ(50))
+
+#define STATUS_PIN 13
+#define POWER_PIN 32
+#define VI_PIN 35
+#define FIELD_PIN 36
+#define HAVE_EEPROM
+#define HAVE_GPIO_COMMANDS
+
+#define SERIAL_BAUD_RATE 115200
+
+#define N64_PIN 38
+#define SNES_LATCH_PIN 3
+#define SNES_CLOCK_PIN 4
+#define SNES_DATA_PIN 5
+
+#define N64_HIGH (CORE_PIN38_DDRREG &= ~CORE_PIN38_BITMASK) //digitalWriteFast(N64_PIN, HIGH)
+#define N64_LOW (CORE_PIN38_DDRREG |= CORE_PIN38_BITMASK) //digitalWriteFast(N64_PIN, LOW)
+#define N64_QUERY ((CORE_PIN38_PINREG & CORE_PIN38_BITMASK) ? 1 : 0) //digitalReadFast(N64_PIN)
+
+#define LED_HIGH (CORE_PIN13_PORTSET = CORE_PIN13_BITMASK) //digitalWriteFast(STATUS_PIN, HIGH)
+#define LED_LOW (CORE_PIN13_PORTCLEAR = CORE_PIN13_BITMASK) //digitalWriteFast(STATUS_PIN, LOW)
+
+#define SERIAL_TIMEOUT_US 5000000
+
+#define DISABLE_N64_INTERRUPT() \
+  volatile uint32_t *config = portConfigRegister(N64_PIN); \
+  uint32_t oldConfig = *config; \
+  *config &= ~0x000F0000;
+#define ENABLE_N64_INTERRUPT() \
+  *config = oldConfig;
+#endif
+
+#define INPUT_BUFFER_SIZE 2048 // Multiples of 512 are ideal since we can read 256*4/2 = 512 bytes at once.
+// 512 bytes is an optimization for reading the sd card and skips using another buffer.
+
+#define INPUT_BUFFER_UPDATE_TIMEOUT 10 // 10 ms
+
+#define MICRO_CYCLES (F_CPU / 1000000)
+#ifdef F_BUS
+#define MICRO_BUS_CYCLES (F_BUS / 1000000ULL)
+#endif
+
+#define MAX_CMD_LEN 4096
+#define MAX_LOOP_LEN 10240
+
+typedef enum Console {
+  N64 = 0,
+  NES = 1,
+  SNES = 2,
+} Console;
+
+static Console console = N64;
+
+static bool skippingInput = false;
+static String inputString;
+
+static char n64_raw_dump[36]; // maximum recv is 1+2+32 bytes + 1 bit
+// n64_raw_dump does not include the command byte. That gets pushed into
+// n64_command:
+static int n64_command;
+// bytes to send to the 64
+// maximum we'll need to send is 33, 32 for a read request and 1 CRC byte
+static unsigned char output_buffer[33];
+
+// Simple switch buffer. (If buffer A fails to load while buffer B is in use,
+// we still okay, and will try again next loop)
+static unsigned char inputBuffer[INPUT_BUFFER_SIZE];
+static size_t bufferEndPos;
+static volatile size_t bufferPos;
+static volatile bool bufferHasData;
+static void updateInputBuffer();
+
+static bool hold = false;
+static bool finished = false;
+
+static SdFile tasFile;
+
+//static unsigned int progressPos = 0;
+static String filename;
+static unsigned long numFrames = 0, curFrame = 0;
+static int edgesRead = 0;
+static int incompleteCommand = 0;
+static int firstEdgeTime = 0;
+static int secondEdgeTime = 0;
+static volatile unsigned long viCount = 0;
+
+static SdFs sd;
+
+#ifdef SNES_DATA_PIN
+static uint16_t currentSnesFrame;
+static uint32_t frameDuration = 0;
+#endif
+
+static uint64_t finalTime = 0;
+static uint64_t lastFrameTime = 0;
+static int doLoop = 0;
+
+#define N_SER_BUFS 16
+#define SER_BUF_SIZE 256
+static char serBuffer[N_SER_BUFS][SER_BUF_SIZE] = {0};
+static volatile bool bufHasData[N_SER_BUFS] = {0};
+
+static void n64Interrupt();
+
+template<class T> void lockedPrintln(const T& input)
+{
+  noInterrupts();
+  Serial.println(input);
+  interrupts();
+}
+
+template<class A, class B> void lockedPrintln(const A& a, const B& b)
+{
+  noInterrupts();
+  Serial.print(a);
+  Serial.println(b);
+  interrupts();
+}
+
+#ifdef ARM_DWT_CYCCNT
+
+static void startTimer()
+{
+  ARM_DWT_CYCCNT = 0;
+}
+
+static uint32_t readTimer()
+{
+  return ARM_DWT_CYCCNT;
+}
+
+static uint32_t readAndResetTimer()
+{
+  return __atomic_exchange_n(&ARM_DWT_CYCCNT, 0, __ATOMIC_SEQ_CST);
+}
+
+#endif
+
+#ifdef PIT_LTMR64H
+static bool timer64Started = false;
+
+static void start64Timer()
+{
+  // turn on PIT
+  SIM_SCGC6 |= SIM_SCGC6_PIT;
+  __asm__ volatile("nop"); // solves timing problem on Teensy 3.5
+  PIT_MCR = 0x00;
+
+  // Timer 1
+  PIT_TCTRL1 = 0x0; // disable timer 1 and its interrupts
+  PIT_LDVAL1 = 0xFFFFFFFF; // setup timer 1 for maximum counting period
+  PIT_TCTRL1 |= PIT_TCTRL_CHN; // chain timer 1 to timer 0
+  PIT_TCTRL1 |= PIT_TCTRL_TEN; // start timer 1
+
+  // Timer 0
+  PIT_TCTRL0 = 0; // disable timer 0 and its interrupts
+  PIT_LDVAL0 = 0xFFFFFFFF; // setup timer 0 for maximum counting period
+  PIT_TCTRL0 = PIT_TCTRL_TEN; // start timer 0
+  timer64Started = true;
+}
+
+static uint64_t read64Timer()
+{
+  if (!timer64Started)
+    return 0;
+
+  uint64_t current_uptime = (uint64_t)PIT_LTMR64H << 32;
+  current_uptime = current_uptime + PIT_LTMR64L;
+  return 0xffffffffffffffffull - current_uptime;
+}
+#endif
+
+
+static size_t getNextRegion(size_t& size) {
+  size_t currentPos = bufferPos;
+  if ((currentPos == bufferEndPos || (currentPos == 0 && bufferEndPos == INPUT_BUFFER_SIZE)) && bufferHasData) {
+    size = 0;
+    return 0;
+  }
+  size_t ret = bufferEndPos;
+  if (ret == INPUT_BUFFER_SIZE)
+    ret = 0;
+  size = INPUT_BUFFER_SIZE - ret;
+  if (ret < currentPos)
+    size = currentPos - ret;
+  return ret;
+}
+
+void initBuffers()
+{
+  bufferEndPos = 0;
+  bufferPos = 0;
+  bufferHasData = false;
+}
+
+static bool overflowed = 0;
+
+void logFromISR(const char *fmt, ...)
+{
+  int i;
+  for (i = 0; i < N_SER_BUFS && bufHasData[i]; i++);
+  if (i >= N_SER_BUFS) {
+    overflowed = 1;
+    return;
+  }
+
+  char* buf = &serBuffer[i][0];
+  size_t size = SER_BUF_SIZE - 1;
+
+  va_list ap;
+  va_start(ap, fmt);
+  vsnprintf(buf, size, fmt, ap);
+  va_end(ap);
+
+  bufHasData[i] = 1;
+  overflowed = 0;
+
+#ifdef ISR_LOG_CB
+  ISR_LOG_CB();
+#endif
+}
+
+static void advanceBuffer(long bytes)
+{
+  curFrame++;
+
+#ifdef PIT_LTMR64H
+  lastFrameTime = read64Timer();
+#endif
+
+  if (curFrame == numFrames) {
+    logFromISR("C:%f",
+#ifdef MICRO_BUS_CYCLES
+        lastFrameTime / (double)(MICRO_BUS_CYCLES * 1000 * 1000)
+#else
+        double(0.)
+#endif
+    );
+    finished = true;
+    finalTime = lastFrameTime;
+  }
+
+  size_t pos = bufferPos + bytes;
+  if (pos == bufferEndPos) {
+    if (hold)
+      return;
+    bufferHasData = false;
+    bufferPos = pos;
+    return;
+  }
+  if (pos >= INPUT_BUFFER_SIZE)
+      pos = 0;
+  bufferPos = pos;
+}
+
+static size_t appendInputs(const String& data)
+{
+  size_t totalWritten = 0;
+  size_t writeLen = 0;
+  do {
+    size_t dataLeft = data.length() - totalWritten;
+    size_t writePos = getNextRegion(writeLen);
+    if (writeLen > dataLeft)
+      writeLen = dataLeft;
+    if (writeLen) {
+      memcpy(inputBuffer + writePos, data.c_str() + totalWritten, writeLen);
+      totalWritten += writeLen;
+      noInterrupts();
+      finished = false;
+      bufferEndPos = writePos + writeLen;
+      bufferHasData = true;
+      interrupts();
+    }
+  } while (writeLen > 0);
+  return totalWritten;
+}
+
+static void emitList(const String& path)
+{
+  SdFile dir;
+  if (!dir.open(&sd, path.c_str(), O_READ)) {
+    Serial.println(F("E:Failed to open requested directory"));
+    return;
+  }
+  dir.rewind();
+  SdFile listFile;
+  while (listFile.openNext(&dir)) {
+    char name[256];
+    listFile.getName(name, sizeof(name));
+    lockedPrintln("A:", name);
+    listFile.close();
+  }
+  dir.close();
+}
+
+void logFrame(const unsigned char *dat, size_t count, size_t num)
+{
+#define LOG_FRAME_START "F:%u %f %lu"
+#define LOG_FRAME_BYTE  " %hhx"
+#define LOG_FRAME_END   " %lu %i %s"
+
+#ifdef PIT_LTMR64H
+#define LOG_TIME (read64Timer() / (double)(MICRO_BUS_CYCLES * 1000 * 1000))
+#else
+#define LOG_TIME (double)0.
+#endif
+
+#define LOG_FRAME_START_ARGS num, LOG_TIME, viCount
+#define LOG_FRAME_END_ARGS numFrames, overflowed, filename.c_str()
+
+  if (count == 4)
+    logFromISR(LOG_FRAME_START LOG_FRAME_BYTE LOG_FRAME_BYTE LOG_FRAME_BYTE LOG_FRAME_BYTE LOG_FRAME_END,
+               LOG_FRAME_START_ARGS, dat[0], dat[1], dat[2], dat[3], LOG_FRAME_END_ARGS);
+  else if (count == 2)
+    logFromISR(LOG_FRAME_START LOG_FRAME_BYTE LOG_FRAME_BYTE LOG_FRAME_END,
+               LOG_FRAME_START_ARGS, dat[0], dat[1], LOG_FRAME_END_ARGS);
+  else if (count == 1)
+    logFromISR(LOG_FRAME_START LOG_FRAME_BYTE LOG_FRAME_END,
+               LOG_FRAME_START_ARGS, dat[0], LOG_FRAME_END_ARGS);
+
+#ifdef FRAME_CB
+  FRAME_CB(dat, count);
+#endif
+}
+
+#ifdef HAVE_GPIO_COMMANDS
+static bool setPinMode(const String& cmd)
+{
+  if (cmd.length() < 3)
+    return false;
+  if (cmd[1] != ':')
+    return false;
+
+  int pinNumber = cmd.substring(2).toInt();
+
+  if (cmd[0] == 'I')
+    pinMode(pinNumber, INPUT);
+  else if (cmd[0] == 'O')
+    pinMode(pinNumber, OUTPUT);
+  else if (cmd[0] == 'U')
+    pinMode(pinNumber, INPUT_PULLUP);
+  else if (cmd[0] == 'D')
+    pinMode(pinNumber, INPUT_PULLDOWN);
+  else
+    return false;
+
+  return true;
+}
+
+static bool writePin(const String& cmd)
+{
+  if (cmd.length() < 3)
+    return false;
+  if (cmd[1] != ':')
+    return false;
+
+  int pinNumber = cmd.substring(2).toInt();
+
+  if (cmd[0] == '0')
+    digitalWrite(pinNumber, LOW);
+  else if (cmd[0] == '1')
+    digitalWrite(pinNumber, HIGH);
+  else
+    return false;
+
+  return true;
+}
+#endif
+
+#ifdef POWER_PIN
+static bool setPower(const String& cmd)
+{
+  if (cmd.length() < 1)
+    return false;
+
+  if (cmd[0] == '0') {
+    digitalWrite(POWER_PIN, LOW);
+  } else if (cmd[0] == '1') {
+    viCount = 0;
+    timer64Started = 0;
+    digitalWrite(POWER_PIN, HIGH);
+  } else {
+    return false;
+  }
+
+  return true;
+}
+#endif
+
+static bool setLoop(const String& cmd)
+{
+  if (cmd.length() < 1)
+    return false;
+
+  if (cmd[0] == '0') {
+    doLoop = 0;
+  } else if (cmd[0] == '1') {
+    doLoop = 1;
+  } else {
+    return false;
+  }
+
+  return true;
+}
+
+void setupConsole()
+{
+  if (console == N64) {
+#ifdef N64_PIN
+    attachInterrupt(N64_PIN, n64Interrupt, FALLING);
+#endif
+  } else if (console == NES || console == SNES) {
+#ifdef SNES_LATCH_PIN
+    attachInterrupt(SNES_LATCH_PIN, snesLatchInterrupt, RISING);
+    attachInterrupt(SNES_CLOCK_PIN, snesClockInterrupt, RISING);
+#endif
+  }
+}
+
+static bool setConsole(const String& cmd)
+{
+  if (cmd == "N64")
+    console = N64;
+  else if (cmd == "NES")
+    console = NES;
+  else if (cmd == "SNES")
+    console = SNES;
+  else
+    return false;
+
+  setupConsole();
+
+  return true;
+}
+
+String hextobin(const String& hex)
+{
+  String ret;
+  ret.reserve((hex.length() + 1) / 2);
+
+  // mapping of ASCII characters to hex values
+  static const uint8_t map[] = {
+    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567
+    0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>?
+    0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // @ABCDEFG
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // HIJKLMNO
+  };
+
+  for (size_t pos = 0; pos < hex.length(); pos += 2) {
+    size_t idx0 = ((uint8_t)hex[pos]     & 0x1F) ^ 0x10,
+           idx1 = ((uint8_t)hex[pos + 1] & 0x1F) ^ 0x10;
+    ret += (char)((map[idx0] << 4) | map[idx1]);
+  }
+
+  return ret;
+}
+
+static SdFile writeFile;
+
+static void appendData(const String& data) {
+  if (writeFile.write(data.c_str(), data.length()) == data.length())
+    Serial.println("AP:OK");
+  else
+    Serial.println("AP:NAK");
+}
+
+static void waitForIdle(unsigned us)
+{
+  LED_HIGH;
+  N64_HIGH;
+  startTimer();
+  int lastReset = 0;
+  while (readTimer() - lastReset < us * MICRO_CYCLES && readTimer() < us * MICRO_CYCLES * 4) {
+    if (!N64_QUERY)
+      lastReset = readTimer();
+  }
+  LED_LOW;
+}
+
+static String basename(const String& path) {
+  int pos = path.lastIndexOf('/');
+  if (pos < 0)
+    return path;
+
+  return path.substring(pos + 1);
+}
+
+static bool openTAS(const String& path) {
+    char signature[4];
+    int version;
+
+    Serial.write("M:");
+    Serial.println(path);
+
+    // Open the file for reading:
+    Serial.print(F("L:Opening file '"));
+    Serial.print(path);
+    Serial.println(F("'..."));
+
+    Serial.flush();
+
+    if (tasFile.isOpen())
+      tasFile.close();
+
+    // Error check
+    if (!tasFile.open(&sd, path.c_str(), O_READ)) {
+        Serial.println(F("E:Error in opening file"));
+        return false;
+    }
+
+    // Open header
+    if (tasFile.read(signature, 4) != 4 || tasFile.read(&version, 4) != 4) {
+        tasFile.close();
+        Serial.println(F("E:Failed to read signature"));
+        return false;
+    }
+
+    // Validate file signature
+    if (memcmp(signature, "M64\x1A", 4) != 0) {
+        Serial.println(F("E:m64 signature invalid"));
+        tasFile.close();
+        return false;
+    }
+
+    // Print version
+    Serial.print(F("L:M64 Version: "));
+    Serial.println(version);
+
+    Serial.flush();
+
+    tasFile.seekSet(0x018);
+
+    // Open header
+    uint32_t newNumFrames;
+    if (tasFile.read(&newNumFrames, 4) != 4) {
+        tasFile.close();
+        Serial.println(F("E:Failed to read frame count"));
+        return false;
+    }
+
+    // Get header size
+    switch(version) {
+        case 1:
+        case 2:
+            tasFile.seekSet(0x200);
+            break;
+        case 3:
+            tasFile.seekSet(0x400);
+            break;
+        default:
+          // Unknown version
+            Serial.println(F("E:unknown M64 version"));
+            tasFile.close();
+            return false;
+    }
+
+    // Final check
+    if (!tasFile.available()) {
+        Serial.println(F("E:No input data found in file"));
+        tasFile.close();
+        return false;
+    }
+
+    newNumFrames = min(newNumFrames, (tasFile.fileSize() - tasFile.curPosition()) / 4);
+
+    Serial.write("N:");
+    Serial.println(newNumFrames);
+    Serial.flush();
+
+    // Wait for the line to go idle, then begin listening
+    /*
+    for (int idle_wait=32; idle_wait>0; --idle_wait) {
+        if (!N64_QUERY) {
+            idle_wait = 32;
+        }
+    }*/
+
+    noInterrupts();
+    finished = false;
+    initBuffers();
+    curFrame = 0;
+    numFrames = newNumFrames;
+    console = N64;
+    filename = basename(path);
+    interrupts();
+
+    setupConsole();
+
+#ifdef POWER_PIN
+    if (doLoop)
+      setPower("1");
+#endif
+
+    return true;
+}
+
+static void updateInputBuffer() {
+    if (finished || !tasFile.isOpen())
+      return;
+
+    // Check for file end
+    if (!tasFile.available()) {
+        tasFile.close();
+        return;
+    }
+
+    size_t availableSize;
+    size_t writePos = getNextRegion(availableSize);
+
+    // Optimized chunk reads
+    if ((availableSize < 512 && (writePos % 512) == 0) || !availableSize)
+        return;
+
+    int readBytes = tasFile.read(inputBuffer + writePos, availableSize);
+    if (readBytes <= 0) {
+      lockedPrintln(F("W:Failed to read next inputs from file. (This is recoverable)"));
+      return;
+    }
+
+    noInterrupts();
+    bufferEndPos = writePos + readBytes;
+    bufferHasData = true;
+    interrupts();
+}
+
+/**
+ * Complete copy and paste of gc_send, but with the N64
+ * pin being manipulated instead.
+ */
+static void n64_send(unsigned char *buffer, char length, bool wide_stop)
+{
+  unsigned long target;
+  LED_HIGH;
+  N64_LOW;
+  startTimer();
+
+  for (int i = 0; i < length * 8; i++) {
+    char bit = (buffer[i >> 3] >> (7 - (i & 7))) & 1;
+    target = MICRO_CYCLES * (3 - bit * 2);
+    while (readTimer() < target);
+      N64_HIGH;
+    while (readTimer() < MICRO_CYCLES * 4);
+      N64_LOW;
+    startTimer();
+  }
+
+  target = MICRO_CYCLES * (1 + wide_stop);
+
+  while (readTimer() < target);
+
+  N64_HIGH;
+  startTimer();
+  while (!N64_QUERY && readTimer() < MICRO_CYCLES * 4);
+  LED_LOW;
+}
+
+/**
+  * Waits for an incomming signal on the N64 pin and reads the command,
+  * and if necessary, any trailing bytes.
+  * 0x00 is an identify request
+  * 0x01 is a status request
+  * 0x02 is a controller pack read
+  * 0x03 is a controller pack write
+  *
+  * for 0x02 and 0x03, additional data is passed in after the command byte,
+  * which is also read by this function.
+  *
+  * All data is raw dumped to the n64_raw_dump array, 1 bit per byte, except
+  * for the command byte, which is placed all packed into n64_command
+  */
+static void get_n64_command()
+{
+    int bitcount = 8;
+    n64_command = 0;
+    char newByte = 0;
+
+    LED_HIGH;
+
+#define FAIL_TIMEOUT {\
+  if (readTimer() >= MICRO_CYCLES * 10) {\
+    n64_command = -1; \
+    goto fail; \
+  } \
+}\
+
+    edgesRead = 0;
+
+    for (int i = 1; i <= bitcount; i++) {
+        while (!N64_QUERY)
+          FAIL_TIMEOUT;
+        long lowTime = readAndResetTimer();
+        if (!edgesRead)
+            firstEdgeTime = lowTime;
+        edgesRead++;
+        while (N64_QUERY)
+          FAIL_TIMEOUT;
+        long highTime = readAndResetTimer();
+        if (edgesRead == 1)
+            secondEdgeTime = highTime;
+        edgesRead++;
+        char bit =
+#ifdef USE_COMPARE_ABSOLUTE
+            (highTime >= MICRO_CYCLES * 2)
+#else
+            (lowTime < highTime)
+#endif
+        ;
+        newByte <<= 1;
+        newByte |= bit;
+
+        if (i == 8) {
+          n64_command = newByte;
+          switch (newByte) {
+              case (0x03):
+                  // write command
+                  // we expect a 2 byte address and 32 bytes of data
+                  bitcount += 272; // 34 bytes * 8 bits per byte
+                  //Serial.println("command is 0x03, write");
+                  break;
+              case (0x02):
+                  // read command 0x02
+                  // we expect a 2 byte address
+                  bitcount += 16;
+                  //Serial.println("command is 0x02, read");
+                  break;
+              case (0x00):
+              case (0x01):
+              default:
+                  // get the last (stop) bit
+                  break;
+          }
+        } else if (!(i & 7)) {
+          n64_raw_dump[(i >> 3) - 1] = newByte;
+        }
+    }
+
+    // Wait for the stop bit
+    while (!N64_QUERY)
+      FAIL_TIMEOUT;
+    startTimer();
+    while (readTimer() < MICRO_CYCLES * 2);
+
+fail:
+    incompleteCommand = newByte;
+
+    LED_LOW;
+}
+
+static void n64Interrupt()
+{
+    noInterrupts();
+
+    unsigned char data, addr;
+    int ticksSinceLast = readAndResetTimer();
+    bool haveFrame;
+
+#ifdef DISABLE_N64_INTERRUPT
+    DISABLE_N64_INTERRUPT();
+#endif
+
+    // Wait for incoming 64 command
+    // this will block until the N64 sends us a command
+    get_n64_command();
+
+    // 0x00 is identify command
+    // 0x01 is status
+    // 0x02 is read
+    // 0x03 is write
+    switch (n64_command)
+    {
+        case 0x00:
+        case 0xFF:
+            // identify
+            // mutilate the output_buffer array with our status
+            // we return 0x050001 to indicate we have a rumble pack
+            // or 0x050002 to indicate the expansion slot is empty
+            //
+            // 0xFF I've seen sent from Mario 64 and Shadows of the Empire.
+            // I don't know why it's different, but the controllers seem to
+            // send a set of status bytes afterwards the same as 0x00, and
+            // it won't work without it.
+            output_buffer[0] = 0x05;
+            output_buffer[1] = 0x00;
+            output_buffer[2] = 0x01;
+
+            n64_send(output_buffer, 3, 1);
+            logFromISR("P:%ld", viCount);
+            viCount = 0;
+#ifdef PIT_LTMR64H
+            start64Timer();
+#endif
+            break;
+        case 0x01:
+            haveFrame = (!finished && bufferHasData);
+            // If the TAS is finished, there's nothing left to do.
+            if (haveFrame)
+              memcpy(output_buffer, inputBuffer + bufferPos, 4);
+            else
+              memset(output_buffer, 0, 4);
+
+            // blast out the pre-assembled array in output_buffer
+            n64_send(output_buffer, 4, 1);
+
+            logFrame(output_buffer, 4, curFrame + (haveFrame ? 1 : 0));
+
+            if (!haveFrame)
+              break;
+
+            // update input buffer and make sure it doesn't take too long
+
+           /*Serial.print(F("Pos: "));
+            Serial.print(bufferPos);
+            Serial.print(F(" Data: "));
+            Serial.println(inputBuffer[bufferPos]);*/
+
+            advanceBuffer(4);
+
+//            while (Serial.availableForWrite() && readTimer() < 10 * 1000 * MICRO_CYCLES && N64_QUERY);
+
+/*            if (!finished)
+              curPos = (curFrame * screen.width()) / numFrames;
+            screen.fill(255,0,0);
+            while (progressPos < curPos) {
+              screen.point(progressPos++, screen.height() - 1);
+            }
+            screen.fill(255,255,255);*/
+
+            break;
+        case 0x02:
+            // A read. If the address is 0x8000, return 32 bytes of 0x80 bytes,
+            // and a CRC byte.  this tells the system our attached controller
+            // pack is a rumble pack
+
+            // Assume it's a read for 0x8000, which is the only thing it should
+            // be requesting anyways
+            memset(output_buffer, 0x80, 32);
+            output_buffer[32] = 0xB8; // CRC
+
+            n64_send(output_buffer, 33, 1);
+            logFromISR("L:Got a read, what?");
+
+            //Serial.println("It was 0x02: the read command");
+            break;
+        case 0x03:
+            // A write. we at least need to respond with a single CRC byte.  If
+            // the write was to address 0xC000 and the data was 0x01, turn on
+            // rumble! All other write addresses are ignored. (but we still
+            // need to return a CRC)
+
+            // decode the first data byte (fourth overall byte), bits indexed
+            // at 24 through 31
+            data = 0;
+            data |= (n64_raw_dump[16] != 0) << 7;
+            data |= (n64_raw_dump[17] != 0) << 6;
+            data |= (n64_raw_dump[18] != 0) << 5;
+            data |= (n64_raw_dump[19] != 0) << 4;
+            data |= (n64_raw_dump[20] != 0) << 3;
+            data |= (n64_raw_dump[21] != 0) << 2;
+            data |= (n64_raw_dump[22] != 0) << 1;
+            data |= (n64_raw_dump[23] != 0);
+
+            // get crc byte, invert it, as per the protocol for
+            // having a memory card attached
+            output_buffer[0] = crc_repeating_table[data] ^ 0xFF;
+
+            // send it
+            n64_send(output_buffer, 1, 1);
+            logFromISR("L:Got a write, what?");
+
+            // end of time critical code
+            // was the address the rumble latch at 0xC000?
+            // decode the first half of the address, bits
+            // 8 through 15
+            addr = 0;
+            addr |= (n64_raw_dump[0] != 0) << 7;
+            addr |= (n64_raw_dump[1] != 0) << 6;
+            addr |= (n64_raw_dump[2] != 0) << 5;
+            addr |= (n64_raw_dump[3] != 0) << 4;
+            addr |= (n64_raw_dump[4] != 0) << 3;
+            addr |= (n64_raw_dump[5] != 0) << 2;
+            addr |= (n64_raw_dump[6] != 0) << 1;
+            addr |= (n64_raw_dump[7] != 0);
+
+            //Serial.println("It was 0x03: the write command");
+            //Serial.print("Addr was 0x");
+            //Serial.print(addr, HEX);
+            //Serial.print(" and data was 0x");
+            //Serial.println(data, HEX);
+            break;
+
+        case -1:
+            if (finished)
+              break;
+            logFromISR("W:RTO; %d read; b:%02hhx; %d since; %d/%d to 1/2", edgesRead, incompleteCommand, ticksSinceLast, firstEdgeTime, secondEdgeTime);
+            if (curFrame > 100)
+              logFromISR("D:timeout");
+            waitForIdle(1000);
+            break;
+
+        default:
+            if (finished)
+              break;
+            logFromISR("W:Unknown; %d read; b:%02hhx; %d since; %d/%d to 1/2", edgesRead, n64_command, ticksSinceLast, firstEdgeTime, secondEdgeTime);
+            if (curFrame > 100)
+              logFromISR("D:inval");
+            waitForIdle(1000);
+            break;
+    }
+
+#ifdef ENABLE_N64_INTERRUPT
+    ENABLE_N64_INTERRUPT();
+#endif
+
+    interrupts();
+}
+
+#ifdef SNES_DATA_PIN
+static void snesWriteBit()
+{
+  digitalWrite(SNES_DATA_PIN, (currentSnesFrame & 0x8000) ? LOW : HIGH);
+  if (currentSnesFrame & 0x8000)
+    LED_HIGH;
+  else
+    LED_LOW;
+  currentSnesFrame <<= 1;
+  currentSnesFrame |= 1;
+}
+
+static void snesLatchInterrupt()
+{
+  noInterrupts();
+  int haveFrame = (!finished && bufferHasData);
+  int len = (console == SNES) ? 2 : 1;
+  if (!haveFrame) {
+    unsigned char logBuf[2] = {0, 0};
+    currentSnesFrame = 0;
+    logFrame(logBuf, len, curFrame + 1);
+  } else {
+    if (console == NES) {
+      currentSnesFrame = (inputBuffer[bufferPos] << 8) | 0xFF;
+    } else {
+      currentSnesFrame = (inputBuffer[bufferPos] << 8) | inputBuffer[bufferPos + 1];
+    }
+    logFrame(inputBuffer + bufferPos, len, curFrame + 1);
+  }
+  snesWriteBit();
+  if (readTimer() >= frameDuration) {
+    startTimer();
+    if (haveFrame)
+      advanceBuffer(len);
+  }
+  interrupts();
+}
+
+static void snesClockInterrupt()
+{
+  noInterrupts();
+  snesWriteBit();
+  interrupts();
+}
+#endif
+
+#ifdef HAVE_EEPROM
+static void setEEPROM(const String& cmd)
+{
+  // Write terminator first, so we won't overread (by much) if we die early
+  EEPROM.write(cmd.length(), 0);
+  unsigned j = 0;
+  for (unsigned i = 0; i < cmd.length(); i++, j++) {
+    if (cmd[i] == '\\' && cmd[i + 1] == 'n') {
+      EEPROM.write(j, '\n');
+      i++;
+    } else if (cmd[i] == '\\' && cmd[i + 1] == '\\') {
+      EEPROM.write(j, '\\');
+      i++;
+    } else {
+      EEPROM.write(j, cmd[i]);
+    }
+  }
+  EEPROM.write(j, 0);
+
+  lockedPrintln("L:Finished writing to EEPROM:", cmd.c_str());
+}
+#endif
+
+static void handleCommand(const String& cmd)
+{
+  if (cmd.startsWith("M:")) {
+    openTAS(cmd.substring(2));
+  } else if (cmd.startsWith("O:")) {
+    //dummy
+  } else if (cmd.startsWith("L:")) {
+    emitList(cmd.substring(2));
+  } else if (cmd.startsWith("MK:")) {
+    lockedPrintln("MK:", sd.mkdir(cmd.substring(3).c_str()));
+  } else if (cmd.startsWith("CR:")) {
+    if (writeFile.isOpen()) {
+      writeFile.close();
+    }
+    if (writeFile.open(&sd, cmd.substring(3).c_str(), O_WRITE | O_CREAT | O_TRUNC)) {
+      lockedPrintln("CR:OK");
+    } else {
+      lockedPrintln("CR:NAK");
+    }
+  } else if (cmd.startsWith("AP:")) {
+    appendData(hextobin(cmd.substring(3)));
+  } else if (cmd.startsWith("IN:")) {
+    size_t len = appendInputs(hextobin(cmd.substring(3)));
+    lockedPrintln("IN:", len);
+  } else if (cmd.startsWith("HL:")) {
+    hold = cmd[3] == '1';
+  } else if (cmd.startsWith("CL:")) {
+    writeFile.close();
+    lockedPrintln("CL:OK");
+#ifdef HAVE_GPIO_COMMANDS
+  } else if (cmd.startsWith("PM:")) {
+    setPinMode(cmd.substring(3));
+  } else if (cmd.startsWith("DW:")) {
+    writePin(cmd.substring(3));
+#endif
+#ifdef POWER_PIN
+  } else if (cmd.startsWith("PW:")) {
+    setPower(cmd.substring(3));
+#endif
+  } else if (cmd.startsWith("SC:")) {
+    setConsole(cmd.substring(3));
+#ifdef HAVE_EEPROM
+  } else if (cmd.startsWith("WN:")) {
+    setEEPROM(cmd.substring(3));
+#endif
+  } else if (cmd.startsWith("LO:")) {
+    setLoop(cmd.substring(3));
+  } else {
+    lockedPrintln("E:Unknown CMD:", cmd.c_str());
+  }
+}
+
+static void handleChar(int newChar)
+{
+  if (newChar == '\n') {
+    if (skippingInput)
+      Serial.println("E:Skipped too-long line");
+    else
+      handleCommand(inputString);
+    inputString = "";
+    skippingInput = false;
+  } else if (inputString.length() > MAX_CMD_LEN) {
+    skippingInput = true;
+  } else {
+    inputString += (char)newChar;
+  }
+}
+
+static void inputLoop()
+{
+  int newChar;
+  int charsRead = 0;
+  while ((newChar = Serial.read()) != -1 && (charsRead++ <= MAX_LOOP_LEN))
+    handleChar(newChar);
+}
+
+static void mainLoop()
+{
+  updateInputBuffer();
+
+  // Record if it took longer than expected
+  /*updateTime = readTimer();
+  if (updateTime > 1000 * MICRO_CYCLES) {
+      Serial.print(F("Input buffer update took too long ("));
+      Serial.print(updateTime / MICRO_CYCLES);
+      Serial.println(F(" us)"));
+  }*/
+}
+
+#ifdef HAVE_EEPROM
+static void runEEPROM()
+{
+  int i = 0;
+  while (i < 1000) {
+    int c = EEPROM.read(i);
+    if (!c)
+      break;
+    handleChar(c);
+    i++;
+  }
+  if (i)
+    handleChar('\n');
+}
+#endif
+
+void loop()
+{
+  inputLoop();
+  mainLoop();
+#ifdef HAVE_EEPROM
+  if (doLoop) {
+    static bool isClearing = false;
+    uint64_t curTime = read64Timer();
+
+    if ((finished && timer64Started && (curTime - finalTime) > (MICRO_BUS_CYCLES * 1000 * 1000 * (isClearing ? 1 : 30)))
+//        || (timer64Started && (curTime - lastFrameTime) > (MICRO_BUS_CYCLES * 1000 * 1000 * 30))
+       ) {
+      setPower("0");
+      while ((read64Timer() - curTime) < (MICRO_BUS_CYCLES * 1000 * 1000 * 2));
+      isClearing = !isClearing;
+      if (isClearing)
+        openTAS("clear.m64");
+      else
+        runEEPROM();
+    }
+  }
+#endif
+
+  bool first = true;
+  bool gotData = false;
+  do {
+    int i = 0;
+    gotData = false;
+    if (first) {
+      for (; i < N_SER_BUFS; i++) {
+        if (!bufHasData[i])
+          break;
+        else
+          gotData = true;
+      }
+    }
+    first = false;
+    for (; i < N_SER_BUFS; i++) {
+      if (bufHasData[i]) {
+#ifdef LOG_CB
+        LOG_CB(&serBuffer[i][0]);
+#endif
+        Serial.println(&serBuffer[i][0]);
+        bufHasData[i] = 0;
+        Serial.flush();
+        gotData = true;
+      }
+    }
+  } while (gotData);
+}
+
+#ifdef VI_PIN
+static void viInterrupt()
+{
+  viCount++;
+}
+#endif
+
+void printSDError(const char* msg) {
+  (void)msg;
+  if (sd.sdErrorCode()) {
+    if (sd.sdErrorCode() == SD_CARD_ERROR_ACMD41) {
+      Serial.println("Try power cycling the SD card.");
+    }
+    printSdErrorSymbol(&Serial, sd.sdErrorCode());
+    Serial.print(", ErrorData: 0X");
+    Serial.println(sd.sdErrorData(), HEX);
+  }
+}
+
+void setup()
+{
+#ifdef ARM_DEMCR
+  ARM_DEMCR |= ARM_DEMCR_TRCENA;
+#endif
+#ifdef ARM_DWT_CTRL
+  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;
+#endif
+
+  startTimer();
+
+#ifdef SERIAL_TIMEOUT_US
+  while (readTimer() > MICRO_CYCLES * 1000 * 1000);
+#endif
+
+  Serial.begin(SERIAL_BAUD_RATE);
+
+#ifdef SERIAL_TIMEOUT_US
+  while (!Serial) {
+    if (readTimer() > MICRO_CYCLES * SERIAL_TIMEOUT_US) {
+      doLoop = 1;
+      break;
+    }
+  } // wait for serial port to connect. Needed for native USB port only
+
+  Serial.print(F("L:Serial init timer exited: doLoop="));
+  Serial.println(doLoop);
+#endif
+
+  LED_HIGH;
+
+  // Status LED
+#ifdef STATUS_PIN
+  digitalWrite(STATUS_PIN, LOW);
+  pinMode(STATUS_PIN, OUTPUT);
+#endif
+
+  // Configure controller pins
+#ifdef N64_PIN
+  pinMode(N64_PIN, INPUT_PULLUP);
+  digitalWrite(N64_PIN, LOW);
+#endif
+
+#ifdef SNES_LATCH_PIN
+  pinMode(SNES_LATCH_PIN, INPUT);
+  pinMode(SNES_CLOCK_PIN, INPUT);
+  pinMode(SNES_DATA_PIN, OUTPUT);
+  digitalWrite(SNES_DATA_PIN, LOW);
+#endif
+
+  // Power
+#ifdef POWER_PIN
+  digitalWrite(POWER_PIN, LOW);
+  pinMode(POWER_PIN, OUTPUT);
+#endif
+
+  // VI pulse
+#ifdef VI_PIN
+  pinMode(VI_PIN, INPUT);
+  attachInterrupt(VI_PIN, viInterrupt, FALLING);
+#endif
+
+  // Let the controller pins interrupt anything else
+#ifdef IRQ_PORTC
+  NVIC_SET_PRIORITY(IRQ_PORTC, 0);
+#endif
+
+  // Let the VI pin interrupt anything other than controller pins
+//  NVIC_SET_PRIORITY(IRQ_PORTC, 16);
+
+#ifndef SD_CONFIG
+#define SD_CONFIG BUILTIN_SDCARD
+#endif
+
+  // Initialize SD card
+  if (!sd.begin(SD_CONFIG)) {
+    printSDError("init");
+    Serial.println(F("E:SD initialization failed!"));
+    return;
+  }
+  Serial.println(F("L:SD initialization done."));
+
+  // Setup buffer
+  initBuffers();
+
+  Serial.println(F("L:Initialization done."));
+
+#ifdef HAVE_EEPROM
+  if (doLoop)
+    runEEPROM();
+#endif
+}

+ 13 - 2
non_catalog_apps/tpms_receiver/Readme.md

@@ -2,7 +2,6 @@
 [![FlipC.org](https://flipc.org/wosk/flipperzero-tpms/badge)](https://flipc.org/wosk/flipperzero-tpms)
 
 ## Features
-
 - Read [TPMS](https://en.wikipedia.org/wiki/Tire-pressure_monitoring_system) sensors
 - Relearn by 125kHz signal
 
@@ -10,6 +9,18 @@
 * Schrader GG4
 * Abarth 124 (soon)
 
-#### Feel free to contribute via PR or fill issue
+## How to use
+In some circumstances TPMS sensors should transmit message periodically (car moving) or by event (emergency pressure reduction or temperature increase), so it can be caught.
+
+While the car is stationary or sensor is not mounted into tire, Relearn mode can be enabled by emitting 125kHz signal. Keep sensor housing or valve near Flipper Zero`s back, like RFID card and push Right button to activate relearn signal for 1 second.
+
+When sensor transmit message, you will see jumps of RSSI meter.
+If sensor is supported and correct frequency and modulation was set, an item with Model and ID will be added for each sensor.
+
+Pressing OK displays temperature and pressure.
+
+![input](tpms.gif)
+
+Feel free to contribute via PR or report issue
 
 Code based on [weather station app](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/external/weather_station)

+ 2 - 2
non_catalog_apps/tpms_receiver/protocols/schrader_gg4.c

@@ -100,8 +100,8 @@ const SubGhzProtocolEncoder tpms_protocol_schrader_gg4_encoder = {
 const SubGhzProtocol tpms_protocol_schrader_gg4 = {
     .name = TPMS_PROTOCOL_SCHRADER_GG4_NAME,
     .type = SubGhzProtocolTypeStatic,
-    .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_868 |
-            SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable,
+    .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_AM |
+            SubGhzProtocolFlag_Decodable,
 
     .decoder = &tpms_protocol_schrader_gg4_decoder,
     .encoder = &tpms_protocol_schrader_gg4_encoder,

+ 35 - 9
non_catalog_apps/tpms_receiver/scenes/tpms_scene_about.c

@@ -1,5 +1,6 @@
 #include "../tpms_app_i.h"
 #include "../helpers/tpms_types.h"
+#include "../protocols/protocol_items.h"
 
 void tpms_scene_about_widget_callback(GuiButtonType result, InputType type, void* context) {
     TPMSApp* app = context;
@@ -11,6 +12,17 @@ void tpms_scene_about_widget_callback(GuiButtonType result, InputType type, void
 void tpms_scene_about_on_enter(void* context) {
     TPMSApp* app = context;
 
+    widget_add_text_box_element(
+        app->widget,
+        0,
+        2,
+        128,
+        14,
+        AlignCenter,
+        AlignBottom,
+        "\e#\e!          TPMS Reader         \e!\n",
+        false);
+
     FuriString* temp_str;
     temp_str = furi_string_alloc();
     furi_string_printf(temp_str, "\e#%s\n", "Information");
@@ -24,16 +36,30 @@ void tpms_scene_about_on_enter(void* context) {
         temp_str, "Reading messages from\nTPMS sensors that work\nwith SubGhz sensors\n\n");
 
     furi_string_cat_printf(temp_str, "Supported protocols:\n");
-    size_t i = 0;
-    const char* protocol_name =
-        subghz_environment_get_protocol_name_registry(app->txrx->environment, i++);
-    do {
-        furi_string_cat_printf(temp_str, "%s\n", protocol_name);
-        protocol_name = subghz_environment_get_protocol_name_registry(app->txrx->environment, i++);
-    } while(protocol_name != NULL);
 
-    widget_add_text_box_element(
-        app->widget, 0, 2, 128, 14, AlignCenter, AlignBottom, "TPMS Reader\n", false);
+    for(size_t i = 0; i < subghz_protocol_registry_count(&tpms_protocol_registry); ++i) {
+        char* frequency = NULL;
+        char* modulation = NULL;
+        const SubGhzProtocol* protocol =
+            subghz_protocol_registry_get_by_index(&tpms_protocol_registry, i);
+
+        if((protocol->flag & SubGhzProtocolFlag_433) &&
+           (protocol->flag & SubGhzProtocolFlag_315)) {
+            frequency = "433|315";
+        } else if(protocol->flag & SubGhzProtocolFlag_433) {
+            frequency = "433";
+        } else if(protocol->flag & SubGhzProtocolFlag_315) {
+            frequency = "315";
+        }
+
+        if(protocol->flag & SubGhzProtocolFlag_AM) {
+            modulation = "AM";
+        } else if(protocol->flag & SubGhzProtocolFlag_FM) {
+            modulation = "FM";
+        }
+        furi_string_cat_printf(temp_str, "%s (%s %s)\n", protocol->name, frequency, modulation);
+    }
+
     widget_add_text_scroll_element(app->widget, 0, 16, 128, 50, furi_string_get_cstr(temp_str));
     furi_string_free(temp_str);
 

BIN
non_catalog_apps/tpms_receiver/tpms.gif


+ 5 - 1
non_catalog_apps/ublox/README.md

@@ -1,2 +1,6 @@
 # ublox
-Flipper Zero u-blox GPS app
+Flipper Zero app to read from a u-blox GPS over I2C. This used to be
+in my repository `flipped`, but got to be both large and good enough
+to merit its own repo. Furthermore, I really, truly fixed the awful
+memory leak that had been plaguing this app for months, so now feature
+development can continue.

+ 1 - 0
non_catalog_apps/ublox/application.fam

@@ -8,6 +8,7 @@ App(
         "gui",
 	"i2c",
 	"locale",
+	"storage",
     ],
     stack_size=2 * 1024,
     order=20,

+ 86 - 0
non_catalog_apps/ublox/helpers/kml.c

@@ -0,0 +1,86 @@
+#include "kml.h"
+
+#define TAG "kml"
+
+bool kml_open_file(Storage* storage, KMLFile* kml, const char* path) {
+    kml->file = storage_file_alloc(storage);
+    if (!storage_file_open(kml->file,
+			   path,
+			   FSAM_WRITE,
+			   FSOM_CREATE_ALWAYS)) {
+	FURI_LOG_E(TAG, "failed to open KML file %s", path);
+	storage_file_free(kml->file);
+	return false;
+    }
+
+    // with the file opened, we need to write the intro KML tags
+    const char* kml_intro =
+	"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+	"<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n"
+	"  <Document>\n"
+	"    <name>Paths</name>\n"
+	"    <Style id=\"yellowLineGreenPoly\">\n"
+	"      <LineStyle>\n"
+	"        <color>7f00ffff</color>\n"
+	"        <width>4</width>\n"
+	"      </LineStyle>\n"
+	"      <PolyStyle>\n"
+	"        <color>7f00ff00</color>\n"
+	"      </PolyStyle>\n"
+	"    </Style>\n"
+	"    <Placemark>\n"
+	"      <name>Path 1</name>\n"
+	"      <description>Path 1</description>\n"
+	"      <styleUrl>#yellowLineGreenPoly</styleUrl>\n"
+	"      <LineString>\n"
+	"        <tessellate>1</tessellate>\n"
+	"        <extrude>1</extrude>\n"
+	"        <altitudeMode>absolute</altitudeMode>\n"
+	"        <coordinates>\n";
+
+    if (!storage_file_write(kml->file,
+			    kml_intro,
+			    strlen(kml_intro))) {
+	storage_file_close(kml->file);
+	storage_file_free(kml->file);
+	return false;
+    }
+
+    return true;
+}
+
+bool kml_add_path_point(KMLFile* kml, double lat, double lon, uint32_t alt) {
+    // KML is longitude then latitude for some reason
+    FuriString* point = furi_string_alloc_printf("          %f,%f,%lu\n", lon, lat, alt);
+    if (!storage_file_write(kml->file,
+			    furi_string_get_cstr(point),
+			    furi_string_size(point))) {
+	return false;
+    }
+
+    return true;
+}
+
+bool kml_close_file(KMLFile* kml) {
+    const char* kml_outro =
+	"        </coordinates>\n"
+	"      </LineString>\n"
+	"    </Placemark>\n"
+	"  </Document>\n"
+	"</kml>";
+
+    if (!storage_file_write(kml->file,
+			    kml_outro,
+			    strlen(kml_outro))) {
+	storage_file_close(kml->file);
+	storage_file_free(kml->file);
+	return false;
+    }
+
+    storage_file_close(kml->file);
+    storage_file_free(kml->file);
+    
+    return true;
+}	
+
+

+ 22 - 0
non_catalog_apps/ublox/helpers/kml.h

@@ -0,0 +1,22 @@
+#pragma once
+
+#include <furi.h>
+#include <storage/storage.h>
+
+#include "../helpers/ublox_types.h"
+
+typedef struct KMLFile {
+    File* file;
+} KMLFile;
+
+/**
+ * Open a KML file and write out initial XML tags for list of points in path.
+ * This assumes that `path` is a valid and complete Flipper filesystem path.
+ */
+bool kml_open_file(Storage* storage, KMLFile* kml, const char* path);
+
+bool kml_add_path_point(KMLFile* kml, double lat, double lon, uint32_t alt);
+
+bool kml_close_file(KMLFile* kml);
+
+

+ 2 - 1
non_catalog_apps/ublox/helpers/ublox_custom_event.h

@@ -1,5 +1,6 @@
 #pragma once
 
 typedef enum {
-  UbloxCustomEventResetOdometer,
+    UbloxCustomEventResetOdometer,
+    UbloxCustomEventTextInputDone,
 } UbloxCustomEvent;

+ 36 - 24
non_catalog_apps/ublox/helpers/ublox_types.h

@@ -1,48 +1,60 @@
 #pragma once
 
+#define UBLOX_VERSION_APP "0.1"
+#define UBLOX_DEVELOPED "liamhays"
+#define UBLOX_GITHUB "https://github.com/liamhays/ublox"
+
+#define UBLOX_KML_EXTENSION ".kml"
+
 typedef enum {
-  UbloxDataDisplayViewModeHandheld,
-  UbloxDataDisplayViewModeCar,
+    UbloxLogStateStartLogging,
+    UbloxLogStateLogging,
+    UbloxLogStateStopLogging,
+    UbloxLogStateNone,
+} UbloxLogState;
+
+typedef enum {
+    UbloxDataDisplayViewModeHandheld,
+    UbloxDataDisplayViewModeCar,
 } UbloxDataDisplayViewMode;
 
-  
 typedef enum {
-  UbloxDataDisplayBacklightOn,
-  UbloxDataDisplayBacklightDefault,
+    UbloxDataDisplayBacklightOn,
+    UbloxDataDisplayBacklightDefault,
 } UbloxDataDisplayBacklightMode;
 
 typedef uint32_t UbloxDataDisplayRefreshRate;
 
 typedef enum {
-  UbloxDataDisplayNotifyOn,
-  UbloxDataDisplayNotifyOff,
+    UbloxDataDisplayNotifyOn,
+    UbloxDataDisplayNotifyOff,
 } UbloxDataDisplayNotifyMode;
 
 typedef enum {
-  UbloxOdometerModeRunning = 0,
-  UbloxOdometerModeCycling = 1,
-  UbloxOdometerModeSwimming = 2,
-  UbloxOdometerModeCar = 3,
+    UbloxOdometerModeRunning = 0,
+    UbloxOdometerModeCycling = 1,
+    UbloxOdometerModeSwimming = 2,
+    UbloxOdometerModeCar = 3,
 } UbloxOdometerMode;
 
 typedef enum {
-  UbloxPlatformModelPortable = 0,
-  UbloxPlatformModelPedestrian = 3,
-  UbloxPlatformModelAutomotive = 4,
-  UbloxPlatformModelAtSea = 5,
-  UbloxPlatformModelAirborne2g = 7,
-  UbloxPlatformModelWrist = 9,
+    UbloxPlatformModelPortable = 0,
+    UbloxPlatformModelPedestrian = 3,
+    UbloxPlatformModelAutomotive = 4,
+    UbloxPlatformModelAtSea = 5,
+    UbloxPlatformModelAirborne2g = 7,
+    UbloxPlatformModelWrist = 9,
 } UbloxPlatformModel;
-  
 
 typedef struct UbloxDataDisplayState {
-  UbloxDataDisplayViewMode view_mode;
-  UbloxDataDisplayBacklightMode backlight_mode;
-  UbloxDataDisplayRefreshRate refresh_rate;
-  UbloxDataDisplayNotifyMode notify_mode;
+    UbloxDataDisplayViewMode view_mode;
+    UbloxDataDisplayBacklightMode backlight_mode;
+    UbloxDataDisplayRefreshRate refresh_rate;
+    UbloxDataDisplayNotifyMode notify_mode;
 } UbloxDataDisplayState;
 
 typedef struct UbloxDeviceState {
-  UbloxOdometerMode odometer_mode;
-  UbloxPlatformModel platform_model;
+    UbloxOdometerMode odometer_mode;
+    UbloxPlatformModel platform_model;
 } UbloxDeviceState;
+

BIN
non_catalog_apps/ublox/images/ublox_wiring.png


BIN
non_catalog_apps/ublox/images/ublox_wiring.xcf


+ 18 - 5
non_catalog_apps/ublox/scenes/ublox_scene_about.c

@@ -17,7 +17,7 @@ void ublox_scene_about_on_enter(void* context) {
     widget_add_text_box_element(
         ublox->widget,
         0,
-        2,
+        0,
         128,
         14,
         AlignCenter,
@@ -25,11 +25,24 @@ void ublox_scene_about_on_enter(void* context) {
         "\e#\e!          u-blox GPS         \e!\n",
         false);
 
-    furi_string_printf(
-        s,
-        "%s\n",
-        "u-blox GPS\n\nMade by: Liam Hays\n\nGitHub: https://github.com/liamhays/ublox\n\nVersion: WIP\n\nLicense: GPL-3.0");
+    furi_string_cat_printf(s, "\e#%s\n", "Information");
+    furi_string_cat_printf(s, "Version: %s\n", UBLOX_VERSION_APP);
+    furi_string_cat_printf(s, "Developed by: %s\n", UBLOX_DEVELOPED);
+    furi_string_cat_printf(s, "GitHub: %s\n", UBLOX_GITHUB);
+
+    furi_string_cat_printf(s, "\e#%s\n", "Description");
+    furi_string_cat_printf(s,
+			   "This app is a multi-purpose tool for u-blox GPS modules connected over I2C."
+			   " It is compatible with 8 and 9 series GPS units, and probably other models,"
+			   " sold by Sparkfun and other vendors.\n");
 
+    furi_string_cat_printf(s, "\e#%s\n", "Usage");
+    furi_string_cat_printf(s,
+			   "Data Display shows GPS data. You can enable logging to a KML file to be"
+			   " viewed in a map program.\n"
+			   "Sync Time to GPS will sync the Flipper's RTC to the GPS. Note that this"
+			   " may be up to one second off, because there is no PPS signal connected.");
+			   
     widget_add_text_scroll_element(ublox->widget, 0, 16, 128, 50, furi_string_get_cstr(s));
 
     furi_string_free(s);

+ 2 - 0
non_catalog_apps/ublox/scenes/ublox_scene_config.h

@@ -1,5 +1,7 @@
 ADD_SCENE(ublox, start, Start)
 ADD_SCENE(ublox, data_display, DataDisplay)
+ADD_SCENE(ublox, enter_file_name, EnterFileName)
+ADD_SCENE(ublox, sync_time, SyncTime)
 ADD_SCENE(ublox, data_display_config, DataDisplayConfig)
 ADD_SCENE(ublox, wiring, Wiring)
 ADD_SCENE(ublox, about, About)

+ 82 - 108
non_catalog_apps/ublox/scenes/ublox_scene_data_display.c

@@ -1,132 +1,106 @@
-// This is a personal academic project. Dear PVS-Studio, please check it.
-
-// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: https://pvs-studio.com
 #include "../ublox_i.h"
 #include "../ublox_worker_i.h"
 
 #define TAG "ublox_scene_data_display"
 
-const NotificationSequence sequence_new_reading = {
-  //&message_vibro_on,
-  &message_green_255,
-  &message_delay_100,
-  &message_green_0,
-  //&message_vibro_off,
-  NULL,
-};
-
 void ublox_scene_data_display_worker_callback(UbloxWorkerEvent event, void* context) {
-  Ublox* ublox = context;
+    Ublox* ublox = context;
 
-  view_dispatcher_send_custom_event(ublox->view_dispatcher, event);
+    view_dispatcher_send_custom_event(ublox->view_dispatcher, event);
 }
 
 void ublox_scene_data_display_view_callback(void* context, InputKey key) {
-  Ublox* ublox = context;
-
-  // just reuse generic events
-  if (key == InputKeyLeft) {
-    view_dispatcher_send_custom_event(ublox->view_dispatcher, GuiButtonTypeLeft);
-  } else if (key == InputKeyOk) {
-    view_dispatcher_send_custom_event(ublox->view_dispatcher, GuiButtonTypeCenter);
-  }
+    Ublox* ublox = context;
+
+    // just reuse generic events
+    if(key == InputKeyLeft) {
+        view_dispatcher_send_custom_event(ublox->view_dispatcher, GuiButtonTypeLeft);
+    } else if(key == InputKeyOk) {
+        view_dispatcher_send_custom_event(ublox->view_dispatcher, GuiButtonTypeCenter);
+    } else if(key == InputKeyRight) {
+	view_dispatcher_send_custom_event(ublox->view_dispatcher, GuiButtonTypeRight);
+    }
 }
 
-static void timer_callback(void* context) {
-  Ublox* ublox = context;
-  FURI_LOG_I(TAG, "mem free before timer callback: %u", memmgr_get_free_heap());
-  // every time, try to set the view back to a GPS-found mode
+void ublox_scene_data_display_on_enter(void* context) {
+    Ublox* ublox = context;
 
-  // TODO: maybe different states for each message, to fix the leak?
-  ublox_worker_start(ublox->worker, UbloxWorkerStateRead,
-		     ublox_scene_data_display_worker_callback, ublox);
-  FURI_LOG_I(TAG, "mem free after timer callback: %u", memmgr_get_free_heap());
-}
+    // Use any existing data
+    data_display_set_nav_messages(ublox->data_display, ublox->nav_pvt, ublox->nav_odo);
+    
+    data_display_set_callback(ublox->data_display, ublox_scene_data_display_view_callback, ublox);
+    
+    if((ublox->data_display_state).view_mode == UbloxDataDisplayViewModeHandheld) {
+        data_display_set_state(ublox->data_display, DataDisplayHandheldMode);
+    } else if((ublox->data_display_state).view_mode == UbloxDataDisplayViewModeCar) {
+        data_display_set_state(ublox->data_display, DataDisplayCarMode);
+    }
 
-void ublox_scene_data_display_on_enter(void* context) {
-  Ublox* ublox = context;
-
-  data_display_set_callback(ublox->data_display, ublox_scene_data_display_view_callback, ublox);
-  if ((ublox->data_display_state).view_mode == UbloxDataDisplayViewModeHandheld) {
-    data_display_set_state(ublox->data_display, DataDisplayHandheldMode);
-  } else if ((ublox->data_display_state).view_mode == UbloxDataDisplayViewModeCar) {
-    data_display_set_state(ublox->data_display, DataDisplayCarMode);
-  }
-  
-  view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewDataDisplay);
-  
-  ublox->timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, ublox);
-
-  // convert from seconds to milliseconds
-  furi_timer_start(ublox->timer, furi_ms_to_ticks((ublox->data_display_state).refresh_rate * 1000));
-
-  ublox_worker_start(ublox->worker, UbloxWorkerStateRead,
-		     ublox_scene_data_display_worker_callback, ublox);
+    view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewDataDisplay);
+
+    ublox_worker_start(ublox->worker, UbloxWorkerStateRead, ublox_scene_data_display_worker_callback, ublox);
 }
 
 bool ublox_scene_data_display_on_event(void* context, SceneManagerEvent event) {
-  Ublox* ublox = context;
-  bool consumed = false;
-  //FURI_LOG_I(TAG, "mem free before event branch: %u", memmgr_get_free_heap());
-  if (event.type == SceneManagerEventTypeCustom) {
-    if (event.event == GuiButtonTypeLeft) {
-      FURI_LOG_I(TAG, "left button pressed");
-      scene_manager_next_scene(ublox->scene_manager, UbloxSceneDataDisplayConfig);
-      consumed = true;
-      
-    } else if (event.event == GuiButtonTypeCenter) {
-      furi_timer_stop(ublox->timer);
-      // MUST stop the worker first! (idk why though)
-      ublox_worker_stop(ublox->worker);
-      FURI_LOG_I(TAG, "reset odometer");
-      ublox_worker_start(ublox->worker, UbloxWorkerStateResetOdometer,
-			 ublox_scene_data_display_worker_callback, ublox);
-      furi_timer_start(ublox->timer, furi_ms_to_ticks((ublox->data_display_state).refresh_rate * 1000));
-      
-    } else if (event.event == UbloxWorkerEventDataReady) {
-      if ((ublox->data_display_state).notify_mode == UbloxDataDisplayNotifyOn) {
-	notification_message(ublox->notifications, &sequence_new_reading);
-      }
-
-      // Not setting the NAV messages in the data display seems to help...?
-      // (so many things have "seemed to help" that it's immensely confusing).
-
-      // disabling refresh in the with_view_model() call in the update function kinda helps
-      data_display_set_nav_messages(ublox->data_display, ublox->nav_pvt, ublox->nav_odo);
-
-      // There used to be a "set view_mode" if/else here. I don't
-      // think it was necessary but I do think it was contributing to
-      // the memory leak. With it removed, we get little to no leakage
-      // (and the leakage "automatically" fixes itself: some memory
-      // will be incorrectly allocated but then fixed).
-
-      // The upshot is that we used to refresh the data display scene
-      // 3 times in this callback. I suspect that the view handling
-      // code in the OS is good but not perfect, and holds on to
-      // memory for longer than it needs to. Reducing the number of
-      // total updates has helped.
-      
-    } else if (event.event == UbloxWorkerEventFailed) {
-      FURI_LOG_I(TAG, "UbloxWorkerEventFailed");
-      data_display_set_state(ublox->data_display, DataDisplayGPSNotFound);
+    Ublox* ublox = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+	    ublox_worker_stop(ublox->worker);
+            scene_manager_next_scene(ublox->scene_manager, UbloxSceneDataDisplayConfig);
+            consumed = true;
+        
+	} else if(event.event == GuiButtonTypeRight) {
+	    // TODO: only allow if GPS is detected?
+	    FURI_LOG_I(TAG, "right button");
+	    if (ublox->log_state == UbloxLogStateNone) {
+		// start logging
+		ublox_worker_stop(ublox->worker);
+		scene_manager_next_scene(ublox->scene_manager, UbloxSceneEnterFileName);
+		consumed = true;
+	    } else if(ublox->log_state == UbloxLogStateLogging) {
+		FURI_LOG_I(TAG, "stop logging from scene");
+		ublox->log_state = UbloxLogStateStopLogging;
+	    }
+	    
+        } else if(event.event == UbloxWorkerEventDataReady) {
+            if((ublox->data_display_state).notify_mode == UbloxDataDisplayNotifyOn) {
+                notification_message(ublox->notifications, &sequence_new_reading);
+            }
+
+            if((ublox->data_display_state).view_mode == UbloxDataDisplayViewModeHandheld) {
+                data_display_set_state(ublox->data_display, DataDisplayHandheldMode);
+            } else if((ublox->data_display_state).view_mode == UbloxDataDisplayViewModeCar) {
+                data_display_set_state(ublox->data_display, DataDisplayCarMode);
+            }
+
+            data_display_set_nav_messages(ublox->data_display, ublox->nav_pvt, ublox->nav_odo);
+
+
+	} else if(event.event == UbloxWorkerEventLogStateChanged) {
+	    data_display_set_log_state(ublox->data_display, ublox->log_state);
+
+	} else if(event.event == UbloxWorkerEventFailed) {
+            FURI_LOG_I(TAG, "UbloxWorkerEventFailed");
+            data_display_set_state(ublox->data_display, DataDisplayGPSNotFound);
+        }
     }
-  }
-  //FURI_LOG_I(TAG, "mem free after event branch: %u", memmgr_get_free_heap());
-  return consumed;
+    return consumed;
 }
 
 void ublox_scene_data_display_on_exit(void* context) {
-  Ublox* ublox = context;
+    Ublox* ublox = context;
+
+    /*if(ublox->log_state == UbloxLogStateLogging) {
+	FURI_LOG_I(TAG, "stop logging on exit");
+	ublox->log_state = UbloxLogStateStopLogging;
+	//while (ublox->log_state != UbloxLogStateNone);
+	//furi_delay_ms(500);
+	}*/
+    
+    ublox_worker_stop(ublox->worker);
 
-  furi_timer_stop(ublox->timer);
-  furi_timer_free(ublox->timer);
+    data_display_reset(ublox->data_display);
 
-  ublox_worker_stop(ublox->worker);
-  
-  data_display_reset(ublox->data_display);
-  // Use any existing data
-  data_display_set_nav_messages(ublox->data_display, ublox->nav_pvt, ublox->nav_odo);
-  FURI_LOG_I(TAG, "leaving data display scene");
 }
-    
-  

+ 291 - 240
non_catalog_apps/ublox/scenes/ublox_scene_data_display_config.c

@@ -3,344 +3,395 @@
 #define TAG "ublox_scene_data_display_config"
 
 enum UbloxSettingIndex {
-  UbloxSettingIndexRefreshRate,
-  UbloxSettingIndexBacklightMode,
-  UbloxSettingIndexDisplayMode,
-  UbloxSettingIndexNotify,
-  UbloxSettingIndexPlatformModel,
-  UbloxSettingIndexOdometerMode,
+    UbloxSettingIndexRefreshRate,
+    UbloxSettingIndexBacklightMode,
+    UbloxSettingIndexDisplayMode,
+    UbloxSettingIndexNotify,
+    UbloxSettingIndexPlatformModel,
+    UbloxSettingIndexOdometerMode,
+    UbloxSettingIndexResetOdometer,
 };
 
 enum UbloxDataDisplayConfigIndex {
-  UbloxDataDisplayConfigIndexDisplayMode,
-  UbloxDataDisplayConfigIndexRefreshRate,
-  UbloxDataDisplayConfigIndexBacklightMode,
+    UbloxDataDisplayConfigIndexDisplayMode,
+    UbloxDataDisplayConfigIndexRefreshRate,
+    UbloxDataDisplayConfigIndexBacklightMode,
 };
 
 #define DISPLAY_VIEW_MODE_COUNT 2
 const char* const display_view_mode_text[DISPLAY_VIEW_MODE_COUNT] = {
-  "Handheld",
-  "Car",
+    "Handheld",
+    "Car",
 };
 
 const UbloxDataDisplayViewMode display_view_mode_value[DISPLAY_VIEW_MODE_COUNT] = {
-  UbloxDataDisplayViewModeHandheld,
-  UbloxDataDisplayViewModeCar,
+    UbloxDataDisplayViewModeHandheld,
+    UbloxDataDisplayViewModeCar,
 };
 
 #define BACKLIGHT_MODE_COUNT 2
 const char* const backlight_mode_text[BACKLIGHT_MODE_COUNT] = {
-  "Default",
-  "On",
+    "Default",
+    "On",
 };
 
 const UbloxDataDisplayBacklightMode backlight_mode_value[BACKLIGHT_MODE_COUNT] = {
-  UbloxDataDisplayBacklightDefault,
-  UbloxDataDisplayBacklightOn,
+    UbloxDataDisplayBacklightDefault,
+    UbloxDataDisplayBacklightOn,
 };
 
 #define REFRESH_RATE_COUNT 8
 // double const means that the data is constant and that the pointer
 // is constant.
 const char* const refresh_rate_text[REFRESH_RATE_COUNT] = {
-  "2s",
-  "5s",
-  "10s",
-  "15s",
-  "20s",
-  "30s",
-  "45s",
-  "60s",
+    "2s",
+    "5s",
+    "10s",
+    "15s",
+    "20s",
+    "30s",
+    "45s",
+    "60s",
 };
 
 // might need to be ms?
 const UbloxDataDisplayRefreshRate refresh_rate_values[REFRESH_RATE_COUNT] = {
-  2,
-  5,
-  10,
-  15,
-  20,
-  30,
-  45,
-  60,
+    2,
+    5,
+    10,
+    15,
+    20,
+    30,
+    45,
+    60,
 };
 
 #define NOTIFY_MODE_COUNT 2
 const char* const notify_mode_text[NOTIFY_MODE_COUNT] = {
-  "On",
-  "Off",
+    "Off",
+    "On",
 };
 
 const UbloxDataDisplayNotifyMode notify_mode_values[NOTIFY_MODE_COUNT] = {
-  UbloxDataDisplayNotifyOn,
-  UbloxDataDisplayNotifyOff,
+    UbloxDataDisplayNotifyOff,
+    UbloxDataDisplayNotifyOn,
 };
 
 #define ODOMETER_MODE_COUNT 4
 const char* const odometer_mode_text[ODOMETER_MODE_COUNT] = {
-  "Run",
-  "Cycle",
-  "Swim",
-  "Car",
+    "Run",
+    "Cycle",
+    "Swim",
+    "Car",
 };
 
 const UbloxOdometerMode odometer_mode_values[ODOMETER_MODE_COUNT] = {
-  UbloxOdometerModeRunning,
-  UbloxOdometerModeCycling,
-  UbloxOdometerModeSwimming,
-  UbloxOdometerModeCar,
+    UbloxOdometerModeRunning,
+    UbloxOdometerModeCycling,
+    UbloxOdometerModeSwimming,
+    UbloxOdometerModeCar,
 };
 
 #define PLATFORM_MODEL_COUNT 6
 const char* const platform_model_text[PLATFORM_MODEL_COUNT] = {
-  "Portable",
-  "Pedestrian",
-  "Automotive",
-  "At Sea",
-  "Airborne <2g",
-  "Wrist",
+    "Portable",
+    "Pedest.",
+    "Auto.",
+    "At Sea",
+    "Air. <2g",
+    "Wrist",
 };
 
 const UbloxPlatformModel platform_model_values[PLATFORM_MODEL_COUNT] = {
-  UbloxPlatformModelPortable,
-  UbloxPlatformModelPedestrian,
-  UbloxPlatformModelAutomotive,
-  UbloxPlatformModelAtSea,
-  UbloxPlatformModelAirborne2g,
-  UbloxPlatformModelWrist,
+    UbloxPlatformModelPortable,
+    UbloxPlatformModelPedestrian,
+    UbloxPlatformModelAutomotive,
+    UbloxPlatformModelAtSea,
+    UbloxPlatformModelAirborne2g,
+    UbloxPlatformModelWrist,
 };
 
-uint8_t ublox_scene_data_display_config_next_refresh_rate(const UbloxDataDisplayRefreshRate value, void* context) {
-  furi_assert(context);
-  
-  uint8_t index = 0;
-  for (int i = 0; i < REFRESH_RATE_COUNT; i++) {
-    if (value == refresh_rate_values[i]) {
-      index = i;
-      break;
-    } else {
-      index = 0;
+uint8_t ublox_scene_data_display_config_next_refresh_rate(
+    const UbloxDataDisplayRefreshRate value,
+    void* context) {
+    furi_assert(context);
+
+    uint8_t index = 0;
+    for(int i = 0; i < REFRESH_RATE_COUNT; i++) {
+        if(value == refresh_rate_values[i]) {
+            index = i;
+            break;
+        } else {
+            index = 0;
+        }
     }
-  }
-  return index;
+    return index;
 }
 
 static void ublox_scene_data_display_config_set_refresh_rate(VariableItem* item) {
-  Ublox* ublox = variable_item_get_context(item);
-  uint8_t index = variable_item_get_current_value_index(item);
+    Ublox* ublox = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
 
-  variable_item_set_current_value_text(item, refresh_rate_text[index]);
-  (ublox->data_display_state).refresh_rate = refresh_rate_values[index];
-  FURI_LOG_I(TAG, "set refresh rate to %lds", (ublox->data_display_state).refresh_rate);
+    variable_item_set_current_value_text(item, refresh_rate_text[index]);
+    (ublox->data_display_state).refresh_rate = refresh_rate_values[index];
+    FURI_LOG_I(TAG, "set refresh rate to %lds", (ublox->data_display_state).refresh_rate);
 }
 
-static uint8_t ublox_scene_data_display_config_next_backlight_mode(const UbloxDataDisplayBacklightMode value, void* context) {
-  furi_assert(context);
-
-  uint8_t index = 0;
-  for (int i = 0; i < BACKLIGHT_MODE_COUNT; i++) {
-    if (value == backlight_mode_value[i]) {
-      index = i;
-      break;
-    } else {
-      index = 0;
+static uint8_t ublox_scene_data_display_config_next_backlight_mode(
+    const UbloxDataDisplayBacklightMode value,
+    void* context) {
+    furi_assert(context);
+
+    uint8_t index = 0;
+    for(int i = 0; i < BACKLIGHT_MODE_COUNT; i++) {
+        if(value == backlight_mode_value[i]) {
+            index = i;
+            break;
+        } else {
+            index = 0;
+        }
     }
-  }
-  return index;
+    return index;
 }
 
-
 static void ublox_scene_data_display_config_set_backlight_mode(VariableItem* item) {
-  Ublox* ublox = variable_item_get_context(item);
-  uint8_t index = variable_item_get_current_value_index(item);
+    Ublox* ublox = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
 
-  variable_item_set_current_value_text(item, backlight_mode_text[index]);
-  (ublox->data_display_state).backlight_mode = backlight_mode_value[index];
+    variable_item_set_current_value_text(item, backlight_mode_text[index]);
+    (ublox->data_display_state).backlight_mode = backlight_mode_value[index];
 
-  if ((ublox->data_display_state).backlight_mode == UbloxDataDisplayBacklightOn) {
-    notification_message_block(ublox->notifications, &sequence_display_backlight_enforce_on);
-  } else if ((ublox->data_display_state).backlight_mode == UbloxDataDisplayBacklightDefault) {
-    notification_message_block(ublox->notifications, &sequence_display_backlight_enforce_auto);
-  }
+    if((ublox->data_display_state).backlight_mode == UbloxDataDisplayBacklightOn) {
+        notification_message_block(ublox->notifications, &sequence_display_backlight_enforce_on);
+    } else if((ublox->data_display_state).backlight_mode == UbloxDataDisplayBacklightDefault) {
+        notification_message_block(ublox->notifications, &sequence_display_backlight_enforce_auto);
+    }
 }
 
-static uint8_t ublox_scene_data_display_config_next_display_view_mode(const UbloxDataDisplayViewMode value, void* context) {
-  furi_assert(context);
-
-  uint8_t index = 0;
-  for (int i = 0; i < DISPLAY_VIEW_MODE_COUNT; i++) {
-    if (value == display_view_mode_value[i]) {
-      index = i;
-      break;
-    } else {
-      index = 0;
+static uint8_t ublox_scene_data_display_config_next_display_view_mode(
+    const UbloxDataDisplayViewMode value,
+    void* context) {
+    furi_assert(context);
+
+    uint8_t index = 0;
+    for(int i = 0; i < DISPLAY_VIEW_MODE_COUNT; i++) {
+        if(value == display_view_mode_value[i]) {
+            index = i;
+            break;
+        } else {
+            index = 0;
+        }
     }
-  }
-  return index;
+    return index;
 }
-    
+
 static void ublox_scene_data_display_config_set_display_view_mode(VariableItem* item) {
-  Ublox* ublox = variable_item_get_context(item);
-  uint8_t index = variable_item_get_current_value_index(item);
+    Ublox* ublox = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
 
-  variable_item_set_current_value_text(item, display_view_mode_text[index]);
-  (ublox->data_display_state).view_mode = display_view_mode_value[index];
+    variable_item_set_current_value_text(item, display_view_mode_text[index]);
+    (ublox->data_display_state).view_mode = display_view_mode_value[index];
 }
 
-static uint8_t ublox_scene_data_display_config_next_notify_mode(const UbloxDataDisplayNotifyMode value, void* context) {
-  furi_assert(context);
-
-  uint8_t index = 0;
-  for (int i = 0; i < NOTIFY_MODE_COUNT; i++) {
-    if (value == notify_mode_values[i]) {
-      index = i;
-      break;
-    } else {
-      index = 0;
+static uint8_t ublox_scene_data_display_config_next_notify_mode(
+    const UbloxDataDisplayNotifyMode value,
+    void* context) {
+    furi_assert(context);
+
+    uint8_t index = 0;
+    for(int i = 0; i < NOTIFY_MODE_COUNT; i++) {
+        if(value == notify_mode_values[i]) {
+            index = i;
+            break;
+        } else {
+            index = 0;
+        }
     }
-  }
-  return index;
+    return index;
 }
 
 static void ublox_scene_data_display_config_set_notify_mode(VariableItem* item) {
-  Ublox* ublox = variable_item_get_context(item);
-  uint8_t index = variable_item_get_current_value_index(item);
+    Ublox* ublox = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
 
-  variable_item_set_current_value_text(item, notify_mode_text[index]);
-  (ublox->data_display_state).notify_mode = notify_mode_values[index];
+    variable_item_set_current_value_text(item, notify_mode_text[index]);
+    (ublox->data_display_state).notify_mode = notify_mode_values[index];
 }
 
-static uint8_t ublox_scene_data_display_config_next_odometer_mode(const UbloxOdometerMode value, void* context) {
-  furi_assert(context);
-
-  uint8_t index = 0;
-  for (int i = 0; i < ODOMETER_MODE_COUNT; i++) {
-    if (value == odometer_mode_values[i]) {
-      index = i;
-      break;
-    } else {
-      index = 0;
+static uint8_t ublox_scene_data_display_config_next_odometer_mode(
+    const UbloxOdometerMode value,
+    void* context) {
+    furi_assert(context);
+
+    uint8_t index = 0;
+    for(int i = 0; i < ODOMETER_MODE_COUNT; i++) {
+        if(value == odometer_mode_values[i]) {
+            index = i;
+            break;
+        } else {
+            index = 0;
+        }
     }
-  }
-  return index;
+    return index;
 }
 
 static void ublox_scene_data_display_config_set_odometer_mode(VariableItem* item) {
-  Ublox* ublox = variable_item_get_context(item);
-  uint8_t index = variable_item_get_current_value_index(item);
+    Ublox* ublox = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
 
-  variable_item_set_current_value_text(item, odometer_mode_text[index]);
-  (ublox->device_state).odometer_mode = odometer_mode_values[index];
-  // device has to be re-configured at next read
-  ublox->gps_initted = false;
+    variable_item_set_current_value_text(item, odometer_mode_text[index]);
+    (ublox->device_state).odometer_mode = odometer_mode_values[index];
+    // device has to be re-configured at next read
+    ublox->gps_initted = false;
 }
 
-static uint8_t ublox_scene_data_display_config_next_platform_model(const UbloxPlatformModel value, void* context) {
-  furi_assert(context);
-
-  uint8_t index = 0;
-  for (int i = 0; i < PLATFORM_MODEL_COUNT; i++) {
-    if (value == platform_model_values[i]) {
-      index = i;
-      break;
-    } else {
-      index = 0;
+static uint8_t ublox_scene_data_display_config_next_platform_model(
+    const UbloxPlatformModel value,
+    void* context) {
+    furi_assert(context);
+
+    uint8_t index = 0;
+    for(int i = 0; i < PLATFORM_MODEL_COUNT; i++) {
+        if(value == platform_model_values[i]) {
+            index = i;
+            break;
+        } else {
+            index = 0;
+        }
     }
-  }
-  return index;
+    return index;
 }
 
 static void ublox_scene_data_display_config_set_platform_model(VariableItem* item) {
-  Ublox* ublox = variable_item_get_context(item);
-  uint8_t index = variable_item_get_current_value_index(item);
+    Ublox* ublox = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
 
-  variable_item_set_current_value_text(item, platform_model_text[index]);
-  (ublox->device_state).platform_model = platform_model_values[index];
-  // device has to be re-configured at next read
-  ublox->gps_initted = false;
+    variable_item_set_current_value_text(item, platform_model_text[index]);
+    (ublox->device_state).platform_model = platform_model_values[index];
+    // device has to be re-configured at next read
+    ublox->gps_initted = false;
+}
+
+static void ublox_scene_data_display_config_enter_callback(void* context, uint32_t index) {
+    Ublox* ublox = context;
+    if(index == UbloxSettingIndexResetOdometer) {
+	view_dispatcher_send_custom_event(ublox->view_dispatcher, UbloxCustomEventResetOdometer);
+    }
 }
 
 void ublox_scene_data_display_config_on_enter(void* context) {
-  Ublox* ublox = context;
-  VariableItem* item;
-  uint8_t value_index;
-
-  item = variable_item_list_add(ublox->variable_item_list,
-				"Refresh Rate:",
-				REFRESH_RATE_COUNT,
-				ublox_scene_data_display_config_set_refresh_rate,
-				ublox);
-
-  value_index = ublox_scene_data_display_config_next_refresh_rate((ublox->data_display_state).refresh_rate,
-								  ublox);
-  variable_item_set_current_value_index(item, value_index);
-  variable_item_set_current_value_text(item, refresh_rate_text[value_index]);
-  
-  item = variable_item_list_add(ublox->variable_item_list,
-				"Backlight:",
-				BACKLIGHT_MODE_COUNT,
-				ublox_scene_data_display_config_set_backlight_mode,
-				ublox);
-  value_index = ublox_scene_data_display_config_next_backlight_mode((ublox->data_display_state).backlight_mode,
-								    ublox);
-  variable_item_set_current_value_index(item, value_index);
-  variable_item_set_current_value_text(item, backlight_mode_text[value_index]);
-  
-  
-  item = variable_item_list_add(ublox->variable_item_list,
-				"Display Mode:",
-				DISPLAY_VIEW_MODE_COUNT,
-				ublox_scene_data_display_config_set_display_view_mode,
-				ublox);
-  value_index = ublox_scene_data_display_config_next_display_view_mode((ublox->data_display_state).view_mode,
-								       ublox);
-  variable_item_set_current_value_index(item, value_index);
-  variable_item_set_current_value_text(item, display_view_mode_text[value_index]);
-
-  item = variable_item_list_add(ublox->variable_item_list,
-				"Notify:",
-				NOTIFY_MODE_COUNT,
-				ublox_scene_data_display_config_set_notify_mode,
-				ublox);
-  value_index = ublox_scene_data_display_config_next_notify_mode((ublox->data_display_state).notify_mode,
-								 ublox);
-  variable_item_set_current_value_index(item, value_index);
-  variable_item_set_current_value_text(item, notify_mode_text[value_index]);
-
-  item = variable_item_list_add(ublox->variable_item_list,
-				"Platform Model:",
-				PLATFORM_MODEL_COUNT,
-				ublox_scene_data_display_config_set_platform_model,
-				ublox);
-  value_index = ublox_scene_data_display_config_next_platform_model((ublox->device_state).platform_model,
-								   ublox);
-  variable_item_set_current_value_index(item, value_index);
-  variable_item_set_current_value_text(item, platform_model_text[value_index]);
-  
-  item = variable_item_list_add(ublox->variable_item_list,
-				"Odo Mode:",
-				ODOMETER_MODE_COUNT,
-				ublox_scene_data_display_config_set_odometer_mode,
-				ublox);
-  value_index = ublox_scene_data_display_config_next_odometer_mode((ublox->device_state).odometer_mode,
-								   ublox);
-  variable_item_set_current_value_index(item, value_index);
-  variable_item_set_current_value_text(item, odometer_mode_text[value_index]);
-
-  view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewVariableItemList);
+    Ublox* ublox = context;
+    VariableItem* item;
+    uint8_t value_index;
+
+    item = variable_item_list_add(
+        ublox->variable_item_list,
+        "Refresh Rate:",
+        REFRESH_RATE_COUNT,
+        ublox_scene_data_display_config_set_refresh_rate,
+        ublox);
+
+    value_index = ublox_scene_data_display_config_next_refresh_rate(
+        (ublox->data_display_state).refresh_rate, ublox);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, refresh_rate_text[value_index]);
+
+    item = variable_item_list_add(
+        ublox->variable_item_list,
+        "Backlight:",
+        BACKLIGHT_MODE_COUNT,
+        ublox_scene_data_display_config_set_backlight_mode,
+        ublox);
+    value_index = ublox_scene_data_display_config_next_backlight_mode(
+        (ublox->data_display_state).backlight_mode, ublox);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, backlight_mode_text[value_index]);
+
+    item = variable_item_list_add(
+        ublox->variable_item_list,
+        "Display Mode:",
+        DISPLAY_VIEW_MODE_COUNT,
+        ublox_scene_data_display_config_set_display_view_mode,
+        ublox);
+    value_index = ublox_scene_data_display_config_next_display_view_mode(
+        (ublox->data_display_state).view_mode, ublox);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, display_view_mode_text[value_index]);
+
+    item = variable_item_list_add(
+        ublox->variable_item_list,
+        "Notify:",
+        NOTIFY_MODE_COUNT,
+        ublox_scene_data_display_config_set_notify_mode,
+        ublox);
+    value_index = ublox_scene_data_display_config_next_notify_mode(
+        (ublox->data_display_state).notify_mode, ublox);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, notify_mode_text[value_index]);
+
+    item = variable_item_list_add(
+        ublox->variable_item_list,
+        "Platform Model:",
+        PLATFORM_MODEL_COUNT,
+        ublox_scene_data_display_config_set_platform_model,
+        ublox);
+    value_index = ublox_scene_data_display_config_next_platform_model(
+        (ublox->device_state).platform_model, ublox);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, platform_model_text[value_index]);
+
+    item = variable_item_list_add(
+        ublox->variable_item_list,
+        "Odo Mode:",
+        ODOMETER_MODE_COUNT,
+        ublox_scene_data_display_config_set_odometer_mode,
+        ublox);
+    value_index = ublox_scene_data_display_config_next_odometer_mode(
+        (ublox->device_state).odometer_mode, ublox);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, odometer_mode_text[value_index]);
+
+    item = variable_item_list_add(
+				  ublox->variable_item_list,
+				  "Reset Odometer",
+				  1, NULL, NULL);
+    variable_item_list_set_enter_callback(ublox->variable_item_list,
+					  ublox_scene_data_display_config_enter_callback,
+					  ublox);
+    view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewVariableItemList);
 }
 
+void ublox_scene_data_display_config_worker_callback(UbloxWorkerEvent event, void* context) {
+    Ublox* ublox = context;
+
+    view_dispatcher_send_custom_event(ublox->view_dispatcher, event);
+}
 
 bool ublox_scene_data_display_config_on_event(void* context, SceneManagerEvent event) {
-  UNUSED(context);
-  UNUSED(event);
-  return false;
+    Ublox* ublox = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+	if(event.event == UbloxCustomEventResetOdometer) {
+	    ublox_worker_start(
+                ublox->worker,
+                UbloxWorkerStateResetOdometer,
+                ublox_scene_data_display_config_worker_callback,
+                ublox);
+	    // don't consume, we want to stay here
+	} else if(event.event == UbloxWorkerEventOdoReset) {
+	    if((ublox->data_display_state).notify_mode == UbloxDataDisplayNotifyOn) {
+                notification_message(ublox->notifications, &sequence_new_reading);
+            }
+	    FURI_LOG_I(TAG, "odometer reset done");
+	}
+    }
+
+    return consumed;
 }
 
 void ublox_scene_data_display_config_on_exit(void* context) {
-  Ublox* ublox = context;
-  variable_item_list_set_selected_item(ublox->variable_item_list, 0);
-  variable_item_list_reset(ublox->variable_item_list);
+    Ublox* ublox = context;
+    variable_item_list_set_selected_item(ublox->variable_item_list, 0);
+    variable_item_list_reset(ublox->variable_item_list);
 }
-
-  

+ 85 - 0
non_catalog_apps/ublox/scenes/ublox_scene_enter_file_name.c

@@ -0,0 +1,85 @@
+// TODO: rename this scene to _save_name
+
+#include "../ublox_i.h"
+
+#define TAG "ublox_scene_enter_file_name"
+
+void ublox_text_input_callback(void* context) {
+    Ublox* ublox = context;
+
+    view_dispatcher_send_custom_event(ublox->view_dispatcher, UbloxCustomEventTextInputDone);
+}
+
+FuriString* ublox_scene_enter_file_name_get_timename() {
+    FuriHalRtcDateTime datetime;
+    furi_hal_rtc_get_datetime(&datetime);
+    FuriString* s = furi_string_alloc();
+    
+    // YMD sorts better
+    furi_string_printf(s,
+		       "gps-%.4d%.2d%.2d-%.2d%.2d%.2d.kml",
+		       datetime.year,
+		       datetime.month,
+		       datetime.day,
+		       datetime.hour,
+		       datetime.minute,
+		       datetime.second);
+    return s;
+}
+
+void ublox_scene_enter_file_name_on_enter(void* context) {
+    Ublox* ublox = context;
+    TextInput* text_input = ublox->text_input;
+
+    text_input_set_header_text(text_input, "Enter KML log file name");
+    text_input_set_result_callback(text_input,
+				   ublox_text_input_callback,
+				   context,
+				   ublox->text_store,
+				   100,
+				   false);
+
+    FuriString* fname = ublox_scene_enter_file_name_get_timename();
+    strcpy(ublox->text_store, furi_string_get_cstr(fname));
+
+
+    //FuriString* full_fname = furi_string_alloc_set(folder_path);
+
+    ValidatorIsFile* validator_is_file =
+	// app path folder, app extension, current file name
+	validator_is_file_alloc_init(furi_string_get_cstr(ublox->logfile_folder),
+				     UBLOX_KML_EXTENSION,
+				     "");
+
+    text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
+    
+    furi_string_free(fname);
+    view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewTextInput);
+    
+}
+
+bool ublox_scene_enter_file_name_on_event(void* context, SceneManagerEvent event) {
+    Ublox* ublox = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+	if(event.event == UbloxCustomEventTextInputDone) {
+	    //FuriString* fullname;
+	    FURI_LOG_I(TAG, "text: %s", ublox->text_store);
+	    ublox->log_state = UbloxLogStateStartLogging;
+	    //scene_manager_next_scene(ublox->scene_manager, UbloxSceneDataDisplay);
+	    // don't add data_display as the next scene, instead go back to the last scene
+	    scene_manager_previous_scene(ublox->scene_manager);
+	    consumed = true;
+	}
+    }
+
+    return consumed;
+}
+
+void ublox_scene_enter_file_name_on_exit(void* context) {
+    UNUSED(context);
+}
+
+
+										  

+ 53 - 35
non_catalog_apps/ublox/scenes/ublox_scene_start.c

@@ -1,57 +1,75 @@
 #include "../ublox_i.h"
 
 enum SubmenuIndex {
-  SubmenuIndexDataDisplay,
-  SubmenuIndexWiring,
-  SubmenuIndexAbout,
+    SubmenuIndexDataDisplay,
+    SubmenuIndexSyncTime,
+    SubmenuIndexWiring,
+    SubmenuIndexAbout,
 };
 
 void ublox_scene_start_submenu_callback(void* context, uint32_t index) {
-  Ublox* ublox = context;
+    Ublox* ublox = context;
 
-  view_dispatcher_send_custom_event(ublox->view_dispatcher, index);
+    view_dispatcher_send_custom_event(ublox->view_dispatcher, index);
 }
 
 void ublox_scene_start_on_enter(void* context) {
-  Ublox* ublox = context;
-  Submenu* submenu = ublox->submenu;
+    Ublox* ublox = context;
+    Submenu* submenu = ublox->submenu;
 
-  submenu_add_item(submenu, "Data Display", SubmenuIndexDataDisplay, ublox_scene_start_submenu_callback, ublox);
-  submenu_add_item(submenu, "Wiring", SubmenuIndexWiring, ublox_scene_start_submenu_callback, ublox);
-  submenu_add_item(submenu, "About", SubmenuIndexAbout, ublox_scene_start_submenu_callback, ublox);
-  
-  submenu_set_selected_item(submenu, scene_manager_get_scene_state(ublox->scene_manager, UbloxSceneStart));
+    submenu_add_item(
+        submenu,
+        "Data Display",
+        SubmenuIndexDataDisplay,
+        ublox_scene_start_submenu_callback,
+        ublox);
+    submenu_add_item(
+        submenu, "Sync Time to GPS", SubmenuIndexSyncTime, ublox_scene_start_submenu_callback, ublox);
+    submenu_add_item(
+        submenu, "Wiring", SubmenuIndexWiring, ublox_scene_start_submenu_callback, ublox);
+    submenu_add_item(
+        submenu, "About", SubmenuIndexAbout, ublox_scene_start_submenu_callback, ublox);
 
-  view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewMenu);
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(ublox->scene_manager, UbloxSceneStart));
+
+    view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewMenu);
 }
 
 bool ublox_scene_start_on_event(void* context, SceneManagerEvent event) {
-  Ublox* ublox = context;
-  UNUSED(ublox);
-  bool consumed = false;
-  
-  if (event.type == SceneManagerEventTypeCustom) {
-    if (event.event == SubmenuIndexDataDisplay) {
-      scene_manager_set_scene_state(ublox->scene_manager, UbloxSceneDataDisplay, SubmenuIndexDataDisplay);
-      scene_manager_next_scene(ublox->scene_manager, UbloxSceneDataDisplay);
-      consumed = true;
-    } else if (event.event == SubmenuIndexWiring) {
-      scene_manager_set_scene_state(ublox->scene_manager, UbloxSceneWiring, SubmenuIndexWiring);
-      scene_manager_next_scene(ublox->scene_manager, UbloxSceneWiring);
-      consumed = true;
-    } else if (event.event == SubmenuIndexAbout) {
-      scene_manager_set_scene_state(ublox->scene_manager, UbloxSceneAbout, SubmenuIndexAbout);
-      scene_manager_next_scene(ublox->scene_manager, UbloxSceneAbout);
-      consumed = true;
+    Ublox* ublox = context;
+    UNUSED(ublox);
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexDataDisplay) {
+            scene_manager_set_scene_state(
+                ublox->scene_manager, UbloxSceneStart, SubmenuIndexDataDisplay);
+            scene_manager_next_scene(ublox->scene_manager, UbloxSceneDataDisplay);
+            consumed = true;
+        } else if(event.event == SubmenuIndexWiring) {
+            scene_manager_set_scene_state(
+                ublox->scene_manager, UbloxSceneStart, SubmenuIndexWiring);
+            scene_manager_next_scene(ublox->scene_manager, UbloxSceneWiring);
+            consumed = true;
+	} else if(event.event == SubmenuIndexSyncTime) {
+	    scene_manager_set_scene_state(
+                ublox->scene_manager, UbloxSceneStart, SubmenuIndexSyncTime);
+            scene_manager_next_scene(ublox->scene_manager, UbloxSceneSyncTime);
+            consumed = true;
+        } else if(event.event == SubmenuIndexAbout) {
+            scene_manager_set_scene_state(
+                ublox->scene_manager, UbloxSceneStart, SubmenuIndexAbout);
+            scene_manager_next_scene(ublox->scene_manager, UbloxSceneAbout);
+            consumed = true;
+        }
     }
-  }
 
-  return consumed;
+    return consumed;
 }
 
 void ublox_scene_start_on_exit(void* context) {
-  Ublox* ublox = context;
+    Ublox* ublox = context;
 
-  submenu_reset(ublox->submenu);
+    submenu_reset(ublox->submenu);
 }
-    

+ 96 - 0
non_catalog_apps/ublox/scenes/ublox_scene_sync_time.c

@@ -0,0 +1,96 @@
+#include "../ublox_i.h"
+#include "../ublox_worker_i.h"
+
+#define TAG "ublox_scene_sync_time"
+
+void ublox_scene_sync_time_worker_callback(UbloxWorkerEvent event, void* context) {
+    Ublox* ublox = context;
+
+    view_dispatcher_send_custom_event(ublox->view_dispatcher, event);
+}
+
+void ublox_scene_sync_time_on_enter(void* context) {
+    Ublox* ublox = context;
+
+    view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewWidget);
+
+    widget_add_string_element(ublox->widget,
+			      3, 5,
+			      AlignLeft, AlignCenter,
+			      FontPrimary,
+			      "Syncing time...");
+    
+    ublox_worker_start(ublox->worker, UbloxWorkerStateSyncTime, ublox_scene_sync_time_worker_callback, ublox);
+}
+
+bool ublox_scene_sync_time_on_event(void* context, SceneManagerEvent event) {
+    Ublox* ublox = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+	if(event.event == UbloxWorkerEventDataReady) {
+	    widget_reset(ublox->widget);
+	    // We don't have a timezone (or even UTC offset) in the
+	    // RTC, so we can only update the minute and second---not
+	    // even the date.
+	    FuriHalRtcDateTime datetime;
+	    furi_hal_rtc_get_datetime(&datetime);
+	    datetime.minute = (ublox->nav_timeutc).min;
+	    datetime.second = (ublox->nav_timeutc).sec;
+	    furi_hal_rtc_set_datetime(&datetime);
+
+	    widget_add_string_element(ublox->widget,
+				      3, 5,
+				      AlignLeft, AlignCenter,
+				      FontPrimary,
+				      "Updated min/sec to GPS");
+	    
+	    FuriString* s = furi_string_alloc();
+	    furi_string_cat_printf(s, "New date/time: ");
+
+	    FuriString* date = furi_string_alloc();
+	    locale_format_date(date, &datetime, locale_get_date_format(), "/");
+	    furi_string_cat_printf(date, " ");
+	    FuriString* time = furi_string_alloc();
+	    locale_format_time(time, &datetime, locale_get_time_format(), ":");
+
+	    furi_string_cat(date, time);
+	    widget_add_string_element(ublox->widget,
+				      3, 25,
+				      AlignLeft, AlignTop,
+				      FontSecondary,
+				      furi_string_get_cstr(s));
+	    widget_add_string_element(ublox->widget,
+				      3, 35,
+				      AlignLeft, AlignTop,
+				      FontSecondary,
+				      furi_string_get_cstr(date));
+	    furi_string_free(time);
+	    furi_string_free(date);
+	    furi_string_free(s);
+	} else if(event.event == UbloxWorkerEventFailed) {
+	    widget_reset(ublox->widget);
+	    widget_add_string_element(ublox->widget,
+				      3, 5,
+				      AlignLeft, AlignCenter,
+				      FontPrimary,
+				      "Syncing time...failed!");
+	    widget_add_string_element(ublox->widget,
+				      3, 20,
+				      AlignLeft, AlignCenter,
+				      FontSecondary,
+				      "No GPS found!");
+	}
+	    
+    }
+
+    return consumed;
+}
+
+void ublox_scene_sync_time_on_exit(void* context) {
+    Ublox* ublox = context;
+    
+    ublox_worker_stop(ublox->worker);
+
+    widget_reset(ublox->widget);
+}

+ 10 - 10
non_catalog_apps/ublox/scenes/ublox_scene_wiring.c

@@ -1,22 +1,22 @@
 #include "../ublox_i.h"
 
 void ublox_scene_wiring_on_enter(void* context) {
-  furi_assert(context);
+    furi_assert(context);
 
-  Ublox* ublox = context;
-  widget_add_icon_element(ublox->widget, 0, 0, &I_ublox_wiring);
-  view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewWidget);
+    Ublox* ublox = context;
+    widget_add_icon_element(ublox->widget, 0, 0, &I_ublox_wiring);
+    view_dispatcher_switch_to_view(ublox->view_dispatcher, UbloxViewWidget);
 }
 
 bool ublox_scene_wiring_on_event(void* context, SceneManagerEvent event) {
-  UNUSED(context);
-  UNUSED(event);
-  return false;
+    UNUSED(context);
+    UNUSED(event);
+    return false;
 }
 
 void ublox_scene_wiring_on_exit(void* context) {
-  furi_assert(context);
+    furi_assert(context);
 
-  Ublox* ublox = context;
-  widget_reset(ublox->widget);
+    Ublox* ublox = context;
+    widget_reset(ublox->widget);
 }

+ 115 - 70
non_catalog_apps/ublox/ublox.c

@@ -1,102 +1,147 @@
 #include "ublox_i.h"
 
+const NotificationSequence sequence_new_reading = {
+    //&message_vibro_on,
+    &message_green_255,
+    &message_delay_100,
+    &message_green_0,
+    //&message_vibro_off,
+    NULL,
+};
+
+
 bool ublox_custom_event_callback(void* context, uint32_t event) {
-  furi_assert(context);
-  Ublox* ublox = context;
-  return scene_manager_handle_custom_event(ublox->scene_manager, event);
+    furi_assert(context);
+    Ublox* ublox = context;
+    return scene_manager_handle_custom_event(ublox->scene_manager, event);
 }
 
 bool ublox_back_event_callback(void* context) {
-  furi_assert(context);
-  Ublox* ublox = context;
-  return scene_manager_handle_back_event(ublox->scene_manager);
+    furi_assert(context);
+    Ublox* ublox = context;
+    return scene_manager_handle_back_event(ublox->scene_manager);
 }
 
 Ublox* ublox_alloc() {
-  Ublox* ublox = malloc(sizeof(Ublox));
-
-  ublox->view_dispatcher = view_dispatcher_alloc();
-  ublox->scene_manager = scene_manager_alloc(&ublox_scene_handlers, ublox);
-  view_dispatcher_enable_queue(ublox->view_dispatcher);
-  view_dispatcher_set_event_callback_context(ublox->view_dispatcher, ublox);
-  view_dispatcher_set_custom_event_callback(ublox->view_dispatcher, ublox_custom_event_callback);
-  view_dispatcher_set_navigation_event_callback(ublox->view_dispatcher, ublox_back_event_callback);
-
-  ublox->worker = ublox_worker_alloc();
-  
-  ublox->gui = furi_record_open(RECORD_GUI);
-
-  ublox->submenu = submenu_alloc();
-  view_dispatcher_add_view(ublox->view_dispatcher, UbloxViewMenu, submenu_get_view(ublox->submenu));
-
-  ublox->widget = widget_alloc();
-  view_dispatcher_add_view(ublox->view_dispatcher, UbloxViewWidget, widget_get_view(ublox->widget));
-
-  ublox->data_display = data_display_alloc();
-  view_dispatcher_add_view(ublox->view_dispatcher, UbloxViewDataDisplay, data_display_get_view(ublox->data_display));
+    Ublox* ublox = malloc(sizeof(Ublox));
+
+    ublox->view_dispatcher = view_dispatcher_alloc();
+    ublox->scene_manager = scene_manager_alloc(&ublox_scene_handlers, ublox);
+    view_dispatcher_enable_queue(ublox->view_dispatcher);
+    view_dispatcher_set_event_callback_context(ublox->view_dispatcher, ublox);
+    view_dispatcher_set_custom_event_callback(ublox->view_dispatcher, ublox_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        ublox->view_dispatcher, ublox_back_event_callback);
+
+    ublox->worker = ublox_worker_alloc();
+
+    ublox->gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(
+        ublox->view_dispatcher, ublox->gui, ViewDispatcherTypeFullscreen);
+
+    ublox->submenu = submenu_alloc();
+    view_dispatcher_add_view(
+        ublox->view_dispatcher, UbloxViewMenu, submenu_get_view(ublox->submenu));
+
+    ublox->widget = widget_alloc();
+    view_dispatcher_add_view(
+        ublox->view_dispatcher, UbloxViewWidget, widget_get_view(ublox->widget));
+
+    ublox->variable_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        ublox->view_dispatcher,
+        UbloxViewVariableItemList,
+        variable_item_list_get_view(ublox->variable_item_list));
+
+    ublox->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        ublox->view_dispatcher, UbloxViewTextInput, text_input_get_view(ublox->text_input));
+    
+    ublox->data_display = data_display_alloc();
+    view_dispatcher_add_view(
+        ublox->view_dispatcher, UbloxViewDataDisplay, data_display_get_view(ublox->data_display));
 
-  ublox->variable_item_list = variable_item_list_alloc();
-  view_dispatcher_add_view(ublox->view_dispatcher, UbloxViewVariableItemList, variable_item_list_get_view(ublox->variable_item_list));
+    ublox->notifications = furi_record_open(RECORD_NOTIFICATION);
+    ublox->storage = furi_record_open(RECORD_STORAGE);
 
-  ublox->notifications = furi_record_open(RECORD_NOTIFICATION);
+    ublox->log_state = UbloxLogStateNone;
+    // default to "/data", which maps to "/ext/apps_data/ublox"
+    ublox->logfile_folder = furi_string_alloc_set(STORAGE_APP_DATA_PATH_PREFIX);
+    
+    // Establish default data display state
+    (ublox->data_display_state).view_mode = UbloxDataDisplayViewModeHandheld;
+    (ublox->data_display_state).backlight_mode = UbloxDataDisplayBacklightDefault;
+    (ublox->data_display_state).refresh_rate = 2;
+    (ublox->data_display_state).notify_mode = UbloxDataDisplayNotifyOn;
 
-  // Establish default data display state
-  (ublox->data_display_state).view_mode = UbloxDataDisplayViewModeHandheld;
-  (ublox->data_display_state).backlight_mode = UbloxDataDisplayBacklightDefault;
-  (ublox->data_display_state).refresh_rate = 2;
-  (ublox->data_display_state).notify_mode = UbloxDataDisplayNotifyOn;
+    (ublox->device_state).odometer_mode = UbloxOdometerModeRunning;
+    // "suitable for most applications" according to u-blox.
+    (ublox->device_state).platform_model = UbloxPlatformModelPortable;
+    ublox->gps_initted = false;
 
-  (ublox->device_state).odometer_mode = UbloxOdometerModeRunning;
-  // "suitable for most applications" according to u-blox.
-  (ublox->device_state).platform_model = UbloxPlatformModelPortable;
-  ublox->gps_initted = false;
-  return ublox;
+    
+    return ublox;
 }
 
+#define TAG "ublox"
+//#include "ublox_worker_i.h"
 void ublox_free(Ublox* ublox) {
-  furi_assert(ublox);
+    furi_assert(ublox);
 
-  ublox_worker_stop(ublox->worker);
-  ublox_worker_free(ublox->worker);
+    // no need to stop the worker, plus it causes the app to crash by NULL
+    // pointer dereference from context in the worker struct
     
-  view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewMenu);
-  submenu_free(ublox->submenu);
+    //FURI_LOG_I(TAG, "stop worker");
+    //ublox_worker_stop(ublox->worker);
+    //FURI_LOG_I(TAG, "%p", ublox->worker->context);
+    FURI_LOG_I(TAG, "free worker");
+    ublox_worker_free(ublox->worker);
 
-  view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewWidget);
-  widget_free(ublox->widget);
+    view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewMenu);
+    submenu_free(ublox->submenu);
 
-  view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewDataDisplay);
-  data_display_free(ublox->data_display);
+    view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewWidget);
+    widget_free(ublox->widget);
 
-  view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewVariableItemList);
-  variable_item_list_free(ublox->variable_item_list);
-  
-  view_dispatcher_free(ublox->view_dispatcher);
+    view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewDataDisplay);
+    data_display_free(ublox->data_display);
 
-  scene_manager_free(ublox->scene_manager);
+    view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewVariableItemList);
+    variable_item_list_free(ublox->variable_item_list);
+
+    view_dispatcher_remove_view(ublox->view_dispatcher, UbloxViewTextInput);
+    text_input_free(ublox->text_input);
+    
+    view_dispatcher_free(ublox->view_dispatcher);
 
-  furi_record_close(RECORD_GUI);
-  furi_record_close(RECORD_NOTIFICATION);
-  ublox->gui = NULL;
+    scene_manager_free(ublox->scene_manager);
 
-  free(ublox);
+    furi_record_close(RECORD_GUI);
+    ublox->gui = NULL;
+    furi_record_close(RECORD_NOTIFICATION);
+    ublox->notifications = NULL;
+    furi_record_close(RECORD_STORAGE);
+    ublox->storage = NULL;
+
+    if(ublox->logfile_folder != NULL) {
+	furi_string_free(ublox->logfile_folder);
+    }
+    free(ublox);
 }
 
 int32_t ublox_app(void* p) {
-  UNUSED(p);
-  
-  Ublox* ublox = ublox_alloc();
+    UNUSED(p);
+
+    Ublox* ublox = ublox_alloc();
+
+    scene_manager_next_scene(ublox->scene_manager, UbloxSceneStart);
 
-  view_dispatcher_attach_to_gui(ublox->view_dispatcher, ublox->gui, ViewDispatcherTypeFullscreen);
-  
-  scene_manager_next_scene(ublox->scene_manager, UbloxSceneStart);
+    view_dispatcher_run(ublox->view_dispatcher);
 
-  view_dispatcher_run(ublox->view_dispatcher);
-  
-  // force restore the default backlight
-  notification_message_block(ublox->notifications, &sequence_display_backlight_enforce_auto);
+    // force restore the default backlight on exit
+    notification_message_block(ublox->notifications, &sequence_display_backlight_enforce_auto);
 
-  ublox_free(ublox);
+    ublox_free(ublox);
 
-  return 0;
+    return 0;
 }

+ 104 - 109
non_catalog_apps/ublox/ublox_device.c

@@ -6,136 +6,131 @@
 #define TAG "ublox_device"
 
 UbloxMessage* ublox_frame_to_bytes(UbloxFrame* frame) {
-  uint32_t message_size = 8 + frame->len;
-  // Found the issue! frame_bytes isn't being freed. This function
-  // should always have returned a pointer.
-  uint8_t* frame_bytes = malloc(message_size);
-  frame->sync1 = 0xb5;
-  frame->sync2 = 0x62;
-  frame_bytes[0] = frame->sync1;
-  frame_bytes[1] = frame->sync2;
-  frame_bytes[2] = frame->class;
-  frame_bytes[3] = frame->id;
-  frame_bytes[4] = frame->len & 0xff;
-  frame_bytes[5] = (frame->len & 0xff) >> 8;
-
-  
-  if (frame->len != 0) {
-    for (int i = 0; i < frame->len; i++) {
-      frame_bytes[6+i] = frame->payload[i];
+    uint32_t message_size = 8 + frame->len;
+    // Found the issue! frame_bytes isn't being freed. This function
+    // should always have returned a pointer.
+    uint8_t* frame_bytes = malloc(message_size);
+    frame->sync1 = 0xb5;
+    frame->sync2 = 0x62;
+    frame_bytes[0] = frame->sync1;
+    frame_bytes[1] = frame->sync2;
+    frame_bytes[2] = frame->class;
+    frame_bytes[3] = frame->id;
+    frame_bytes[4] = frame->len & 0xff;
+    frame_bytes[5] = (frame->len & 0xff) >> 8;
+
+    if(frame->len != 0) {
+        for(int i = 0; i < frame->len; i++) {
+            frame_bytes[6 + i] = frame->payload[i];
+        }
     }
-  }
 
-  frame->ck_a = 0;
-  frame->ck_b = 0;
-  // checksum is calculated over class, id, length, and payload
-  for (int i = 2; i < 2 + (frame->len+4); i++) {
-    frame->ck_a = frame->ck_a + frame_bytes[i];
-    frame->ck_b = frame->ck_b + frame->ck_a;
-  }
+    frame->ck_a = 0;
+    frame->ck_b = 0;
+    // checksum is calculated over class, id, length, and payload
+    for(int i = 2; i < 2 + (frame->len + 4); i++) {
+        frame->ck_a = frame->ck_a + frame_bytes[i];
+        frame->ck_b = frame->ck_b + frame->ck_a;
+    }
 
-  frame_bytes[message_size - 2] = frame->ck_a;
-  frame_bytes[message_size - 1] = frame->ck_b;
+    frame_bytes[message_size - 2] = frame->ck_a;
+    frame_bytes[message_size - 1] = frame->ck_b;
 
-  UbloxMessage* m = malloc(sizeof(UbloxMessage));
-  m->message = frame_bytes;
-  m->length = message_size;
+    UbloxMessage* m = malloc(sizeof(UbloxMessage));
+    m->message = frame_bytes;
+    m->length = message_size;
 
-  return m;
+    return m;
 }
 
 void ublox_message_free(UbloxMessage* message) {
-  if (message != NULL) {
-    if (message->message != NULL) {
-      free(message->message);
-    } /*else {
+    if(message != NULL) {
+        if(message->message != NULL) {
+            free(message->message);
+        } /*else {
       FURI_LOG_I(TAG, "message free: message->message == NULL");
       }*/
-    free(message);
-  } /*else {
+        free(message);
+    } /*else {
     FURI_LOG_I(TAG, "message free: message == NULL");
     }*/
 }
 
-
 // Pointer, because we are assigning a pointer in the returned frame.
 UbloxFrame* ublox_bytes_to_frame(UbloxMessage* message) {
-  if (message->length < 8) {
-    FURI_LOG_I(TAG, "message length in bytes_to_frame < 8, = 0x%x", message->length);
-    // minimum 8 bytes in a message (message with no payload)
-    return NULL;
-  }
-  
-  UbloxFrame* frame = malloc(sizeof(UbloxFrame));
-
-  
-  if (message->message[0] != 0xb5) {
-    FURI_LOG_E(TAG, "message[0] != 0xb5, = 0x%x", message->message[0]);
-    free(frame);
-    return NULL;
-  }
-
-  frame->sync1 = message->message[0];
-
-  if (message->message[1] != 0x62) {
-    FURI_LOG_E(TAG, "Message[1] != 0x62, = 0x%x", message->message[1]);
-    free(frame);
-    return NULL;
-  }
-
-  frame->sync2 = message->message[1];
-
-  frame->class = message->message[2];
-  frame->id = message->message[3];
-
-  // little-endian
-  frame->len = (message->message[5] << 8) | (message->message[4]);
-
-  // frame->len must be initialized before malloc (duh, but I made that mistake...)
-  frame->payload = malloc(frame->len);
-  //FURI_LOG_I(TAG, "frame->len: %d", frame->len);
-  for (int i = 6; i < 6 + frame->len; i++) {
-    frame->payload[i - 6] = message->message[i];
-  }
-
-  frame->ck_a = message->message[6 + frame->len];
-  frame->ck_b = message->message[6 + frame->len+1];
-
-  // Test checksum
-  uint8_t ck_a = 0, ck_b = 0;
-  for (int i = 2; i < 2 + (frame->len+4); i++) {
-    ck_a = ck_a + message->message[i];
-    ck_b = ck_b + ck_a;
-  }
-
-  if (ck_a != frame->ck_a) {
-    FURI_LOG_E(TAG, "checksum A doesn't match! expected 0x%x, got 0x%x", ck_a, frame->ck_a);
-    free(frame);
-    free(frame->payload);
-    return NULL;
-  }
-
-  if (ck_b != frame->ck_b) {
-    FURI_LOG_E(TAG, "checksum B doesn't match! expected 0x%x, got 0x%x", ck_b, frame->ck_b);
-    free(frame);
-    free(frame->payload);
-    return NULL;
-  }
-
-  return frame;
+    if(message->length < 8) {
+        FURI_LOG_I(TAG, "message length in bytes_to_frame < 8, = 0x%x", message->length);
+        // minimum 8 bytes in a message (message with no payload)
+        return NULL;
+    }
+
+    UbloxFrame* frame = malloc(sizeof(UbloxFrame));
+
+    if(message->message[0] != 0xb5) {
+        FURI_LOG_E(TAG, "message[0] != 0xb5, = 0x%x", message->message[0]);
+        free(frame);
+        return NULL;
+    }
+
+    frame->sync1 = message->message[0];
+
+    if(message->message[1] != 0x62) {
+        FURI_LOG_E(TAG, "Message[1] != 0x62, = 0x%x", message->message[1]);
+        free(frame);
+        return NULL;
+    }
+
+    frame->sync2 = message->message[1];
+
+    frame->class = message->message[2];
+    frame->id = message->message[3];
+
+    // little-endian
+    frame->len = (message->message[5] << 8) | (message->message[4]);
+
+    // frame->len must be initialized before malloc (duh, but I made that mistake...)
+    frame->payload = malloc(frame->len);
+    //FURI_LOG_I(TAG, "frame->len: %d", frame->len);
+    for(int i = 6; i < 6 + frame->len; i++) {
+        frame->payload[i - 6] = message->message[i];
+    }
+
+    frame->ck_a = message->message[6 + frame->len];
+    frame->ck_b = message->message[6 + frame->len + 1];
+
+    // Test checksum
+    uint8_t ck_a = 0, ck_b = 0;
+    for(int i = 2; i < 2 + (frame->len + 4); i++) {
+        ck_a = ck_a + message->message[i];
+        ck_b = ck_b + ck_a;
+    }
+
+    if(ck_a != frame->ck_a) {
+        FURI_LOG_E(TAG, "checksum A doesn't match! expected 0x%x, got 0x%x", ck_a, frame->ck_a);
+        free(frame);
+        free(frame->payload);
+        return NULL;
+    }
+
+    if(ck_b != frame->ck_b) {
+        FURI_LOG_E(TAG, "checksum B doesn't match! expected 0x%x, got 0x%x", ck_b, frame->ck_b);
+        free(frame);
+        free(frame->payload);
+        return NULL;
+    }
+
+    return frame;
 }
 
-  
 void ublox_frame_free(UbloxFrame* frame) {
-  if (frame != NULL) {
-    if (frame->payload != NULL) {
-      free(frame->payload);
-    }/* else {
+    if(frame != NULL) {
+        if(frame->payload != NULL) {
+            free(frame->payload);
+        } /* else {
       FURI_LOG_I(TAG, "frame free: frame->payload == NULL");
       }*/
-    free(frame);
-  }/* else {
+        free(frame);
+    } /* else {
     FURI_LOG_I(TAG, "frame free: frame == NULL");
     }*/
 }
-

+ 80 - 65
non_catalog_apps/ublox/ublox_device.h

@@ -32,99 +32,114 @@
 // ACK_CLASS
 #define UBX_ACK_ACK_MESSAGE 0x01
 // ACK and NAK have the same length
-#define UBX_ACK_ACK_MESSAGE_LENGTH (8+2)
+#define UBX_ACK_ACK_MESSAGE_LENGTH (8 + 2)
 
 // NAV_CLASS
 #define UBX_NAV_PVT_MESSAGE 0x07
-#define UBX_NAV_PVT_MESSAGE_LENGTH (8+92)
+#define UBX_NAV_PVT_MESSAGE_LENGTH (8 + 92)
 #define UBX_NAV_SAT_MESSAGE 0x35
 #define UBX_NAV_ODO_MESSAGE 0x09
-#define UBX_NAV_ODO_MESSAGE_LENGTH (8+20)
+#define UBX_NAV_ODO_MESSAGE_LENGTH (8 + 20)
 #define UBX_NAV_RESETODO_MESSAGE 0x10
+#define UBX_NAV_TIMEUTC_MESSAGE 0x21
+#define UBX_NAV_TIMEUTC_MESSAGE_LENGTH (8 + 20)
 
 // CFG_CLASS
 #define UBX_CFG_PMS_MESSAGE 0x86
-#define UBX_CFG_PMS_MESSAGE_LENGTH (8+8)
+#define UBX_CFG_PMS_MESSAGE_LENGTH (8 + 8)
 #define UBX_CFG_ODO_MESSAGE 0x1e
-#define UBX_CFG_ODO_MESSAGE_LENGTH (8+20)
+#define UBX_CFG_ODO_MESSAGE_LENGTH (8 + 20)
 #define UBX_CFG_NAV5_MESSAGE 0x24
-#define UBX_CFG_NAV5_MESSAGE_LENGTH (8+36)
+#define UBX_CFG_NAV5_MESSAGE_LENGTH (8 + 36)
 /** A frame is a message sent to the GPS. This app supports u-blox 8 devices. */
-  
+
 typedef struct UbloxFrame {
-  uint8_t sync1; // always 0xb5, or ISO-8859.1 for 'µ'
-  uint8_t sync2; // always 0x62, or ASCII for 'b'
-  // class and id together indicate what kind of message is being sent.
-  uint8_t class; // message class
-  uint8_t id; // message id
+    uint8_t sync1; // always 0xb5, or ISO-8859.1 for 'µ'
+    uint8_t sync2; // always 0x62, or ASCII for 'b'
+    // class and id together indicate what kind of message is being sent.
+    uint8_t class; // message class
+    uint8_t id; // message id
 
-  uint16_t len; // length of the payload only, 2 bytes, little-endian
-  uint8_t* payload; // any number of bytes
+    uint16_t len; // length of the payload only, 2 bytes, little-endian
+    uint8_t* payload; // any number of bytes
 
-  // 2 bytes of checksum
-  uint8_t ck_a;
-  uint8_t ck_b;
+    // 2 bytes of checksum
+    uint8_t ck_a;
+    uint8_t ck_b;
 
-  // metadata
-  bool valid;
+    // metadata
+    bool valid;
 } UbloxFrame;
 
 typedef struct UbloxMessage {
-  uint8_t* message;
-  uint8_t length;
+    uint8_t* message;
+    uint8_t length;
 } UbloxMessage;
 
 // Field names taken directly from u-blox protocol manual.
 typedef struct Ublox_NAV_PVT_Message {
-  uint32_t iTOW;
-  uint16_t year;
-  uint8_t month;
-  uint8_t day;
-  uint8_t hour;
-  uint8_t min;
-  uint8_t sec;
-  uint8_t valid;
-  uint32_t tAcc;
-  int32_t nano;
-  uint8_t fixType;
-  uint8_t flags;
-  uint8_t flags2;
-  uint8_t numSV;
-  int32_t lon;
-  int32_t lat;
-  int32_t height;
-  int32_t hMSL;
-  uint32_t hAcc;
-  uint32_t vAcc;
-  int32_t velN;
-  int32_t velE;
-  int32_t velD;
-  int32_t gSpeed;
-  int32_t headMot;
-  uint32_t sAcc;
-  uint32_t headAcc;
-  uint16_t pDOP;
-  uint16_t flags3;
-  uint8_t reserved1;
-  uint8_t reserved2;
-  uint8_t reserved3;
-  uint8_t reserved4;
-  int32_t headVeh;
-  int16_t magDec;
-  uint16_t magAcc;
+    uint32_t iTOW;
+    uint16_t year;
+    uint8_t month;
+    uint8_t day;
+    uint8_t hour;
+    uint8_t min;
+    uint8_t sec;
+    uint8_t valid;
+    uint32_t tAcc;
+    int32_t nano;
+    uint8_t fixType;
+    uint8_t flags;
+    uint8_t flags2;
+    uint8_t numSV;
+    int32_t lon;
+    int32_t lat;
+    int32_t height;
+    int32_t hMSL;
+    uint32_t hAcc;
+    uint32_t vAcc;
+    int32_t velN;
+    int32_t velE;
+    int32_t velD;
+    int32_t gSpeed;
+    int32_t headMot;
+    uint32_t sAcc;
+    uint32_t headAcc;
+    uint16_t pDOP;
+    uint16_t flags3;
+    uint8_t reserved1;
+    uint8_t reserved2;
+    uint8_t reserved3;
+    uint8_t reserved4;
+    int32_t headVeh;
+    int16_t magDec;
+    uint16_t magAcc;
 } Ublox_NAV_PVT_Message;
 
 typedef struct Ublox_NAV_ODO_Message {
-  uint8_t version;
-  uint8_t reserved1;
-  uint8_t reserved2;
-  uint8_t reserved3;
-  uint32_t iTOW;
-  uint32_t distance;
-  uint32_t totalDistance;
-  uint32_t distanceStd;
+    uint8_t version;
+    uint8_t reserved1;
+    uint8_t reserved2;
+    uint8_t reserved3;
+    uint32_t iTOW;
+    uint32_t distance;
+    uint32_t totalDistance;
+    uint32_t distanceStd;
 } Ublox_NAV_ODO_Message;
 
+typedef struct Ublox_NAV_TIMEUTC_Message {
+    uint32_t iTOW;
+    uint32_t tAcc;
+    int32_t nano;
+    uint16_t year;
+    uint8_t month;
+    uint8_t day;
+    uint8_t hour;
+    uint8_t min;
+    uint8_t sec;
+    uint8_t valid;
+} Ublox_NAV_TIMEUTC_Message;
+
 /** For a given UbloxFrame, populate the sync bytes, calculate the
  * checksum bytes (storing them in `frame`), allocate a uint8_t array,
  * and fill it with the contents of the frame in an order ready to

+ 41 - 24
non_catalog_apps/ublox/ublox_i.h

@@ -16,42 +16,59 @@
 #include <gui/modules/submenu.h>
 #include <gui/modules/widget.h>
 #include <gui/modules/variable_item_list.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/validators.h>
 
 #include <notification/notification_messages.h>
 
+#include <storage/storage.h>
+#include <storage/filesystem_api_defines.h>
+
 #include "scenes/ublox_scene.h"
 #include <ublox_icons.h>
 #include "views/data_display_view.h"
 #include "helpers/ublox_custom_event.h"
+#include "helpers/kml.h"
+
+extern const NotificationSequence sequence_new_reading;
 
 struct Ublox {
-  ViewDispatcher* view_dispatcher;
-  Gui* gui;
-  SceneManager* scene_manager;
-
-  Submenu* submenu;
-  Widget* widget;
-  VariableItemList* variable_item_list;
-  DataDisplayView* data_display;
-  NotificationApp* notifications;
-  
-  UbloxWorker* worker;
-  FuriTimer* timer;
-  
-  Ublox_NAV_PVT_Message nav_pvt;
-  Ublox_NAV_ODO_Message nav_odo;
-  
-  UbloxDataDisplayState data_display_state;
-  UbloxDeviceState device_state;
-  bool gps_initted;
+    ViewDispatcher* view_dispatcher;
+    Gui* gui;
+    SceneManager* scene_manager;
+
+    Submenu* submenu;
+    Widget* widget;
+    VariableItemList* variable_item_list;
+    TextInput* text_input;
+    DataDisplayView* data_display;
+    
+    Storage* storage;
+    NotificationApp* notifications;
+
+    UbloxWorker* worker;
+
+    // file stuff
+    KMLFile kmlfile;
+    UbloxLogState log_state;
+    FuriString* logfile_folder;
+    char text_store[100];
+    Ublox_NAV_PVT_Message nav_pvt;
+    Ublox_NAV_ODO_Message nav_odo;
+    Ublox_NAV_TIMEUTC_Message nav_timeutc;
+    
+    UbloxDataDisplayState data_display_state;
+    UbloxDeviceState device_state;
+    bool gps_initted;
 };
 
 typedef enum {
-  UbloxViewMenu,
-  UbloxViewWidget,
-  UbloxViewDataDisplay,
-  UbloxViewVariableItemList,
+    UbloxViewMenu,
+    UbloxViewWidget,
+    UbloxViewVariableItemList,
+    UbloxViewTextInput,
+    // custom
+    UbloxViewDataDisplay,
 } UbloxView;
 
 Ublox* ublox_alloc();
-

+ 593 - 413
non_catalog_apps/ublox/ublox_worker.c

@@ -3,479 +3,659 @@
 #define TAG "UbloxWorker"
 
 UbloxWorker* ublox_worker_alloc() {
-  UbloxWorker* ublox_worker = malloc(sizeof(UbloxWorker));
+    UbloxWorker* ublox_worker = malloc(sizeof(UbloxWorker));
 
-  ublox_worker->thread = furi_thread_alloc_ex("UbloxWorker", 2*1024, ublox_worker_task, ublox_worker);
+    ublox_worker->thread =
+        furi_thread_alloc_ex("UbloxWorker", 2 * 1024, ublox_worker_task, ublox_worker);
 
-  ublox_worker->callback = NULL;
-  ublox_worker->context = NULL;
+    ublox_worker->callback = NULL;
+    ublox_worker->context = NULL;
 
-  ublox_worker_change_state(ublox_worker, UbloxWorkerStateReady);
+    ublox_worker_change_state(ublox_worker, UbloxWorkerStateReady);
 
-  return ublox_worker;
+    return ublox_worker;
 }
 
 void ublox_worker_free(UbloxWorker* ublox_worker) {
-  furi_assert(ublox_worker);
+    furi_assert(ublox_worker);
 
-  furi_thread_free(ublox_worker->thread);
-  
-  free(ublox_worker);
+    furi_thread_free(ublox_worker->thread);
+
+    free(ublox_worker);
 }
 
 UbloxWorkerState ublox_worker_get_state(UbloxWorker* ublox_worker) {
-  return ublox_worker->state;
+    return ublox_worker->state;
 }
 
-void ublox_worker_start(UbloxWorker* ublox_worker,
-			UbloxWorkerState state,
-			UbloxWorkerCallback callback,
-			void* context) {
-  furi_assert(ublox_worker);
+void ublox_worker_start(
+    UbloxWorker* ublox_worker,
+    UbloxWorkerState state,
+    UbloxWorkerCallback callback,
+    void* context) {
+    furi_assert(ublox_worker);
 
-  ublox_worker->callback = callback;
-  ublox_worker->context = context;
+    ublox_worker->callback = callback;
+    ublox_worker->context = context;
 
-  ublox_worker_change_state(ublox_worker, state);
-  furi_thread_start(ublox_worker->thread);
+    ublox_worker_change_state(ublox_worker, state);
+    furi_thread_start(ublox_worker->thread);
 }
 
 void ublox_worker_stop(UbloxWorker* ublox_worker) {
-  furi_assert(ublox_worker);
-  furi_assert(ublox_worker->thread);
-  FURI_LOG_I(TAG, "worker_stop");
-  
-  if (furi_thread_get_state(ublox_worker->thread) != FuriThreadStateStopped) {
-    FURI_LOG_I(TAG, "set thread state to stopped");
-    ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
-    furi_thread_join(ublox_worker->thread);
-  }
+    furi_assert(ublox_worker);
+    furi_assert(ublox_worker->thread);
+    Ublox* ublox = ublox_worker->context;
+    furi_assert(ublox);
+    //FURI_LOG_I(TAG, "worker_stop: %p", ublox);
+
+    /*FuriThreadState state = furi_thread_get_state(ublox_worker->thread);
+  if (state == FuriThreadStateStopped) {
+      FURI_LOG_I(TAG, "worker state stopped");
+  } else if (state == FuriThreadStateStarting) {
+      FURI_LOG_I(TAG, "worker state starting");
+  } else if (state == FuriThreadStateRunning) {
+      FURI_LOG_I(TAG, "worker state running");
+      }*/
+
+    // close the logfile if currently logging
+    //FURI_LOG_I(TAG, "log state: %d", ublox->log_state);
+    if(ublox->log_state == UbloxLogStateLogging) {
+	FURI_LOG_I(TAG, "closing log file on worker stop");
+	ublox->log_state = UbloxLogStateNone;
+	if (!kml_close_file(&(ublox->kmlfile))) {
+	    FURI_LOG_E(TAG, "failed to close KML file!");
+	}
+    }
+    if(furi_thread_get_state(ublox_worker->thread) != FuriThreadStateStopped) {
+        ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
+        furi_thread_join(ublox_worker->thread);
+    }
 }
 
 void ublox_worker_change_state(UbloxWorker* ublox_worker, UbloxWorkerState state) {
-  ublox_worker->state = state;
+    ublox_worker->state = state;
 }
 
 void clear_ublox_data() {
-  uint8_t tx[] = {0xff};
-  uint8_t response = 0;
-  while (response != 0xff) {
-    if (!furi_hal_i2c_trx(&furi_hal_i2c_handle_external,
-			  UBLOX_I2C_ADDRESS << 1,
-			  tx, 1,
-			  &response, 1,
-			  furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
-      FURI_LOG_E(TAG, "error reading first byte of response");
+    int fails = 0;
+    
+    if (!furi_hal_i2c_is_device_ready(
+	    &furi_hal_i2c_handle_external,
+	    UBLOX_I2C_ADDRESS << 1,
+	    furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+	FURI_LOG_E(TAG, "clear_ublox_data(): device not ready");
+	return;
+    }
+    
+    uint8_t tx[] = {0xff};
+    uint8_t response = 0;
+    
+    while(response != 0xff && fails < 30) {
+        if(!furi_hal_i2c_trx(
+               &furi_hal_i2c_handle_external,
+               UBLOX_I2C_ADDRESS << 1,
+               tx, 1,
+               &response, 1,
+               furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+	    // if the GPS is disconnected during this loop, this will
+	    // loop forever, we must make that not happen. 30 loops is
+	    // plenty, and if the clearing doesn't work, the requisite
+	    // error will be generated by the caller on the next
+	    // actual attempt to reach the GPS.
+	    fails++;
+            FURI_LOG_E(TAG, "clear_ublox_data(): error clearing ublox data");
+        }
     }
-  }
 }
 
+
 int32_t ublox_worker_task(void* context) {
-  UbloxWorker* ublox_worker = context;
-  Ublox* ublox = ublox_worker->context;
 
+    UbloxWorker* ublox_worker = context;
 
-  if (ublox_worker->state == UbloxWorkerStateRead) {
     furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
-    if (!ublox->gps_initted) {
-      if (ublox_worker_init_gps(ublox_worker)) {
-	ublox->gps_initted = true;
-      } else {
-	ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
-	FURI_LOG_E(TAG, "init GPS failed");
-	furi_hal_i2c_release(&furi_hal_i2c_handle_external);
-	return 1;
-      }
-      // have to do this...don't know why, though, because the data
-      // should already be cleared out (also why does this even work, it
-      // seems like it should be capturing the first byte of the next
-      // message)
-      clear_ublox_data();
+    
+    if(ublox_worker->state == UbloxWorkerStateRead) {
+	ublox_worker_read_nav_messages(context);
+    } else if (ublox_worker->state == UbloxWorkerStateSyncTime) {
+	FURI_LOG_I(TAG, "sync time");
+	ublox_worker_sync_to_gps_time(ublox_worker);
+    } else if(ublox_worker->state == UbloxWorkerStateResetOdometer) {
+        ublox_worker_reset_odo(ublox_worker);
+    } else if(ublox_worker->state == UbloxWorkerStateStop) {
+        FURI_LOG_D(TAG, "state stop");
     }
 
-    ublox_worker_read_pvt(ublox_worker);
-    ublox_worker_read_odo(ublox_worker);
+    ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
+
     furi_hal_i2c_release(&furi_hal_i2c_handle_external);
-    ublox_worker->callback(UbloxWorkerEventDataReady, ublox_worker->context);
-    /*if (ublox_worker_read_odo(ublox_worker)) {
-      ublox_worker_read_pvt(ublox_worker);
-    } else {
-      next_state = UbloxWorkerStateStop;
-      ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
-      }*/
 
-  } else if (ublox_worker->state == UbloxWorkerStateResetOdometer) {
-    ublox_worker_reset_odo(ublox_worker);
-  } else if (ublox_worker->state == UbloxWorkerStateStop) {
-    FURI_LOG_I(TAG, "state stop");
-  } else if (ublox_worker->state == UbloxWorkerStateReady) {
-    FURI_LOG_I(TAG, "state ready");
-  }
+    return 0;
+}
 
-  ublox_worker_change_state(ublox_worker, UbloxWorkerStateReady);
-  
+void ublox_worker_read_nav_messages(void* context) {
+    // this function is fairly complicated: it inits the GPS, handles
+    // logging states, and reads data from the GPS to push it to the
+    // main app struct.
+    
+    // IMPORTANT NOTE: we don't use a timer that continually respawns
+    // the thread because that causes a memory leak.
+    UbloxWorker* ublox_worker = context;
+    Ublox* ublox = ublox_worker->context;
+
+    // We only start logging at the same time we restart the worker.
+    if(ublox->log_state == UbloxLogStateStartLogging) {
+	FURI_LOG_I(TAG, "start logging");
+	// assemble full logfile pathname
+	FuriString* fullname = furi_string_alloc();
+	path_concat(furi_string_get_cstr(ublox->logfile_folder), ublox->text_store, fullname);
+	FURI_LOG_I(TAG, "fullname is %s", furi_string_get_cstr(fullname));
+	
+	if (!kml_open_file(ublox->storage, &(ublox->kmlfile), furi_string_get_cstr(fullname))) {
+	    FURI_LOG_E(TAG, "failed to open KML file %s!", furi_string_get_cstr(fullname));
+	    ublox->log_state = UbloxLogStateNone;
+	}
+	ublox->log_state = UbloxLogStateLogging;
+	furi_string_free(fullname);
+
+	ublox_worker->callback(UbloxWorkerEventLogStateChanged, ublox_worker->context);
+           
+    }
+    
+    while(!ublox->gps_initted) {
+	if(ublox_worker->state != UbloxWorkerStateRead) {
+	    return;
+	}
+
+	// have to clear right before init to make retrying init work
+	clear_ublox_data();
+	if(ublox_worker_init_gps(ublox_worker)) {
+	    ublox->gps_initted = true;
+	    break;
+	} else {
+	    ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
+	    FURI_LOG_E(TAG, "init GPS failed, try again");
+	}
+	// don't try constantly, no reason to
+	furi_delay_ms(500);
+    }
 
-  //FURI_LOG_I(TAG, "mem free after: %u", memmgr_get_free_heap());
-  return 0;
+    // clear data so we don't an error on startup
+    clear_ublox_data();
+    
+    // break the loop when the thread state changes
+    while(ublox_worker->state == UbloxWorkerStateRead) {
+	// we interrupt with checking the state to help reduce
+	// lag. it's not perfect, but it does overall improve things.
+	bool pvt = ublox_worker_read_pvt(ublox_worker);
+	
+	if (ublox_worker->state != UbloxWorkerStateRead) break;
+	// clearing makes the second read much faster
+	clear_ublox_data();
+	
+	if (ublox_worker->state != UbloxWorkerStateRead) break;
+	
+	bool odo = ublox_worker_read_odo(ublox_worker);
+	
+	if (pvt && odo) {
+	    ublox_worker->callback(UbloxWorkerEventDataReady, ublox_worker->context);
+	    
+	    if (ublox->log_state == UbloxLogStateLogging) {
+		if (!kml_add_path_point(&(ublox->kmlfile),
+					// ublox returns values as floats * 1e7 in int form
+					(double)(ublox->nav_pvt.lat) / (double)1e7,
+					(double)(ublox->nav_pvt.lon) / (double)1e7,
+					ublox->nav_pvt.hMSL / 1e3)) { // convert altitude to meters
+		    FURI_LOG_E(TAG, "failed to write line to file");
+		}
+	    }
+	} else {
+	    ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
+	}
+
+	uint32_t ticks = furi_get_tick();
+	while(furi_get_tick() - ticks < furi_ms_to_ticks(((ublox->data_display_state).refresh_rate * 1000))) {
+	    // putting this here (should) make the logging response faster
+	    if(ublox->log_state == UbloxLogStateStopLogging) {
+		FURI_LOG_I(TAG, "stop logging");
+		if (!kml_close_file(&(ublox->kmlfile))) {
+		    FURI_LOG_E(TAG, "failed to close KML file!");
+		}
+		ublox->log_state = UbloxLogStateNone;
+		ublox_worker->callback(UbloxWorkerEventLogStateChanged, ublox_worker->context);
+	    }
+	    if(ublox_worker->state != UbloxWorkerStateRead) {
+		return;
+	    }
+	}
+    }
 }
 
+void ublox_worker_sync_to_gps_time(void* context) {
+    UbloxWorker* ublox_worker = context;
+    Ublox* ublox = ublox_worker->context;
+
+    UbloxFrame frame_tx;
+    frame_tx.class = UBX_NAV_CLASS;
+    frame_tx.id = UBX_NAV_TIMEUTC_MESSAGE;
+    frame_tx.len = 0;
+    frame_tx.payload = NULL;
+    UbloxMessage* message_tx = ublox_frame_to_bytes(&frame_tx);
+
+
+    UbloxMessage* message_rx = ublox_worker_i2c_transfer(message_tx, UBX_NAV_TIMEUTC_MESSAGE_LENGTH);
+    if(message_rx == NULL) {
+	FURI_LOG_E(TAG, "get_gps_time transfer failed");
+	ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
+	ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
+	return;
+    }
+    FURI_LOG_I(TAG, "got message");
+    UbloxFrame* frame_rx = ublox_bytes_to_frame(message_rx);
+    ublox_message_free(message_rx);
+
+    if(frame_rx == NULL) {
+	FURI_LOG_E(TAG, "NULL pointer, something wrong with NAV-TIMEUTC message!");
+	ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
+	ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
+	return;
+    } else {
+	Ublox_NAV_TIMEUTC_Message nav_timeutc = {
+	    .iTOW = (frame_rx->payload[0]) | (frame_rx->payload[1] << 8) |
+                    (frame_rx->payload[2] << 16) | (frame_rx->payload[3] << 24),
+	    .tAcc = (frame_rx->payload[4]) | (frame_rx->payload[5] << 8) |
+                    (frame_rx->payload[6] << 16) | (frame_rx->payload[7] << 24),
+	    .nano = (frame_rx->payload[8]) | (frame_rx->payload[9] << 8) |
+                    (frame_rx->payload[10] << 16) | (frame_rx->payload[11] << 24),
+            .year = (frame_rx->payload[12]) | (frame_rx->payload[13] << 8),
+            .month = frame_rx->payload[14],
+            .day = frame_rx->payload[15],
+            .hour = frame_rx->payload[16],
+            .min = frame_rx->payload[17],
+            .sec = frame_rx->payload[18],
+            .valid = frame_rx->payload[19],
+	};
+
+	ublox->nav_timeutc = nav_timeutc;
+	ublox_frame_free(frame_rx);
+	ublox_worker->callback(UbloxWorkerEventDataReady, ublox_worker->context);
+    }
+}
 
 FuriString* print_uint8_array(uint8_t* array, int length) {
-  FuriString* s = furi_string_alloc();
-  
-  for (int i = 0; i < length - 1; i++) {
-    furi_string_cat_printf(s, "%x, ", array[i]);
-  }
-  furi_string_cat_printf(s, "%x", array[length - 1]);
-
-  return s;
+    FuriString* s = furi_string_alloc();
+
+    for(int i = 0; i < length - 1; i++) {
+        furi_string_cat_printf(s, "%x, ", array[i]);
+    }
+    furi_string_cat_printf(s, "%x", array[length - 1]);
+
+    return s;
 }
 
 UbloxMessage* ublox_worker_i2c_transfer(UbloxMessage* message_tx, uint8_t read_length) {
-  //FURI_LOG_I(TAG, "ublox_worker_i2c_transfer");
-  if (!furi_hal_i2c_is_device_ready(&furi_hal_i2c_handle_external,
-				    UBLOX_I2C_ADDRESS << 1,
-				    furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
-    FURI_LOG_E(TAG, "GPS not found!");
-    return NULL;
-  }
-  
-  if (!furi_hal_i2c_tx(&furi_hal_i2c_handle_external,
-		       UBLOX_I2C_ADDRESS << 1,
-		       message_tx->message,
-		       message_tx->length,
-		       furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
-    FURI_LOG_I(TAG, "error writing message from GPS");
-    return NULL;
-  }
-  uint8_t* response = malloc((size_t)read_length);
-  // The GPS sends 0xff until it has a complete message to respond
-  // with. We have to wait until it stops sending that. (Why this
-  // works is a little bit...uh, well, I don't know. Shouldn't reading
-  // more bytes make it so that the data is completely read out and no
-  // longer available?)
-
-  // Also, we know that this function is the traceable source of the
-  // memory leak whenever it's run a second time.
-
-  // ** The leak comes after this point.
-  uint8_t tx[] = {0xff};
-
-  while (true) {
-    //FURI_LOG_I(TAG, "mem free in loop: %u", memmgr_get_free_heap());
-    if (!furi_hal_i2c_trx(&furi_hal_i2c_handle_external,
-			  UBLOX_I2C_ADDRESS << 1,
-			  tx, 1,
-			  response, 1,
-			  furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
-      FURI_LOG_E(TAG, "error reading first byte of response");
-      free(response);
-      return NULL;
-    }
-    //FURI_LOG_I(TAG, "read one byte");
-    // checking with 0xb5 prevents strange bursts of junk data from becoming an issue.
-    if (response[0] != 0xff && response[0] == 0xb5) {
-      //FURI_LOG_I(TAG, "mem free before final read: %u", memmgr_get_free_heap());
-      //FURI_LOG_I(TAG, "got data that isn't 0xff");
-      if (!furi_hal_i2c_trx(&furi_hal_i2c_handle_external,
-			    UBLOX_I2C_ADDRESS << 1,
-			    tx, 1,
-			    &(response[1]), read_length - 1, // first byte already read
-			    furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
-	FURI_LOG_E(TAG, "error reading rest of response");
-	free(response);
+
+    if (!furi_hal_i2c_is_device_ready(
+	    &furi_hal_i2c_handle_external,
+	    UBLOX_I2C_ADDRESS << 1,
+	    furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+	FURI_LOG_E(TAG, "device not ready");
 	return NULL;
-      }
-      //FURI_LOG_I(TAG, "mem free after final read: %u", memmgr_get_free_heap());
-      break;
     }
-  }
 
-  //FURI_LOG_I(TAG, "i2c_transfer: byte 0 = %d", response[0]);
-  UbloxMessage* message_rx = malloc(sizeof(UbloxMessage));
-  message_rx->message = response;
-  message_rx->length = read_length;
-  return message_rx; // message_rx->message needs to be freed later
+    // Either our I2C implementation is broken or the GPS's is, so we
+    // end up reading a lot more data than we need to. That means that
+    // the I2C comm code for this app is a little bit of a hack, but
+    // it works fine and is fast enough, so I don't really care. It
+    // certainly doesn't break the GPS.
+    if(!furi_hal_i2c_tx(
+           &furi_hal_i2c_handle_external,
+           UBLOX_I2C_ADDRESS << 1,
+           message_tx->message, message_tx->length,
+           furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+        FURI_LOG_E(TAG, "error writing message to GPS");
+        return NULL;
+    }
+    uint8_t* response = malloc((size_t)read_length);
+    // The GPS sends 0xff until it has a complete message to respond
+    // with. We have to wait until it stops sending that. (Why this
+    // works is a little bit...uh, well, I don't know. Shouldn't reading
+    // more bytes make it so that the data is completely read out and no
+    // longer available?)
+
+    //FURI_LOG_I(TAG, "start ticks at %lu", furi_get_tick()); // returns ms
+    while(true) {
+	if(!furi_hal_i2c_rx(
+	       &furi_hal_i2c_handle_external,
+               UBLOX_I2C_ADDRESS << 1,
+               response, 1,
+               furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+	    FURI_LOG_E(TAG, "error reading first byte of response");
+            free(response);
+            return NULL;
+	}
+	
+        // checking with 0xb5 prevents strange bursts of junk data from becoming an issue.
+        if(response[0] != 0xff && response[0] == 0xb5) {
+	    //FURI_LOG_I(TAG, "read rest of message at %lu", furi_get_tick());
+            if(!furi_hal_i2c_rx(
+                   &furi_hal_i2c_handle_external,
+                   UBLOX_I2C_ADDRESS << 1,
+                   &(response[1]), read_length - 1, // first byte already read
+                   furi_ms_to_ticks(I2C_TIMEOUT_MS))) {
+                FURI_LOG_E(TAG, "error reading rest of response");
+                free(response);
+                return NULL;
+            }
+            break;
+        }
+	furi_delay_ms(1);
+    }
+
+    UbloxMessage* message_rx = malloc(sizeof(UbloxMessage));
+    message_rx->message = response;
+    message_rx->length = read_length;
+    return message_rx; // message_rx->message needs to be freed later
 }
 
-void ublox_worker_read_pvt(UbloxWorker* ublox_worker) {
-  //FURI_LOG_I(TAG, "mem free before PVT read: %u", memmgr_get_free_heap());
-  Ublox* ublox = ublox_worker->context;
-  
-  // Read NAV-PVT by sending NAV-PVT with no payload
-  UbloxFrame* frame_tx = malloc(sizeof(UbloxFrame));
-  frame_tx->class = UBX_NAV_CLASS;
-  frame_tx->id = UBX_NAV_PVT_MESSAGE;
-  frame_tx->len = 0;
-  frame_tx->payload = NULL;
-  UbloxMessage* message_tx = ublox_frame_to_bytes(frame_tx);
-  ublox_frame_free(frame_tx);
-  
-  UbloxMessage* message_rx = ublox_worker_i2c_transfer(message_tx, UBX_NAV_PVT_MESSAGE_LENGTH);
-  ublox_message_free(message_tx);
-  if (message_rx == NULL) {
-    FURI_LOG_E(TAG, "read_pvt transfer failed");
-    ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
-    ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
-    return;
-  }
+bool ublox_worker_read_pvt(UbloxWorker* ublox_worker) {
+    //FURI_LOG_I(TAG, "mem free before PVT read: %u", memmgr_get_free_heap());
+    Ublox* ublox = ublox_worker->context;
+
+    // Read NAV-PVT by sending NAV-PVT with no payload
+    UbloxFrame* frame_tx = malloc(sizeof(UbloxFrame));
+    frame_tx->class = UBX_NAV_CLASS;
+    frame_tx->id = UBX_NAV_PVT_MESSAGE;
+    frame_tx->len = 0;
+    frame_tx->payload = NULL;
+    UbloxMessage* message_tx = ublox_frame_to_bytes(frame_tx);
+    ublox_frame_free(frame_tx);
+
+    UbloxMessage* message_rx = ublox_worker_i2c_transfer(message_tx, UBX_NAV_PVT_MESSAGE_LENGTH);
+    ublox_message_free(message_tx);
+    if(message_rx == NULL) {
+        FURI_LOG_E(TAG, "read_pvt transfer failed");
+        //ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
+        return false;
+    }
 
-  UbloxFrame* frame_rx = ublox_bytes_to_frame(message_rx);
-  ublox_message_free(message_rx);
+    UbloxFrame* frame_rx = ublox_bytes_to_frame(message_rx);
+    ublox_message_free(message_rx);
 
-  if (frame_rx == NULL) {
-    FURI_LOG_E(TAG, "NULL pointer, something wrong with NAV-PVT message!");
-    ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
-    ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
-  } else {
-    // build nav-pvt struct. yes this is very ugly.
-    Ublox_NAV_PVT_Message nav_pvt = {
-      .iTOW = (frame_rx->payload[0]) | (frame_rx->payload[1] << 8) | (frame_rx->payload[2] << 16) | (frame_rx->payload[3] << 24),
-      .year = (frame_rx->payload[4]) | (frame_rx->payload[5] << 8),
-      .month = frame_rx->payload[6],
-      .day = frame_rx->payload[7],
-      .hour = frame_rx->payload[8],
-      .min = frame_rx->payload[9],
-      .sec = frame_rx->payload[10],
-      .valid = frame_rx->payload[11],
-      .tAcc = (frame_rx->payload[12]) | (frame_rx->payload[13] << 8) | (frame_rx->payload[14] << 16) | (frame_rx->payload[15] << 24),
-      .nano = (frame_rx->payload[16]) | (frame_rx->payload[17] << 8) | (frame_rx->payload[18] << 16) | (frame_rx->payload[19] << 24),
-      .fixType = frame_rx->payload[20],
-      .flags = frame_rx->payload[21],
-      .flags2 = frame_rx->payload[22],
-      .numSV = frame_rx->payload[23],
-      .lon = (frame_rx->payload[24]) | (frame_rx->payload[25] << 8) | (frame_rx->payload[26] << 16) | (frame_rx->payload[27] << 24),
-      .lat = (frame_rx->payload[28]) | (frame_rx->payload[29] << 8) | (frame_rx->payload[30] << 16) | (frame_rx->payload[31] << 24),
-      .height = (frame_rx->payload[32]) | (frame_rx->payload[33] << 8) | (frame_rx->payload[34] << 16) | (frame_rx->payload[35] << 24),
-      .hMSL = (frame_rx->payload[36]) | (frame_rx->payload[37] << 8) | (frame_rx->payload[38] << 16) | (frame_rx->payload[39] << 24),
-      .hAcc = (frame_rx->payload[40]) | (frame_rx->payload[41] << 8) | (frame_rx->payload[42] << 16) | (frame_rx->payload[43] << 24),
-      .vAcc = (frame_rx->payload[44]) | (frame_rx->payload[45] << 8) | (frame_rx->payload[46] << 16) | (frame_rx->payload[47] << 24),
-      .velN = (frame_rx->payload[48]) | (frame_rx->payload[49] << 8) | (frame_rx->payload[50] << 16) | (frame_rx->payload[51] << 24),
-      .velE = (frame_rx->payload[52]) | (frame_rx->payload[53] << 8) | (frame_rx->payload[54] << 16) | (frame_rx->payload[55] << 24),
-      .velD = (frame_rx->payload[56]) | (frame_rx->payload[57] << 8) | (frame_rx->payload[58] << 16) | (frame_rx->payload[59] << 24),
-      .gSpeed = (frame_rx->payload[60]) | (frame_rx->payload[61] << 8) | (frame_rx->payload[62] << 16) | (frame_rx->payload[63] << 24),
-      .headMot = (frame_rx->payload[64]) | (frame_rx->payload[65] << 8) | (frame_rx->payload[66] << 16) | (frame_rx->payload[67] << 24),
-      .sAcc = (frame_rx->payload[68]) | (frame_rx->payload[69] << 8) | (frame_rx->payload[70] << 16) | (frame_rx->payload[71] << 24),
-      .headAcc = (frame_rx->payload[72]) | (frame_rx->payload[73] << 8) | (frame_rx->payload[74] << 16) | (frame_rx->payload[75] << 24),
-      .pDOP = (frame_rx->payload[76]) | (frame_rx->payload[77] << 8),
-      .flags3 = (frame_rx->payload[78]) | (frame_rx->payload[79] << 8),
-      .reserved1 = frame_rx->payload[80],
-      .reserved2 = frame_rx->payload[81],
-      .reserved3 = frame_rx->payload[82],
-      .reserved4 = frame_rx->payload[83],
-      .headVeh = (frame_rx->payload[84]) | (frame_rx->payload[85] << 8) | (frame_rx->payload[86] << 16) | (frame_rx->payload[87] << 24),
-      .magDec = (frame_rx->payload[88]) | (frame_rx->payload[89] << 8),
-      .magAcc = (frame_rx->payload[90]) | (frame_rx->payload[91] << 8),
-    };
-
-    // Using a local variable for nav_pvt is fine, because nav_pvt in
-    // the Ublox struct is also not a pointer, so this assignment
-    // effectively compiles to a memcpy.
-    ublox->nav_pvt = nav_pvt;
-    ublox_frame_free(frame_rx);
-    //ublox_worker->callback(UbloxWorkerEventDataReady, ublox_worker->context);
-  }
-  //FURI_LOG_I(TAG, "mem free after PVT read: %u", memmgr_get_free_heap());
+    if(frame_rx == NULL) {
+        FURI_LOG_E(TAG, "NULL pointer, something wrong with NAV-PVT message!");
+        //ublox_worker_change_state(ublox_worker, UbloxWorkerStateStop);
+        return false;
+    } else {
+        // build nav-pvt struct. this is very ugly and there's not much I can do about it.
+        Ublox_NAV_PVT_Message nav_pvt = {
+            .iTOW = (frame_rx->payload[0]) | (frame_rx->payload[1] << 8) |
+                    (frame_rx->payload[2] << 16) | (frame_rx->payload[3] << 24),
+            .year = (frame_rx->payload[4]) | (frame_rx->payload[5] << 8),
+            .month = frame_rx->payload[6],
+            .day = frame_rx->payload[7],
+            .hour = frame_rx->payload[8],
+            .min = frame_rx->payload[9],
+            .sec = frame_rx->payload[10],
+            .valid = frame_rx->payload[11],
+            .tAcc = (frame_rx->payload[12]) | (frame_rx->payload[13] << 8) |
+                    (frame_rx->payload[14] << 16) | (frame_rx->payload[15] << 24),
+            .nano = (frame_rx->payload[16]) | (frame_rx->payload[17] << 8) |
+                    (frame_rx->payload[18] << 16) | (frame_rx->payload[19] << 24),
+            .fixType = frame_rx->payload[20],
+            .flags = frame_rx->payload[21],
+            .flags2 = frame_rx->payload[22],
+            .numSV = frame_rx->payload[23],
+            .lon = (frame_rx->payload[24]) | (frame_rx->payload[25] << 8) |
+                   (frame_rx->payload[26] << 16) | (frame_rx->payload[27] << 24),
+            .lat = (frame_rx->payload[28]) | (frame_rx->payload[29] << 8) |
+                   (frame_rx->payload[30] << 16) | (frame_rx->payload[31] << 24),
+            .height = (frame_rx->payload[32]) | (frame_rx->payload[33] << 8) |
+                      (frame_rx->payload[34] << 16) | (frame_rx->payload[35] << 24),
+            .hMSL = (frame_rx->payload[36]) | (frame_rx->payload[37] << 8) |
+                    (frame_rx->payload[38] << 16) | (frame_rx->payload[39] << 24),
+            .hAcc = (frame_rx->payload[40]) | (frame_rx->payload[41] << 8) |
+                    (frame_rx->payload[42] << 16) | (frame_rx->payload[43] << 24),
+            .vAcc = (frame_rx->payload[44]) | (frame_rx->payload[45] << 8) |
+                    (frame_rx->payload[46] << 16) | (frame_rx->payload[47] << 24),
+            .velN = (frame_rx->payload[48]) | (frame_rx->payload[49] << 8) |
+                    (frame_rx->payload[50] << 16) | (frame_rx->payload[51] << 24),
+            .velE = (frame_rx->payload[52]) | (frame_rx->payload[53] << 8) |
+                    (frame_rx->payload[54] << 16) | (frame_rx->payload[55] << 24),
+            .velD = (frame_rx->payload[56]) | (frame_rx->payload[57] << 8) |
+                    (frame_rx->payload[58] << 16) | (frame_rx->payload[59] << 24),
+            .gSpeed = (frame_rx->payload[60]) | (frame_rx->payload[61] << 8) |
+                      (frame_rx->payload[62] << 16) | (frame_rx->payload[63] << 24),
+            .headMot = (frame_rx->payload[64]) | (frame_rx->payload[65] << 8) |
+                       (frame_rx->payload[66] << 16) | (frame_rx->payload[67] << 24),
+            .sAcc = (frame_rx->payload[68]) | (frame_rx->payload[69] << 8) |
+                    (frame_rx->payload[70] << 16) | (frame_rx->payload[71] << 24),
+            .headAcc = (frame_rx->payload[72]) | (frame_rx->payload[73] << 8) |
+                       (frame_rx->payload[74] << 16) | (frame_rx->payload[75] << 24),
+            .pDOP = (frame_rx->payload[76]) | (frame_rx->payload[77] << 8),
+            .flags3 = (frame_rx->payload[78]) | (frame_rx->payload[79] << 8),
+            .reserved1 = frame_rx->payload[80],
+            .reserved2 = frame_rx->payload[81],
+            .reserved3 = frame_rx->payload[82],
+            .reserved4 = frame_rx->payload[83],
+            .headVeh = (frame_rx->payload[84]) | (frame_rx->payload[85] << 8) |
+                       (frame_rx->payload[86] << 16) | (frame_rx->payload[87] << 24),
+            .magDec = (frame_rx->payload[88]) | (frame_rx->payload[89] << 8),
+            .magAcc = (frame_rx->payload[90]) | (frame_rx->payload[91] << 8),
+        };
+
+        // Using a local variable for nav_pvt is fine, because nav_pvt in
+        // the Ublox struct is also not a pointer, so this assignment
+        // effectively compiles to a memcpy.
+        ublox->nav_pvt = nav_pvt;
+        ublox_frame_free(frame_rx);
+        return true;
+    }
+    return false;
 }
 
 bool ublox_worker_read_odo(UbloxWorker* ublox_worker) {
-  //FURI_LOG_I(TAG, "mem free before odo read: %u", memmgr_get_free_heap());
-  Ublox* ublox = ublox_worker->context;
-  UbloxFrame* frame_tx = malloc(sizeof(UbloxFrame));
-  frame_tx->class = UBX_NAV_CLASS;
-  frame_tx->id = UBX_NAV_ODO_MESSAGE;
-  frame_tx->len = 0;
-  frame_tx->payload = NULL;
-  UbloxMessage* message_tx = ublox_frame_to_bytes(frame_tx);
-  ublox_frame_free(frame_tx);
-
-  
-  UbloxMessage* message_rx = ublox_worker_i2c_transfer(message_tx, UBX_NAV_ODO_MESSAGE_LENGTH);
-  ublox_message_free(message_tx);
-  if (message_rx == NULL) {
-    FURI_LOG_E(TAG, "read_odo transfer failed");
-    return false;
-  }
-  UbloxFrame* frame_rx = ublox_bytes_to_frame(message_rx);
-  ublox_message_free(message_rx);
-  
-  if (frame_rx == NULL) {
-    FURI_LOG_E(TAG, "NULL pointer, something wrong with NAV-ODO message!");
-    return false;
-  } else {
-    Ublox_NAV_ODO_Message nav_odo = {
-      .version = frame_rx->payload[0],
-      .reserved1 = frame_rx->payload[1],
-      .reserved2 = frame_rx->payload[2],
-      .reserved3 = frame_rx->payload[3],
-      .iTOW = (frame_rx->payload[4]) | (frame_rx->payload[5] << 8) | (frame_rx->payload[6] << 16) | (frame_rx->payload[7] << 24),
-      .distance = (frame_rx->payload[8]) | (frame_rx->payload[9] << 8) | (frame_rx->payload[10] << 16) | (frame_rx->payload[11] << 24),
-      .totalDistance = (frame_rx->payload[12]) | (frame_rx->payload[13] << 8) | (frame_rx->payload[14] << 16) | (frame_rx->payload[15] << 24),
-      .distanceStd = (frame_rx->payload[16]) | (frame_rx->payload[17] << 8) | (frame_rx->payload[18] << 16) | (frame_rx->payload[19] << 24),
-    };
-    ublox->nav_odo = nav_odo;
-    ublox_frame_free(frame_rx);
-    //FURI_LOG_I(TAG, "mem free after odo read: %u", memmgr_get_free_heap());
-    return true;
-  }
+    Ublox* ublox = ublox_worker->context;
+    UbloxFrame* frame_tx = malloc(sizeof(UbloxFrame));
+    frame_tx->class = UBX_NAV_CLASS;
+    frame_tx->id = UBX_NAV_ODO_MESSAGE;
+    frame_tx->len = 0;
+    frame_tx->payload = NULL;
+    UbloxMessage* message_tx = ublox_frame_to_bytes(frame_tx);
+    ublox_frame_free(frame_tx);
+
+    UbloxMessage* message_rx = ublox_worker_i2c_transfer(message_tx, UBX_NAV_ODO_MESSAGE_LENGTH);
+    ublox_message_free(message_tx);
+    if(message_rx == NULL) {
+        FURI_LOG_E(TAG, "read_odo transfer failed");
+        return false;
+    }
+    UbloxFrame* frame_rx = ublox_bytes_to_frame(message_rx);
+    ublox_message_free(message_rx);
 
+    if(frame_rx == NULL) {
+        FURI_LOG_E(TAG, "NULL pointer, something wrong with NAV-ODO message!");
+        return false;
+    } else {
+        Ublox_NAV_ODO_Message nav_odo = {
+            .version = frame_rx->payload[0],
+            .reserved1 = frame_rx->payload[1],
+            .reserved2 = frame_rx->payload[2],
+            .reserved3 = frame_rx->payload[3],
+            .iTOW = (frame_rx->payload[4]) | (frame_rx->payload[5] << 8) |
+                    (frame_rx->payload[6] << 16) | (frame_rx->payload[7] << 24),
+            .distance = (frame_rx->payload[8]) | (frame_rx->payload[9] << 8) |
+                        (frame_rx->payload[10] << 16) | (frame_rx->payload[11] << 24),
+            .totalDistance = (frame_rx->payload[12]) | (frame_rx->payload[13] << 8) |
+                             (frame_rx->payload[14] << 16) | (frame_rx->payload[15] << 24),
+            .distanceStd = (frame_rx->payload[16]) | (frame_rx->payload[17] << 8) |
+                           (frame_rx->payload[18] << 16) | (frame_rx->payload[19] << 24),
+        };
+	FURI_LOG_I(TAG, "odo (m): %lu", nav_odo.distance);
+        ublox->nav_odo = nav_odo;
+        ublox_frame_free(frame_rx);
+        return true;
+    }
 }
 
-/** Set the power mode to "Aggressive with 1Hz", enable the odometer,
-    and configure odometer and dynamic platform model. */
+/**
+ * Set the power mode to "Balanced", enable the odometer, and
+ * configure odometer and dynamic platform model according to user
+ * settings.
+ */
 bool ublox_worker_init_gps(UbloxWorker* ublox_worker) {
-  Ublox* ublox = ublox_worker->context;
-  // Set power mode
-  /*** read initial cfg-pms configuration first ***/
-  UbloxFrame* pms_frame_tx = malloc(sizeof(UbloxFrame));
-  pms_frame_tx->class = UBX_CFG_CLASS;
-  pms_frame_tx->id = UBX_CFG_PMS_MESSAGE;
-  pms_frame_tx->len = 0;
-  pms_frame_tx->payload = NULL;
-  UbloxMessage* pms_message_tx = ublox_frame_to_bytes(pms_frame_tx);
-  ublox_frame_free(pms_frame_tx);
-
-  UbloxMessage* pms_message_rx = ublox_worker_i2c_transfer(pms_message_tx, UBX_CFG_PMS_MESSAGE_LENGTH);
-  ublox_message_free(pms_message_tx);
-  if (pms_message_rx == NULL) {
-    FURI_LOG_E(TAG, "CFG-PMS read transfer failed");
-    return false;
-  }
-
-  // set power setup value to "aggressive with 1Hz"
-  pms_message_rx->message[6+1] = 0x03;
-
-  pms_frame_tx = malloc(sizeof(UbloxFrame));
-  pms_frame_tx->class = UBX_CFG_CLASS;
-  pms_frame_tx->id = UBX_CFG_PMS_MESSAGE;
-  pms_frame_tx->len = 8;
-  pms_frame_tx->payload = pms_message_rx->message;
-
-  pms_message_tx = ublox_frame_to_bytes(pms_frame_tx);
-  ublox_frame_free(pms_frame_tx);
-  
-  UbloxMessage* ack = ublox_worker_i2c_transfer(pms_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
-  if (ack == NULL) {
-    FURI_LOG_E(TAG, "ACK after CFG-PMS set transfer failed");
-    return false;
-  }
-  FURI_LOG_I(TAG, "CFG-PMS ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
-  ublox_message_free(pms_message_tx);
-  ublox_message_free(pms_message_rx);
-  ublox_message_free(ack);
-  
-  /***** Odometer *****/
-  // Enable odometer by changing CFG-ODO.
-  UbloxFrame* odo_frame_tx = malloc(sizeof(UbloxFrame));
-  odo_frame_tx->class = UBX_CFG_CLASS;
-  odo_frame_tx->id = UBX_CFG_ODO_MESSAGE;
-  odo_frame_tx->len = 0;
-  odo_frame_tx->payload = NULL;
-  UbloxMessage* odo_message_tx = ublox_frame_to_bytes(odo_frame_tx);
-  ublox_frame_free(odo_frame_tx);
-
-
-  UbloxMessage* odo_message_rx = ublox_worker_i2c_transfer(odo_message_tx, UBX_CFG_ODO_MESSAGE_LENGTH);
-  ublox_message_free(odo_message_tx);
-  if (odo_message_rx == NULL) {
-    FURI_LOG_E(TAG, "CFG-ODO transfer failed");
-    return false;
-  }
-
-  odo_frame_tx = malloc(sizeof(UbloxFrame));
-  odo_frame_tx->class = UBX_CFG_CLASS;
-  odo_frame_tx->id = UBX_CFG_ODO_MESSAGE;
-  odo_frame_tx->len = 20;
-  odo_frame_tx->payload = odo_message_rx->message;
-
-  // TODO: low-pass filters in settings?
-  // enable useODO bit in flags
-  odo_frame_tx->payload[4] |= 1;
-  odo_frame_tx->payload[5] = (ublox->device_state).odometer_mode;
-  
-  odo_message_tx = ublox_frame_to_bytes(odo_frame_tx);
-  ublox_frame_free(odo_frame_tx);
-
-  ack = ublox_worker_i2c_transfer(odo_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
-  if (ack == NULL) {
-    FURI_LOG_E(TAG, "ACK after CFG-ODO set transfer failed");
-    return false;
-  }
-  FURI_LOG_I(TAG, "CFG-ODO ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
-
-  ublox_message_free(odo_message_tx);
-  ublox_message_free(odo_message_rx);
-  ublox_message_free(ack);
-
-  // finally configure the navigation engine
-  UbloxFrame* nav5_frame_tx = malloc(sizeof(UbloxFrame));
-  nav5_frame_tx->class = UBX_CFG_CLASS;
-  nav5_frame_tx->id = UBX_CFG_NAV5_MESSAGE;
-  nav5_frame_tx->len = 0;
-  nav5_frame_tx->payload = NULL;
-  UbloxMessage* nav5_message_tx = ublox_frame_to_bytes(nav5_frame_tx);
-  ublox_frame_free(nav5_frame_tx);
-
-  UbloxMessage* nav5_message_rx = ublox_worker_i2c_transfer(nav5_message_tx, UBX_CFG_NAV5_MESSAGE_LENGTH);
-  if (nav5_message_rx == NULL) {
-    FURI_LOG_E(TAG, "CFG-NAV5 transfer failed");
-    return false;
-  }
-
-  // first two bytes tell the GPS what changes to apply, setting this
-  // bit tells it to apply the dynamic platfrom model settings.
-  nav5_frame_tx = malloc(sizeof(UbloxFrame));
-  nav5_frame_tx->class = UBX_CFG_CLASS;
-  nav5_frame_tx->id = UBX_CFG_NAV5_MESSAGE;
-  nav5_frame_tx->len = 36;
-  nav5_frame_tx->payload = nav5_message_rx->message;
-
-  nav5_frame_tx->payload[0] |= 1;
-  nav5_frame_tx->payload[2] = (ublox->device_state).platform_model;
-
-  nav5_message_tx = ublox_frame_to_bytes(nav5_frame_tx);
-  ublox_frame_free(nav5_frame_tx);
-  
-  ack = ublox_worker_i2c_transfer(nav5_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
-  if (ack == NULL) {
-    FURI_LOG_E(TAG, "ACK after CFG-NAV5 set transfer failed");
-    return false;
-  }
-  FURI_LOG_I(TAG, "CFG-NAV5 ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
-  
-  ublox_message_free(nav5_message_tx);
-  ublox_message_free(nav5_message_rx);
-  ublox_message_free(ack);
-  return true;
+    Ublox* ublox = ublox_worker->context;
+    // Set power mode
+    /*** read initial cfg-pms configuration first ***/
+    UbloxFrame pms_frame_tx;
+    pms_frame_tx.class = UBX_CFG_CLASS;
+    pms_frame_tx.id = UBX_CFG_PMS_MESSAGE;
+    pms_frame_tx.len = 0;
+    pms_frame_tx.payload = NULL;
+    UbloxMessage* pms_message_tx = ublox_frame_to_bytes(&pms_frame_tx);
+
+    UbloxMessage* pms_message_rx =
+        ublox_worker_i2c_transfer(pms_message_tx, UBX_CFG_PMS_MESSAGE_LENGTH);
+    ublox_message_free(pms_message_tx);
+    if(pms_message_rx == NULL) {
+        FURI_LOG_E(TAG, "CFG-PMS read transfer failed");
+        return false;
+    }
 
-}
+    // set power setup value to "balanced"
+    pms_message_rx->message[6 + 1] = 0x01;
+
+    pms_frame_tx.class = UBX_CFG_CLASS;
+    pms_frame_tx.id = UBX_CFG_PMS_MESSAGE;
+    pms_frame_tx.len = 8;
+    pms_frame_tx.payload = pms_message_rx->message;
+
+    pms_message_tx = ublox_frame_to_bytes(&pms_frame_tx);
 
+    UbloxMessage* ack = ublox_worker_i2c_transfer(pms_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
+    if(ack == NULL) {
+        FURI_LOG_E(TAG, "ACK after CFG-PMS set transfer failed");
+        return false;
+    }
+    FURI_LOG_I(
+        TAG, "CFG-PMS ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
+    ublox_message_free(pms_message_tx);
+    ublox_message_free(pms_message_rx);
+    ublox_message_free(ack);
+
+    /***** Odometer *****/
+    // Enable odometer by changing CFG-ODO.
+    UbloxFrame odo_frame_tx;
+    odo_frame_tx.class = UBX_CFG_CLASS;
+    odo_frame_tx.id = UBX_CFG_ODO_MESSAGE;
+    odo_frame_tx.len = 0;
+    odo_frame_tx.payload = NULL;
+    UbloxMessage* odo_message_tx = ublox_frame_to_bytes(&odo_frame_tx);
+
+    UbloxMessage* odo_message_rx =
+        ublox_worker_i2c_transfer(odo_message_tx, UBX_CFG_ODO_MESSAGE_LENGTH);
+    ublox_message_free(odo_message_tx);
+    if(odo_message_rx == NULL) {
+        FURI_LOG_E(TAG, "CFG-ODO transfer failed");
+        return false;
+    }
+
+    odo_frame_tx.class = UBX_CFG_CLASS;
+    odo_frame_tx.id = UBX_CFG_ODO_MESSAGE;
+    odo_frame_tx.len = 20;
+    odo_frame_tx.payload = odo_message_rx->message;
+
+    // TODO: low-pass filters in settings?
+    // enable useODO bit in flags
+    odo_frame_tx.payload[4] |= 1;
+    odo_frame_tx.payload[5] = (ublox->device_state).odometer_mode;
+
+    odo_message_tx = ublox_frame_to_bytes(&odo_frame_tx);
+
+    ack = ublox_worker_i2c_transfer(odo_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
+    if(ack == NULL) {
+        FURI_LOG_E(TAG, "ACK after CFG-ODO set transfer failed");
+        return false;
+    }
+    FURI_LOG_I(
+        TAG, "CFG-ODO ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
+
+    ublox_message_free(odo_message_tx);
+    ublox_message_free(odo_message_rx);
+    ublox_message_free(ack);
+
+    // finally configure the navigation engine
+    UbloxFrame nav5_frame_tx;
+    nav5_frame_tx.class = UBX_CFG_CLASS;
+    nav5_frame_tx.id = UBX_CFG_NAV5_MESSAGE;
+    nav5_frame_tx.len = 0;
+    nav5_frame_tx.payload = NULL;
+    UbloxMessage* nav5_message_tx = ublox_frame_to_bytes(&nav5_frame_tx);
+
+    UbloxMessage* nav5_message_rx =
+        ublox_worker_i2c_transfer(nav5_message_tx, UBX_CFG_NAV5_MESSAGE_LENGTH);
+    ublox_message_free(nav5_message_tx);
+    
+    if(nav5_message_rx == NULL) {
+        FURI_LOG_E(TAG, "CFG-NAV5 transfer failed");
+        return false;
+    }
+
+    // first two bytes tell the GPS what changes to apply, setting this
+    // bit tells it to apply the dynamic platfrom model settings.
+    nav5_frame_tx.class = UBX_CFG_CLASS;
+    nav5_frame_tx.id = UBX_CFG_NAV5_MESSAGE;
+    nav5_frame_tx.len = 36;
+    nav5_frame_tx.payload = nav5_message_rx->message;
+
+    nav5_frame_tx.payload[0] = 1; // tell GPS to apply only the platform model settings
+    nav5_frame_tx.payload[2] = (ublox->device_state).platform_model;
 
+    nav5_message_tx = ublox_frame_to_bytes(&nav5_frame_tx);
+
+    ack = ublox_worker_i2c_transfer(nav5_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
+    if(ack == NULL) {
+        FURI_LOG_E(TAG, "ACK after CFG-NAV5 set transfer failed");
+        return false;
+    }
+    FURI_LOG_I(
+        TAG, "CFG-NAV5 ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
+
+
+    ublox_message_free(nav5_message_rx);
+    ublox_message_free(ack);
+    return true;
+}
+
+// this one is being kind of slow
 void ublox_worker_reset_odo(UbloxWorker* ublox_worker) {
-  FURI_LOG_I(TAG, "ublox_worker_reset_odo");
-  UbloxFrame* odo_frame_tx = malloc(sizeof(UbloxFrame));
-  odo_frame_tx->class = UBX_NAV_CLASS;
-  odo_frame_tx->id = UBX_NAV_RESETODO_MESSAGE;
-  odo_frame_tx->len = 0;
-  odo_frame_tx->payload = NULL;
-  UbloxMessage* odo_message_tx = ublox_frame_to_bytes(odo_frame_tx);
-  ublox_frame_free(odo_frame_tx);
-  
-  UbloxMessage* ack = ublox_worker_i2c_transfer(odo_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
-  if (ack == NULL) {
-    FURI_LOG_E(TAG, "ACK after NAV-RESETODO set transfer failed");
-    ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
-    return;
-  }
-  FURI_LOG_I(TAG, "NAV-RESETODO ack: id = %u, type = %s", ack->message[3], ack->message[3] ? "ACK" : "NAK");
-  ublox_message_free(odo_message_tx);
-  ublox_message_free(ack);
-  // no reason to trigger an event on success, the user will see that
-  // the odometer has been reset on the next update.
+    FURI_LOG_I(TAG, "ublox_worker_reset_odo");
+    UbloxFrame odo_frame_tx;
+    odo_frame_tx.class = UBX_NAV_CLASS;
+    odo_frame_tx.id = UBX_NAV_RESETODO_MESSAGE;
+    odo_frame_tx.len = 0;
+    odo_frame_tx.payload = NULL;
+    UbloxMessage* odo_message_tx = ublox_frame_to_bytes(&odo_frame_tx);
+
+    UbloxMessage* ack = ublox_worker_i2c_transfer(odo_message_tx, UBX_ACK_ACK_MESSAGE_LENGTH);
+    
+    ublox_message_free(odo_message_tx);
+    
+    if(ack == NULL) {
+        FURI_LOG_E(TAG, "ACK after NAV-RESETODO set transfer failed");
+        ublox_worker->callback(UbloxWorkerEventFailed, ublox_worker->context);
+        return;
+    } else {
+	FURI_LOG_I(
+	    TAG,
+	    "NAV-RESETODO ack: id = %u, type = %s",
+	    ack->message[3],
+	    ack->message[3] ? "ACK" : "NAK");
+	
+	ublox_message_free(ack);
+    }
+    ublox_worker->callback(UbloxWorkerEventOdoReset, ublox_worker->context);
+    // no reason to trigger an event on success, the user will see that
+    // the odometer has been reset on the next update.
 }
-  /*FuriString* s = furi_string_alloc();
-  for (int i = 0; i < 92+8; i++) {
-    furi_string_cat_printf(s, "0x%x, ", message_rx->message[i]);
-  }
-  FURI_LOG_I(TAG, "array: %s", furi_string_get_cstr(s));
-  furi_string_free(s);*/

+ 26 - 17
non_catalog_apps/ublox/ublox_worker.h

@@ -5,20 +5,24 @@
 typedef struct UbloxWorker UbloxWorker;
 
 typedef enum {
-  UbloxWorkerStateNone,
-  UbloxWorkerStateReady,
-  UbloxWorkerStateRead,
-  UbloxWorkerStateResetOdometer,
-  UbloxWorkerStateStop,
+    UbloxWorkerStateNone,
+    UbloxWorkerStateReady,
+    UbloxWorkerStateRead,
+    UbloxWorkerStateSyncTime,
+    UbloxWorkerStateResetOdometer,
+    UbloxWorkerStateStop,
 } UbloxWorkerState;
 
 typedef enum {
-  // reserve space for application events
-  UbloxWorkerEventReserved = 50,
-  
-  UbloxWorkerEventSuccess,
-  UbloxWorkerEventFailed,
-  UbloxWorkerEventDataReady,
+    // reserve space for application events
+    UbloxWorkerEventReserved = 50,
+
+    UbloxWorkerEventSuccess,
+    UbloxWorkerEventFailed,
+    UbloxWorkerEventDataReady,
+    UbloxWorkerEventOdoReset,
+    // specific event to update the screen on log state changed
+    UbloxWorkerEventLogStateChanged,
 } UbloxWorkerEvent;
 
 typedef void (*UbloxWorkerCallback)(UbloxWorkerEvent event, void* context);
@@ -29,13 +33,18 @@ UbloxWorkerState ublox_worker_get_state(UbloxWorker* ublox_worker);
 
 void ublox_worker_free(UbloxWorker* ublox_worker);
 
-void ublox_worker_start(UbloxWorker* ublox_worker,
-			UbloxWorkerState state,
-			UbloxWorkerCallback callback,
-			void* context);
+void ublox_worker_start(
+    UbloxWorker* ublox_worker,
+    UbloxWorkerState state,
+    UbloxWorkerCallback callback,
+    void* context);
 
 void ublox_worker_stop(UbloxWorker* ublox_worker);
-		       
-bool ublox_worker_init_gps();//UbloxWorker* ublox_worker);
 
+UbloxMessage* ublox_worker_i2c_transfer(UbloxMessage* message_tx, uint8_t read_length);
+    
+bool ublox_worker_init_gps();
 
+void ublox_worker_read_nav_messages(void* context);
+
+void ublox_worker_sync_to_gps_time(void* context);

+ 9 - 7
non_catalog_apps/ublox/ublox_worker_i.h

@@ -6,21 +6,23 @@
 #include <furi.h>
 #include <furi_hal.h>
 
+#include <toolbox/path.h>
+
 struct UbloxWorker {
-  FuriThread* thread;
-  FuriTimer* timer;
-  
-  UbloxWorkerCallback callback;
-  void* context;
+    FuriThread* thread;
+    FuriTimer* timer;
+
+    UbloxWorkerCallback callback;
+    void* context;
 
-  UbloxWorkerState state;
+    UbloxWorkerState state;
 };
 
 void ublox_worker_change_state(UbloxWorker* ublox_worker, UbloxWorkerState state);
 
 int32_t ublox_worker_task(void* context);
 
-void ublox_worker_read_pvt(UbloxWorker* ublox_worker);
+bool ublox_worker_read_pvt(UbloxWorker* ublox_worker);
 
 bool ublox_worker_read_odo(UbloxWorker* ublox_worker);
 

+ 268 - 229
non_catalog_apps/ublox/views/data_display_view.c

@@ -4,268 +4,307 @@
 
 #define TAG "data_display_view"
 
-struct DataDisplayView {
-  View* view;
-  DataDisplayViewCallback callback;
-  void* context;
-};
+typedef struct DataDisplayView {
+    View* view;
+    DataDisplayViewCallback callback;
+    void* context;
+} DataDisplayView;
 
 typedef struct {
-  DataDisplayState state;
-  Ublox_NAV_PVT_Message nav_pvt;
-  Ublox_NAV_ODO_Message nav_odo;
+    DataDisplayState state;
+    Ublox_NAV_PVT_Message nav_pvt;
+    Ublox_NAV_ODO_Message nav_odo;
+    UbloxLogState log_state;
 } DataDisplayViewModel;
 
-static void data_display_draw_callback(Canvas* canvas, void* model) {
-  DataDisplayViewModel* m = model;
-  if (m->state == DataDisplayGPSNotFound) {
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 27, 20, "GPS not found!");
-
-    canvas_set_font(canvas, FontSecondary);
-    canvas_draw_str(canvas, 5, 34, "Connect u-blox 8 series GPS.");
-    
-  } else if (m->state == DataDisplayHandheldMode) {
-    // TODO: check invalidLlh flag in flags3
-    Ublox_NAV_PVT_Message message = m->nav_pvt;
-    Ublox_NAV_ODO_Message nav_odo = m->nav_odo;
-    FuriString* s = furi_string_alloc();
+static void draw_buttons(Canvas* canvas, void* model) {
+    DataDisplayViewModel* m = model;
     elements_button_left(canvas, "Config");
-    elements_button_center(canvas, "Reset");
-    
-    /*** Draw fix ***/
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 0, 9, "Fix:");
-
-    canvas_set_font(canvas, FontSecondary);
-    
-    if (message.fixType == 0) {
-      canvas_draw_str(canvas, 21, 9, "N");
-    } else if (message.fixType == 1) {
-      canvas_draw_str(canvas, 21, 9, "R");
-    } else if (message.fixType == 2) {
-      canvas_draw_str(canvas, 21, 9, "2D");
-    } else if (message.fixType == 3) {
-      canvas_draw_str(canvas, 21, 9, "3D");
-    } else if (message.fixType == 4) {
-      canvas_draw_str(canvas, 21, 9, "G+D");
-    } else if (message.fixType == 5) {
-      canvas_draw_str(canvas, 21, 9, "TO");
-    }
-
-    /*** Draw number of satellites ***/
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 37, 9, "Sat:");
-
-    canvas_set_font(canvas, FontSecondary);
-    furi_string_printf(s, "%u", message.numSV);
-    canvas_draw_str(canvas, 60, 9, furi_string_get_cstr(s));
-
-    /*** Draw odometer ***/
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 74, 9, "Odo:");
-
-    canvas_set_font(canvas, FontSecondary);
-    furi_string_printf(s, "%.1f", (double)(nav_odo.distance / 1e3));
-    canvas_draw_str(canvas, 100, 9, furi_string_get_cstr(s));
-    
-    /*** Draw latitude ***/
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 0, 22, "Lat:");
-
-    canvas_set_font(canvas, FontSecondary);
-    furi_string_printf(s, "%.4f", (double)(message.lat / 1e7));
-    canvas_draw_str(canvas, 22, 22, furi_string_get_cstr(s));
-    
-    /*** Draw longitude ***/
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 0, 35, "Lon:");
-
-    canvas_set_font(canvas, FontSecondary);
-    furi_string_printf(s, "%.4f", (double)(message.lon / 1e7));
-    canvas_draw_str(canvas, 23, 35, furi_string_get_cstr(s));
-
-    /*** Draw altitude ***/
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 70, 22, "Alt:");
-
-    canvas_set_font(canvas, FontSecondary);
-    // hMSL is height above mean sea level in mm
-    if (locale_get_measurement_unit() == LocaleMeasurementUnitsMetric) {
-      furi_string_printf(s, "%.0fm", (double)(message.hMSL / 1e3));
+    if(m->log_state == UbloxLogStateLogging) {
+	elements_button_right(canvas, "Stop Log");
     } else {
-      furi_string_printf(s, "%.0fft", (double)(message.hMSL / 1e3 * 3.281));
+	elements_button_right(canvas, "Start Log");
     }
-    canvas_draw_str(canvas, 91, 22, furi_string_get_cstr(s));
-    
-    /*** Draw heading ***/
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 75, 35, "Head:");
-
-    canvas_set_font(canvas, FontSecondary);
-    furi_string_printf(s, "%.0f", (double)(message.headMot / 1e5));
-    canvas_draw_str(canvas, 105, 35, furi_string_get_cstr(s));
-
-    /*** Draw time ***/
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 0, 48, "UTC:");
-
-    canvas_set_font(canvas, FontSecondary);
-    FuriHalRtcDateTime datetime = {
-      .hour = message.hour,
-      .minute = message.min,
-      .second = message.sec,
-
-    };
-
-    FuriString* s2 = furi_string_alloc();
-    // built-in date functions make strings that are too long
-    if (locale_get_date_format() == LocaleDateFormatDMY) {
-      furi_string_printf(s, "%u/%u/'%u ",
-			 message.day, message.month, (message.year % 100));
-    } else if (locale_get_date_format() == LocaleDateFormatMDY) {
-      furi_string_printf(s, "%u/%u/'%u ",
-			 message.month, message.day, (message.year % 100));
-    } else if (locale_get_date_format() == LocaleDateFormatYMD) {
-      furi_string_printf(s, "'%u/%u/%u ",
-			 (message.year % 100), message.month, message.day);
-    }
-    locale_format_time(s2, &datetime, locale_get_time_format(), false);
-    furi_string_cat(s, s2);
-    furi_string_free(s2);
-    
-
-    canvas_draw_str(canvas, 27, 48, furi_string_get_cstr(s));
-    furi_string_free(s);
-    
-  } else if (m->state == DataDisplayCarMode) {
-    Ublox_NAV_PVT_Message message = m->nav_pvt;
-    Ublox_NAV_ODO_Message nav_odo = m->nav_odo;
-    FuriString* s = furi_string_alloc();
-    elements_button_left(canvas, "Config");
-    elements_button_center(canvas, "Reset");
-
-    // TODO: imperial/metric
-    canvas_set_font(canvas, FontPrimary);
-    // gSpeed is in mm/s
-    if (locale_get_measurement_unit() == LocaleMeasurementUnitsMetric) {
-      canvas_draw_str(canvas, 0, 12, "Spd (km/s):");
-      furi_string_printf(s, "%.1f", (double)(message.gSpeed / 1e3));
-    } else {
-      canvas_draw_str(canvas, 0, 12, "Spd (mph):");
-      furi_string_printf(s, "%.1f", (double)(message.gSpeed / 1e3 * 2.23694));
-    }
-
-    canvas_set_font(canvas, FontBigNumbers);
-    canvas_draw_str(canvas, 60, 15, furi_string_get_cstr(s));
-
-
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 0, 29, "Heading:");
-
-    canvas_set_font(canvas, FontBigNumbers);
-    furi_string_printf(s, "%.0f", (double)(message.headMot / 1e5));
-    canvas_draw_str(canvas, 60, 32, furi_string_get_cstr(s));
+}
+	
+static void data_display_draw_callback(Canvas* canvas, void* model) {
+    DataDisplayViewModel* m = model;
+    if(m->state == DataDisplayGPSNotFound) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 27, 20, "GPS not found!");
+
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 5, 34, "Connect u-blox 8 series GPS.");
+
+    } else if(m->state == DataDisplayHandheldMode) {
+        // TODO: check invalidLlh flag in flags3?
+        Ublox_NAV_PVT_Message message = m->nav_pvt;
+        Ublox_NAV_ODO_Message nav_odo = m->nav_odo;
+	draw_buttons(canvas, model);
+        FuriString* s = furi_string_alloc();
+
+        /*** Draw fix ***/
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 0, 9, "F/S:");
+
+        canvas_set_font(canvas, FontSecondary);
+
+        if(message.fixType == 0) {
+	    furi_string_printf(s, "N");
+        } else if(message.fixType == 1) {
+	    furi_string_printf(s, "R");
+        } else if(message.fixType == 2) {
+	    furi_string_printf(s, "2D");
+        } else if(message.fixType == 3) {
+	    furi_string_printf(s, "3D");
+        } else if(message.fixType == 4) {
+	    furi_string_printf(s, "G+D");
+        } else if(message.fixType == 5) {
+	    furi_string_printf(s, "TO");
+        }
+
+        /*** Draw number of satellites ***/
+	furi_string_cat_printf(s, "/%u", message.numSV);
+	canvas_draw_str(canvas, 23, 9, furi_string_get_cstr(s));
+
+        /*** Draw odometer ***/
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 58, 9, "Od:");
+	
+        canvas_set_font(canvas, FontSecondary);
+	// distance values are in meters
+	if(locale_get_measurement_unit() == LocaleMeasurementUnitsMetric) {
+	    furi_string_printf(s, "%.1fkm", (double)(nav_odo.distance / 1e3)); // km
+	} else {
+	    furi_string_printf(s, "%.1fmi", (double)(nav_odo.distance / 1e3 * 0.6214)); // km to mi
+	}
+        canvas_draw_str(canvas, 77, 9, furi_string_get_cstr(s));
+
+	canvas_set_font(canvas, FontPrimary);
+	canvas_draw_str(canvas, 112, 9, "L:");
+
+	canvas_set_font(canvas, FontSecondary);
+	if (m->log_state == UbloxLogStateLogging) {
+	    canvas_draw_str(canvas, 122, 9, "Y"); // yes
+	} else {
+	    canvas_draw_str(canvas, 122, 9, "N"); // no
+	}
+	
+	/*** Draw latitude ***/
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 0, 22, "Lat:");
+
+        canvas_set_font(canvas, FontSecondary);
+        furi_string_printf(s, "%.4f", (double)(message.lat / 1e7));
+        canvas_draw_str(canvas, 22, 22, furi_string_get_cstr(s));
+
+        /*** Draw longitude ***/
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 0, 35, "Lon:");
+
+        canvas_set_font(canvas, FontSecondary);
+        furi_string_printf(s, "%.4f", (double)(message.lon / 1e7));
+        canvas_draw_str(canvas, 23, 35, furi_string_get_cstr(s));
+
+        /*** Draw altitude ***/
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 75, 22, "Alt:");
+
+        canvas_set_font(canvas, FontSecondary);
+        // hMSL is height above mean sea level in mm
+        if(locale_get_measurement_unit() == LocaleMeasurementUnitsMetric) {
+            furi_string_printf(s, "%.0fm", (double)(message.hMSL / 1e3));
+        } else {
+            furi_string_printf(s, "%.0fft", (double)(message.hMSL / 1e3 * 3.281));
+        }
+        canvas_draw_str(canvas, 96, 22, furi_string_get_cstr(s));
+
+        /*** Draw heading ***/
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 75, 35, "Head:");
+
+        canvas_set_font(canvas, FontSecondary);
+        furi_string_printf(s, "%.0f", (double)(message.headMot / 1e5));
+        canvas_draw_str(canvas, 105, 35, furi_string_get_cstr(s));
+
+        /*** Draw time ***/
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 0, 48, "UTC:");
+
+        canvas_set_font(canvas, FontSecondary);
+        FuriHalRtcDateTime datetime = {
+            .hour = message.hour,
+            .minute = message.min,
+            .second = message.sec,
+        };
+
+        FuriString* s2 = furi_string_alloc();
+        // built-in date functions make strings that are too long
+        if(locale_get_date_format() == LocaleDateFormatDMY) {
+            furi_string_printf(s, "%u/%u/'%u ", message.day, message.month, (message.year % 100));
+        } else if(locale_get_date_format() == LocaleDateFormatMDY) {
+            furi_string_printf(s, "%u/%u/'%u ", message.month, message.day, (message.year % 100));
+        } else if(locale_get_date_format() == LocaleDateFormatYMD) {
+            furi_string_printf(s, "'%u/%u/%u ", (message.year % 100), message.month, message.day);
+        }
+        locale_format_time(s2, &datetime, locale_get_time_format(), false);
+        furi_string_cat(s, s2);
+        furi_string_free(s2);
+
+        canvas_draw_str(canvas, 27, 48, furi_string_get_cstr(s));
+        furi_string_free(s);
+
+    } else if(m->state == DataDisplayCarMode) {
+        Ublox_NAV_PVT_Message message = m->nav_pvt;
+        Ublox_NAV_ODO_Message nav_odo = m->nav_odo;
+        FuriString* s = furi_string_alloc();
+	draw_buttons(canvas, model);
+        // TODO: imperial/metric
+        canvas_set_font(canvas, FontPrimary);
+        // gSpeed is in mm/s
+        if(locale_get_measurement_unit() == LocaleMeasurementUnitsMetric) {
+            canvas_draw_str(canvas, 0, 12, "Spd (km/s):");
+            furi_string_printf(s, "%.1f", (double)(message.gSpeed / 1e3));
+        } else {
+            canvas_draw_str(canvas, 0, 12, "Spd (mph):");
+            furi_string_printf(s, "%.1f", (double)(message.gSpeed / 1e3 * 2.23694));
+        }
+
+        canvas_set_font(canvas, FontBigNumbers);
+        canvas_draw_str(canvas, 60, 15, furi_string_get_cstr(s));
+
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 0, 29, "Heading:");
+
+        canvas_set_font(canvas, FontBigNumbers);
+        furi_string_printf(s, "%.0f", (double)(message.headMot / 1e5));
+        canvas_draw_str(canvas, 60, 32, furi_string_get_cstr(s));
+
+        canvas_set_font(canvas, FontPrimary);
+        // distance is in meters
+        if(locale_get_measurement_unit() == LocaleMeasurementUnitsMetric) {
+            canvas_draw_str(canvas, 0, 45, "Odo (km):");
+            furi_string_printf(s, "%.2f", (double)(nav_odo.distance / 1e3));
+        } else {
+            canvas_draw_str(canvas, 0, 45, "Odo (mi):");
+            furi_string_printf(s, "%.2f", (double)(nav_odo.distance / 1e3 * 0.6214));
+        }
+
+        canvas_set_font(canvas, FontBigNumbers);
+        canvas_draw_str(canvas, 60, 49, furi_string_get_cstr(s));
+
+        furi_string_free(s);
 
-    canvas_set_font(canvas, FontPrimary);
-    // distance is in meters
-    if (locale_get_measurement_unit() == LocaleMeasurementUnitsMetric) {
-      canvas_draw_str(canvas, 0, 45, "Odo (km):");
-      furi_string_printf(s, "%.2f", (double)(nav_odo.distance / 1e3));
-    } else {
-      canvas_draw_str(canvas, 0, 45, "Odo (mi):");
-      furi_string_printf(s, "%.2f", (double)(nav_odo.distance / 1e3 * 0.6214));
     }
-
-    canvas_set_font(canvas, FontBigNumbers);
-    canvas_draw_str(canvas, 60, 49, furi_string_get_cstr(s));
-    
-    furi_string_free(s);
-
-    // TODO: compass direction next to heading?
-    // TODO: better localization
-  }
 }
 
 static bool data_display_input_callback(InputEvent* event, void* context) {
-  DataDisplayView* data_display = context;
-  bool consumed = false;
-
-  if (event->type == InputTypeShort) {
-    if (event->key == InputKeyLeft) {
-      if (data_display->callback) {      
-	data_display->callback(data_display->context, event->key);
-      }
-      consumed = true;
-    } else if (event->key == InputKeyOk) {
-      if (data_display->callback) {      
-	data_display->callback(data_display->context, event->key);
-      }
+    DataDisplayView* data_display = context;
+    bool consumed = false;
+
+    if(event->type == InputTypeShort) {
+        if(event->key == InputKeyLeft) {
+            if(data_display->callback) {
+                data_display->callback(data_display->context, event->key);
+            }
+            consumed = true;
+        } else if(event->key == InputKeyOk) {
+            if(data_display->callback) {
+                data_display->callback(data_display->context, event->key);
+            }
+        } else if(event->key == InputKeyRight) {
+	    if(data_display->callback) {
+                data_display->callback(data_display->context, event->key);
+            }
+	    consumed = true;
+	}
     }
-  }
-  return consumed;
+    return consumed;
 }
 
 DataDisplayView* data_display_alloc() {
-  DataDisplayView* data_display = malloc(sizeof(DataDisplayView));
-  data_display->view = view_alloc();
+    DataDisplayView* data_display = malloc(sizeof(DataDisplayView));
+    data_display->view = view_alloc();
 
-  view_allocate_model(data_display->view, ViewModelTypeLocking, sizeof(DataDisplayViewModel));
-  view_set_context(data_display->view, data_display);
-  
-  view_set_draw_callback(data_display->view, data_display_draw_callback);
-  view_set_input_callback(data_display->view, data_display_input_callback);
+    view_allocate_model(data_display->view, ViewModelTypeLocking, sizeof(DataDisplayViewModel));
+    view_set_context(data_display->view, data_display);
 
+    view_set_draw_callback(data_display->view, data_display_draw_callback);
+    view_set_input_callback(data_display->view, data_display_input_callback);
 
-  return data_display;
+    return data_display;
 }
 
 void data_display_free(DataDisplayView* data_display) {
-  furi_assert(data_display);
-  view_free(data_display->view);
-  free(data_display);
+    furi_assert(data_display);
+    view_free(data_display->view);
+    free(data_display);
 }
 
 void data_display_reset(DataDisplayView* data_display) {
-  furi_assert(data_display);
-  Ublox_NAV_PVT_Message p = {0};
-  Ublox_NAV_ODO_Message o = {0};
-  with_view_model(data_display->view,
-		  DataDisplayViewModel * model,
-		  { model->state = DataDisplayHandheldMode;
-		    model->nav_pvt = p;
-		    model->nav_odo = o;
-		  },
-		  false);
+    furi_assert(data_display);
+    Ublox_NAV_PVT_Message p = {0};
+    Ublox_NAV_ODO_Message o = {0};
+    with_view_model(
+        data_display->view,
+        DataDisplayViewModel * model,
+        {
+            model->state = DataDisplayHandheldMode;
+            model->nav_pvt = p;
+            model->nav_odo = o;
+        },
+        false);
 }
 
 View* data_display_get_view(DataDisplayView* data_display) {
-  furi_assert(data_display);
-  return data_display->view;
+    furi_assert(data_display);
+    return data_display->view;
+}
+
+void data_display_set_callback(
+    DataDisplayView* data_display,
+    DataDisplayViewCallback callback,
+    void* context) {
+    furi_assert(data_display);
+    furi_assert(callback);
+    data_display->callback = callback;
+    data_display->context = context;
 }
 
-void data_display_set_callback(DataDisplayView* data_display, DataDisplayViewCallback callback, void* context) {
-  furi_assert(data_display);
-  furi_assert(callback);
-  data_display->callback = callback;
-  data_display->context = context;
+void data_display_set_nav_messages(
+    DataDisplayView* data_display,
+    Ublox_NAV_PVT_Message pvt_message,
+    Ublox_NAV_ODO_Message odo_message) {
+    furi_assert(data_display);
+    with_view_model(
+        data_display->view,
+        DataDisplayViewModel * model,
+        {
+            model->nav_pvt = pvt_message;
+            model->nav_odo = odo_message;
+        },
+        true);
 }
 
-void data_display_set_nav_messages(DataDisplayView* data_display, Ublox_NAV_PVT_Message pvt_message, Ublox_NAV_ODO_Message odo_message) {
-  furi_assert(data_display);
-  with_view_model(data_display->view,
-		  DataDisplayViewModel * model,
-		  {
-		      model->nav_pvt = pvt_message;
-		      model->nav_odo = odo_message;
-		  },
-		  true);
+void data_display_set_log_state(
+    DataDisplayView* data_display,
+    UbloxLogState log_state) {
+    furi_assert(data_display);
+    with_view_model(
+	data_display->view,
+	DataDisplayViewModel * model,
+	{
+	    model->log_state = log_state;
+	},
+	true);
 }
 
 void data_display_set_state(DataDisplayView* data_display, DataDisplayState state) {
-  furi_assert(data_display);
-  with_view_model(data_display->view,
-		  DataDisplayViewModel * model,
-		  { model->state = state; },
-		  true);
+    furi_assert(data_display);
+    with_view_model(
+        data_display->view,
+	DataDisplayViewModel * model,
+	{ model->state = state; },
+	// do refresh
+	true);
 }

+ 14 - 5
non_catalog_apps/ublox/views/data_display_view.h

@@ -7,11 +7,12 @@
 #include <locale/locale.h>
 #include <furi_hal.h>
 #include "../ublox_device.h"
+#include "../helpers/ublox_types.h"
 
 typedef enum {
-  DataDisplayHandheldMode,
-  DataDisplayCarMode,
-  DataDisplayGPSNotFound,
+    DataDisplayHandheldMode,
+    DataDisplayCarMode,
+    DataDisplayGPSNotFound,
 } DataDisplayState;
 
 typedef struct DataDisplayView DataDisplayView;
@@ -26,8 +27,16 @@ void data_display_reset(DataDisplayView* data_display);
 
 View* data_display_get_view(DataDisplayView* data_display);
 
-void data_display_set_callback(DataDisplayView* data_display, DataDisplayViewCallback callback, void* context);
+void data_display_set_callback(
+    DataDisplayView* data_display,
+    DataDisplayViewCallback callback,
+    void* context);
 
-void data_display_set_nav_messages(DataDisplayView* data_display, Ublox_NAV_PVT_Message pvt_message, Ublox_NAV_ODO_Message odo_message);
+void data_display_set_nav_messages(
+    DataDisplayView* data_display,
+    Ublox_NAV_PVT_Message pvt_message,
+    Ublox_NAV_ODO_Message odo_message);
 
 void data_display_set_state(DataDisplayView* data_display, DataDisplayState state);
+
+void data_display_set_log_state(DataDisplayView* data_display, UbloxLogState log_state);