commit:c7c86bb885dc722fda5ac6acc5bb3d6efcf1a570
author:Chip
committer:Chip
date:Sat Mar 12 01:18:22 2022 -0600
parents:e2a5db648636152dc763a509ae2a0fedcf3a7139
Add bluetooth mode, streaming reads thread and test
diff --git a/assets/sprite/btn_ffwd_4.png b/assets/sprite/btn_ffwd_4.png
line changes: +0/-0
index 0000000..e890745
--- /dev/null
+++ b/assets/sprite/btn_ffwd_4.png

diff --git a/assets/sprite/btn_ftrk_5.png b/assets/sprite/btn_ftrk_5.png
line changes: +0/-0
index 0000000..2b03e16
--- /dev/null
+++ b/assets/sprite/btn_ftrk_5.png

diff --git a/assets/sprite/btn_list.h b/assets/sprite/btn_list.h
line changes: +7/-0
index 0000000..a4b8d14
--- /dev/null
+++ b/assets/sprite/btn_list.h
@@ -0,0 +1,7 @@
+static uint8_t* btn_list[] = {
+	btn_1_bits,
+	btn_2_bits,
+	btn_3_bits,
+	btn_4_bits,
+	btn_5_bits,
+};

diff --git a/assets/sprite/btn_pause_3.png b/assets/sprite/btn_pause_3.png
line changes: +0/-0
index 0000000..c33a495
--- /dev/null
+++ b/assets/sprite/btn_pause_3.png

diff --git a/assets/sprite/btn_play_3.png b/assets/sprite/btn_play_3.png
line changes: +0/-0
index 0000000..5cafa83
--- /dev/null
+++ b/assets/sprite/btn_play_3.png

diff --git a/assets/sprite/btn_rrev_2.png b/assets/sprite/btn_rrev_2.png
line changes: +0/-0
index 0000000..a89ced9
--- /dev/null
+++ b/assets/sprite/btn_rrev_2.png

diff --git a/assets/sprite/btn_rtrk_1.png b/assets/sprite/btn_rtrk_1.png
line changes: +0/-0
index 0000000..4be8842
--- /dev/null
+++ b/assets/sprite/btn_rtrk_1.png

diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
line changes: +12/-1
index 67c48a5..af0e6a4
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -1,4 +1,15 @@
 idf_component_register(
-	SRCS "main.c" "keyboard.c" "u8g2_esp32_hal.c" "io.c" "fonts.c" "pm.c" "gfx.c" "audio.c" "bt.c" "battery.c"
+	SRCS "main.c"
+	     "keyboard.c"
+	     "u8g2_esp32_hal.c"
+	     "io.c"
+	     "fonts.c"
+	     "pm.c"
+	     "gfx.c"
+	     "audio.c"
+	     "bt.c"
+	     "battery.c"
+	     "play.c"
+	     "file.c"
 	INCLUDE_DIRS "."
 )

diff --git a/main/bt.c b/main/bt.c
line changes: +59/-4
index 53c980d..224cd54
--- a/main/bt.c
+++ b/main/bt.c
@@ -11,13 +11,14 @@
 #include "audio.h"
 #include "bt.h"
 #include "gfx.h"
+#include "play.h"
 
 void _bt_set_mode(bt_mode m);
 
 bt_mode mode = BT_INACTIVE;
 esp_bd_addr_t pairing_peer = {0};
 char bt_addr_buf[18];
-esp_avrc_playback_stat_t playback_state = ESP_AVRC_PLAYBACK_STOPPED;
+bt_playback_state playback_state = {0};
 
 char* bt_addr_str(esp_bd_addr_t addr) {
 	snprintf(bt_addr_buf, 18, "%02x:%02x:%02x:%02x:%02x:%02x", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
@@ -38,6 +39,10 @@ char* arr_to_str(void* val, int len) {
 	return s;
 }
 
+int max(int a, int b) {
+	return a > b ? a : b;
+}
+
 void set_last_connected(esp_bd_addr_t* bda) {
 	const char TAG[] = "set_last_connected";
 	nvs_handle_t h;
@@ -109,6 +114,7 @@ void a2dp_connection_state_event(struct a2d_conn_stat_param* p) {
 	case ESP_A2D_CONNECTION_STATE_CONNECTED:
 		event_str = "Connected to";
 		set_last_connected(&p->remote_bda);
+		play_set_media_type(PLAY_MEDIA_BLUETOOTH);
 		break;
 	case ESP_A2D_CONNECTION_STATE_CONNECTING:
 		event_str = "Connecting to";
@@ -162,6 +168,15 @@ void a2dp_callback(esp_a2d_cb_event_t event, esp_a2d_cb_param_t* param) {
 	}
 }
 
+void grab_metadata() {
+	esp_err_t ret;
+	
+	ret = esp_avrc_ct_send_metadata_cmd(1, ESP_AVRC_MD_ATTR_TITLE | ESP_AVRC_MD_ATTR_ARTIST | ESP_AVRC_MD_ATTR_PLAYING_TIME);
+	if (ret != ESP_OK) {
+		ESP_LOGE("BT", "Could not send metadata request: %s", esp_err_to_name(ret));
+	}
+}
+
 void a2dp_data_callback(const uint8_t *data, uint32_t len) {
 	audio_send((uint16_t*)data, len / 2);
 }
@@ -170,10 +185,14 @@ void avrc_callback(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t* param) 
 	const char TAG[] = "avrc_callback";
 	char *text;
 	esp_err_t ret;
+	int len;
 
 	switch (event) {
 	case ESP_AVRC_CT_CONNECTION_STATE_EVT:
 		ESP_LOGI(TAG, "connection state connected:%d addr:%s", param->conn_stat.connected, bt_addr_str(param->conn_stat.remote_bda));
+		if (param->conn_stat.connected) {
+			grab_metadata();
+		}
 		break;
 	case ESP_AVRC_CT_PASSTHROUGH_RSP_EVT:
 		// param->psth_rsp
@@ -183,7 +202,25 @@ void avrc_callback(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t* param) 
 		// param->meta_rsp
 		text = arr_to_str(param->meta_rsp.attr_text, param->meta_rsp.attr_length);
 		ESP_LOGI(TAG, "metadata response attr_id:%d text:%s", param->meta_rsp.attr_id, text);
+		gfx_data_t* gd = gfx_acquire_data(SCENE_NO_CHANGE);
+		switch(param->meta_rsp.attr_id) {
+		case ESP_AVRC_MD_ATTR_TITLE:
+			len = max(param->meta_rsp.attr_length, META_STR_MAX);
+			memcpy(gd->playing_meta.title, param->meta_rsp.attr_text, len);
+			gd->playing_meta.title[META_STR_MAX - 1] = 0;
+			break;
+		case ESP_AVRC_MD_ATTR_ARTIST:
+			len = max(param->meta_rsp.attr_length, META_STR_MAX);
+			memcpy(gd->playing_meta.artist, param->meta_rsp.attr_text, len);
+			gd->playing_meta.artist[META_STR_MAX - 1] = 0;
+			break;
+		case ESP_AVRC_MD_ATTR_PLAYING_TIME:
+			gd->playing_meta.track_length = atoi(text) / 1000;
+			break;
+		}
+		gfx_release_data();
 		free(text);
+
 		break;
 	case ESP_AVRC_CT_PLAY_STATUS_RSP_EVT:
 		// ???
@@ -218,8 +255,9 @@ void avrc_callback(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t* param) 
 				break;
 			}
 			ESP_LOGI(TAG, "playback status changed:%s", text);
+			grab_metadata();
 
-			playback_state = param->change_ntf.event_parameter.playback;
+			playback_state.state = param->change_ntf.event_parameter.playback;
 
 			ret = esp_avrc_ct_send_register_notification_cmd(1, ESP_AVRC_RN_PLAY_STATUS_CHANGE, 0);
 			if (ret != ESP_OK) {
@@ -229,6 +267,13 @@ void avrc_callback(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t* param) 
 		case ESP_AVRC_RN_TRACK_CHANGE:
 			ESP_LOGI(TAG, "track change:");
 			print_hex(param->change_ntf.event_parameter.elm_id, 8);
+			printf("\n");
+			grab_metadata();
+
+			ret = esp_avrc_ct_send_register_notification_cmd(3, ESP_AVRC_RN_TRACK_CHANGE, 0);
+			if (ret != ESP_OK) {
+				ESP_LOGE(TAG, "Could not register track change");
+			}
 			break;
 		case ESP_AVRC_RN_PLAY_POS_CHANGED:
 			ESP_LOGI(TAG, "play position change:%d", param->change_ntf.event_parameter.play_pos);
@@ -285,6 +330,16 @@ void avrc_callback(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t* param) 
 			ESP_LOGI(TAG, "volume change not supported");
 		}
 
+		if (esp_avrc_rn_evt_bit_mask_operation(ESP_AVRC_BIT_MASK_OP_TEST, events, ESP_AVRC_RN_TRACK_CHANGE)) {
+			ret = esp_avrc_ct_send_register_notification_cmd(3, ESP_AVRC_RN_TRACK_CHANGE, 0);
+			if (ret != ESP_OK) {
+				ESP_LOGE(TAG, "Could not register track change");
+			}
+			ESP_LOGI(TAG, "track change notification registered");
+		} else {
+			ESP_LOGI(TAG, "track change not supported");
+		}
+
 		break;
 	case ESP_AVRC_CT_SET_ABSOLUTE_VOLUME_RSP_EVT:
 		// param->set_volume_rsp
@@ -543,6 +598,6 @@ void bt_send_key(esp_avrc_pt_cmd_t cmd, esp_avrc_pt_cmd_state_t state) {
 	esp_avrc_ct_send_passthrough_cmd(1, cmd, state);
 }
 
-esp_avrc_playback_stat_t bt_get_playback_state() {
-	return playback_state;
+bt_playback_state* bt_get_playback_state() {
+	return &playback_state;
 }

diff --git a/main/bt.h b/main/bt.h
line changes: +6/-1
index 30927f5..14efc04
--- a/main/bt.h
+++ b/main/bt.h
@@ -9,11 +9,16 @@ typedef enum {
 	BT_DISCOVERABLE,
 } bt_mode;
 
+
+typedef struct {
+	esp_avrc_playback_stat_t state;
+} bt_playback_state;
+
 int bt_init();
 bt_mode bt_get_mode();
 void bt_set_mode(bt_mode m);
 void bt_confirm_pair(int accept);
 void bt_send_key(esp_avrc_pt_cmd_t cmd, esp_avrc_pt_cmd_state_t state);
-esp_avrc_playback_stat_t bt_get_playback_state();
+bt_playback_state* bt_get_playback_state();
 
 #endif // __BT_H

diff --git a/main/file.c b/main/file.c
line changes: +186/-0
index 0000000..a1d70ce
--- /dev/null
+++ b/main/file.c
@@ -0,0 +1,186 @@
+#include <stdio.h>
+#include <stdbool.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/queue.h"
+#include "freertos/semphr.h"
+#include "freertos/stream_buffer.h"
+#include "esp_log.h"
+
+#include "file.h"
+
+#define BUFSIZE 4096
+#define TAG "file reader"
+
+typedef enum {
+	FILE_READER_OPEN,
+	FILE_READER_SUBSCRIBE,
+	FILE_READER_START,
+	FILE_READER_STOP,
+	FILE_READER_SEEK,
+	FILE_READER_CLOSE,
+} file_reader_command_t;
+
+typedef struct {
+	long offset;
+	int whence;
+} file_reader_seek_args_t;
+
+typedef struct {
+	file_reader_command_t command;
+	union {
+		const char* open_filename;
+		StreamBufferHandle_t subscribe_sb;
+		file_reader_seek_args_t seek;
+	};
+} file_reader_command_args_t;
+
+QueueHandle_t file_reader_command_queue = NULL;
+TaskHandle_t  file_reader_task_handle = NULL;
+StreamBufferHandle_t file_reader_streambuffer = NULL;
+SemaphoreHandle_t seek_sync_sem = NULL;
+
+void file_reader_task(void* pvParameters) {
+	BaseType_t err;
+	bool reading = false;
+	size_t bytes_read = 0, bytes_sent = 0;
+	FILE* f = NULL;
+
+	uint8_t *buf = (uint8_t *) malloc(BUFSIZE);
+	if (buf == NULL) {
+		ESP_LOGE(TAG, "Could not allocate read buffer");
+		return;
+	}
+
+	ESP_LOGI(TAG, "Reader task started");
+
+	while (1) {
+		file_reader_command_args_t a;
+		err = xQueueReceive(file_reader_command_queue, &a, reading ? 0 : portMAX_DELAY);
+		if (err == pdTRUE) {
+			ESP_LOGI(TAG, "Received command %d", a.command);
+			switch (a.command) {
+			case FILE_READER_OPEN:
+				if (f) {
+					fclose(f);
+				}
+				f = fopen(a.open_filename, "r");
+				if (f == NULL) {
+					ESP_LOGE(TAG, "Could not open %s", a.open_filename);
+				}
+				break;
+			case FILE_READER_SUBSCRIBE:
+				file_reader_streambuffer = a.subscribe_sb;
+				break;
+			case FILE_READER_START:
+				reading = true;
+				break;
+			case FILE_READER_STOP:
+				reading = false;
+				break;
+			case FILE_READER_SEEK:
+				if (f == NULL) {
+					ESP_LOGE(TAG, "Cannot seek without file");
+					break;
+				}
+				fseek(f, a.seek.offset, a.seek.whence);
+				bytes_sent = bytes_read = 0;
+				if (file_reader_streambuffer) {
+					err = xStreamBufferReset(file_reader_streambuffer);
+					if (err != pdPASS) {
+						ESP_LOGE(TAG, "Could not reset streambuffer during seek");
+					}
+				}
+				xSemaphoreGive(seek_sync_sem);
+				break;
+			case FILE_READER_CLOSE:
+				fclose(f);
+				f = NULL;
+				reading = false;
+				bytes_sent = bytes_read = 0;
+				break;
+			}
+		}
+
+		if (reading) {
+			ESP_LOGI(TAG, "%d sent %d read", bytes_sent, bytes_read);
+			if (bytes_sent == bytes_read) {
+				if (f == NULL) {
+					ESP_LOGE(TAG, "FILE pointer null while reading");
+					reading = false;
+				} else if (feof(f)) {
+					ESP_LOGI(TAG, "end of file");
+					fclose(f);
+					f = NULL;
+				} else {
+					bytes_sent = 0;
+					bytes_read = fread(buf, 1, BUFSIZE, f);
+					ESP_LOGI(TAG, "read %d bytes", bytes_read);
+				}
+			}
+			bytes_sent += xStreamBufferSend(
+				file_reader_streambuffer,
+				buf + bytes_sent,
+				bytes_read - bytes_sent,
+				500 / portTICK_PERIOD_MS
+			);
+		}
+	}
+
+	vTaskDelete(NULL);
+}
+
+int file_init() {
+	BaseType_t err;
+
+	seek_sync_sem = xSemaphoreCreateBinary();
+	file_reader_command_queue = xQueueCreate(1, sizeof(file_reader_command_args_t));
+	
+	err = xTaskCreate(file_reader_task, "file reader", 4096, NULL, 2, &file_reader_task_handle);
+	if (err != pdPASS) {
+		ESP_LOGE(TAG, "could not initialize file reader task: %d", err);
+		return 0;
+	}
+
+	return 1;
+}
+
+void file_reader_open(const char* filename) {
+	file_reader_command_args_t a;
+	a.command = FILE_READER_OPEN;
+	a.open_filename = filename;
+	xQueueSend(file_reader_command_queue, &a, portMAX_DELAY);
+}
+
+void file_reader_subscribe(StreamBufferHandle_t bh) {
+	file_reader_command_args_t a;
+	a.command = FILE_READER_SUBSCRIBE;
+	a.subscribe_sb = bh;
+	xQueueSend(file_reader_command_queue, &a, portMAX_DELAY);
+}
+
+void file_reader_start() {
+	file_reader_command_args_t a;
+	a.command = FILE_READER_START;
+	xQueueSend(file_reader_command_queue, &a, portMAX_DELAY);
+}
+
+void file_reader_stop() {
+	file_reader_command_args_t a;
+	a.command = FILE_READER_STOP;
+	xQueueSend(file_reader_command_queue, &a, portMAX_DELAY);
+}
+
+void file_reader_seek(long offset, int whence) {
+	file_reader_command_args_t a;
+	a.command = FILE_READER_SEEK;
+	a.seek.offset = offset;
+	a.seek.whence = whence;
+	xQueueSend(file_reader_command_queue, &a, portMAX_DELAY);
+	xSemaphoreTake(seek_sync_sem, portMAX_DELAY);
+}
+
+void file_reader_close() {
+	file_reader_command_args_t a;
+	a.command = FILE_READER_CLOSE;
+	xQueueSend(file_reader_command_queue, &a, portMAX_DELAY);
+}

diff --git a/main/file.h b/main/file.h
line changes: +12/-0
index 0000000..554b34f
--- /dev/null
+++ b/main/file.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/stream_buffer.h"
+
+int file_init();
+void file_reader_open(const char* filename);
+void file_reader_subscribe(StreamBufferHandle_t bh);
+void file_reader_start();
+void file_reader_stop();
+void file_reader_seek(long offset, int whence);
+void file_reader_close();

diff --git a/main/gfx.c b/main/gfx.c
line changes: +45/-11
index 719da1f..7febede
--- a/main/gfx.c
+++ b/main/gfx.c
@@ -81,30 +81,64 @@ void gfx_message(const char* str) {
 	key_get_pressed();
 }
 
-void draw_play(play_scene_t *data) {
+void draw_play(gfx_data_t *data) {
 	struct tm localtime;
-	char buf[10];
-	int len;
+	int len, x;
+	char buf[68];
+	uint64_t t = esp_timer_get_time();
 
 	u8g2_ClearBuffer(&u8g2);
 
+	u8g2_DrawHLine(&u8g2, 0, 20, 59);
+	u8g2_DrawLine(&u8g2, 59, 19, 68, 10);
+	u8g2_DrawHLine(&u8g2, 69, 9, 59);
+
+	if (data->play.play_state == PLAY_STATE_PLAYING) {
+		x = (t / 4000) % 256;
+		if (x < 128) {
+			u8g2_SetDrawColor(&u8g2, 0);
+			u8g2_DrawBox(&u8g2, x, 9, 13, 12);
+			u8g2_SetDrawColor(&u8g2, 1);
+		}
+	}
+
+	u8g2_DrawXBM(&u8g2, 2, 0, btn_rtrk_1_width, btn_rtrk_1_height, btn_rtrk_1_bits);
+	u8g2_DrawXBM(&u8g2, 13, 0, btn_rrev_2_width, btn_rrev_2_height, btn_rrev_2_bits);
+	if (data->play.play_state == PLAY_STATE_PLAYING) {
+		u8g2_DrawXBM(&u8g2, 22, 0, btn_play_3_width, btn_play_3_height, btn_play_3_bits);
+	} else {
+		u8g2_DrawXBM(&u8g2, 22, 0, btn_pause_3_width, btn_pause_3_height, btn_pause_3_bits);
+	}
+	u8g2_DrawXBM(&u8g2, 31, 0, btn_ffwd_4_width, btn_ffwd_4_height, btn_ffwd_4_bits);
+	u8g2_DrawXBM(&u8g2, 40, 0, btn_ftrk_5_width, btn_ftrk_5_height, btn_ftrk_5_bits);
+
 	u8g2_SetFont(&u8g2, hyperspace_7n);
 
-	gmtime_r(&data->elapsed, &localtime);
+	gmtime_r(&data->playing_meta.track_position, &localtime);
 	strftime(buf, sizeof(buf), "%M:%S", &localtime);
 	len = u8g2_GetStrWidth(&u8g2, buf);
 	u8g2_DrawStr(&u8g2, 58 - len, 18, buf);
 
-	gmtime_r(&data->total, &localtime);
+	gmtime_r(&data->playing_meta.track_length, &localtime);
 	strftime(buf, sizeof(buf), "%M:%S", &localtime);
 	u8g2_DrawStr(&u8g2, 70, 19, buf);
 
-	u8g2_DrawHLine(&u8g2, 0, 20, 59);
-	u8g2_DrawLine(&u8g2, 59, 19, 68, 10);
-	u8g2_DrawHLine(&u8g2, 69, 9, 59);
+	// TODO: actual font
+	u8g2_SetFont(&u8g2, u8g2_font_6x10_tf);
+	if (data->playing_meta.artist[0]) {
+		snprintf(buf, 68, "%s - %s", data->playing_meta.artist, data->playing_meta.title);
+	} else {
+		snprintf(buf, 68, "%s", data->playing_meta.title);
+	}
 
-	u8g2_SetFont(&u8g2, maximum_overdrift);
-	u8g2_DrawStr(&u8g2, 2, 30, data->title);
+	u8g2_uint_t w = u8g2_GetStrWidth(&u8g2, buf);
+	if (w > 128) {
+		int l = w - 128;
+		int x = (t / 100000) % l;
+		u8g2_DrawStr(&u8g2, -x, 30, buf);
+	} else {
+		u8g2_DrawStr(&u8g2, 0, 30, buf);
+	}
 }
 
 void draw_bt(bt_info_t* info) {
@@ -166,7 +200,7 @@ void gfx_thread(void *pvParameters) {
 			draw_menu(&gfx_data.menu);
 			break;
 		case SCENE_PLAY:
-			draw_play(&gfx_data.play);
+			draw_play(&gfx_data);
 			break;
 		case SCENE_BT:
 			draw_bt(&gfx_data.bt);

diff --git a/main/gfx.h b/main/gfx.h
line changes: +12/-3
index f20d56e..2d2362b
--- a/main/gfx.h
+++ b/main/gfx.h
@@ -7,6 +7,9 @@
 
 #include "battery.h"
 #include "bt.h"
+#include "play.h"
+
+#define META_STR_MAX 32
 
 typedef enum {
 	SCENE_CLOCK,
@@ -17,6 +20,13 @@ typedef enum {
 } scene_t;
 
 typedef struct {
+	char title[META_STR_MAX];
+	char artist[META_STR_MAX];
+	time_t track_position;
+	time_t track_length;
+} playing_metadata_t;
+
+typedef struct {
 	const char *label;
 	void (*func)();
 } menu_def_t;
@@ -32,9 +42,7 @@ typedef struct {
 } menu_scene_t;
 
 typedef struct {
-	char *title;
-	time_t elapsed;
-	time_t total;
+	play_state play_state;
 } play_scene_t;
 
 typedef struct {
@@ -46,6 +54,7 @@ typedef struct {
 	scene_t scene;
 	bt_info_t bt;
 	battery_info_t battery;
+	playing_metadata_t playing_meta;
 	union {
 		clock_scene_t clock;
 		menu_scene_t menu;

diff --git a/main/main.c b/main/main.c
line changes: +57/-96
index 9b952d9..6ef7e8a
--- a/main/main.c
+++ b/main/main.c
@@ -30,7 +30,9 @@
 #include "audio.h"
 #include "battery.h"
 #include "bt.h"
+#include "file.h"
 #include "fonts.h"
+#include "play.h"
 #include "sprites.h"
 #include "keyboard.h"
 #include "gfx.h"
@@ -191,38 +193,7 @@ size_t roll_to_syncword(uint8_t *buf, int bufsize, StreamBufferHandle_t sb, int 
 	return total_roll;
 }
 
-typedef struct {
-	const char *         filename;
-	StreamBufferHandle_t sb;
-	int                  eof;
-} read_task_parameter_t;
-
-void read_task(void *pvParameters) {
-	read_task_parameter_t *params = (read_task_parameter_t *) pvParameters;
-
-	uint8_t *buf = (uint8_t *) malloc(BUFSIZE / 2);
-	if (buf == NULL) {
-		ESP_LOGE("read_task", "Could not allocate read buffer");
-		return;
-	}
-
-	FILE *f = fopen(params->filename, "r");
-	if (f == NULL) {
-		ESP_LOGE("read_task", "Could not open %s", params->filename);
-		return;
-	}
-
-	while (!feof(f)) {
-		size_t n = fread(buf, 1, BUFSIZE / 2, f);
-		ESP_LOGI("read_task", "read %d bytes", n);
-		xStreamBufferSend(params->sb, buf, n, portMAX_DELAY);
-	}
-
-	params->eof = 1;
-
-	vTaskDelete(NULL);
-}
-
+/*
 void play_mp3() {
 	const char TAG[] = "play_mp3";
 	BaseType_t xerr;
@@ -468,6 +439,7 @@ void play_mod() {
 		ESP_LOGI(TAG, "wrote %d samples", bytes_written);
 	}
 }
+*/
 
 void print_task_list(void *pvParameters) {
 	while (1) {
@@ -690,55 +662,6 @@ void test_mode() {
 	do_menu("TEST", 5, menu);
 }
 
-void play_mode() {
-	TaskHandle_t decoder_task;
-	key_event_t ke;
-	struct timeval tv;
-	float now, t;
-
-	FATFS *fs = sd_card_mount();
-	if (fs == NULL) {
-		u8g2_ClearBuffer(&u8g2);
-		u8g2_DrawStr(&u8g2, 51, 19, "NO SD");
-		u8g2_SendBuffer(&u8g2);
-		vTaskDelay(1000 / portTICK_PERIOD_MS);
-		return;
-	}
-
-	int ret = audio_open();
-	if (!ret) {
-		gfx_message("NO AUDIO");
-		return;
-	}
-
-	SemaphoreHandle_t waiter = xSemaphoreCreateBinary();
-	xTaskCreate(play_flac, "flac", 4096, &waiter, 5, &decoder_task);
-
-	gettimeofday(&tv, NULL);
-	now = (float)tv.tv_sec + (float)tv.tv_usec / 1000000.0;
-
-	while (ke = key_get_event(0), !(ke.type == KEY_PRESSED && ke.code == KEY_BAND)) {
-		gettimeofday(&tv, NULL);
-		t = (float)tv.tv_sec + (float)tv.tv_usec / 1000000.0;
-		gfx_data_t *gd = gfx_acquire_data(SCENE_PLAY);
-		gd->play.title = "FLAC";
-		gd->play.elapsed = (time_t) (t - now);
-		gd->play.total = (time_t) 341;
-		gfx_release_data();
-
-		vTaskDelay(100 / portTICK_PERIOD_MS);
-	}
-
-	// Kill the decoder
-	xTaskNotifyGive(decoder_task);
-
-	xSemaphoreTake(waiter, portMAX_DELAY);
-
-	sd_card_unmount();
-
-	audio_close();
-}
-
 void bt_config_mode() {
 	key_event_t ke;
 
@@ -757,14 +680,6 @@ void bt_config_mode() {
 				}
 			}
 		} else {
-			if (ke.code == KEY_SNOOZE && ke.type != KEY_NO_EVENT) {
-				if (bt_get_playback_state() == ESP_AVRC_PLAYBACK_PLAYING) {
-					bt_send_key(ESP_AVRC_PT_CMD_PAUSE, ke.type - 1);
-				} else {
-					bt_send_key(ESP_AVRC_PT_CMD_PLAY, ke.type - 1);
-				}
-			}
-
 			if (ke.type == KEY_PRESSED) {
 				switch (ke.code) {
 				case KEY_1:
@@ -795,7 +710,7 @@ void erase_nvs() {
 
 void main_menu() {
 	menu_def_t menu[] = {
-		{"PLAY", play_mode},
+		{"PLAY", play_controller},
 		{"TEST", test_mode},
 		{"BT", bt_config_mode},
 	};
@@ -868,7 +783,7 @@ int io_init() {
 	esp_err_t ret;
 
 	gpio_config_t conf = {
-		pin_bit_mask: GPIO_SEL_13 | GPIO_SEL_15,
+		pin_bit_mask: 1ull << 13 | 1ull << 15,
 		mode: GPIO_MODE_INPUT,
 		pull_up_en: GPIO_PULLUP_ENABLE,
 		pull_down_en: GPIO_PULLDOWN_DISABLE,
@@ -880,7 +795,7 @@ int io_init() {
 		return 0;
 	}
 
-	conf.pin_bit_mask = GPIO_SEL_14;
+	conf.pin_bit_mask = 1ull << 14;
 	conf.mode = GPIO_MODE_OUTPUT;
 	conf.pull_up_en = GPIO_PULLUP_DISABLE;
 	conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
@@ -910,21 +825,67 @@ int nvs_init() {
 	return 1;
 }
 
+void print_hexdump_line(uint32_t addr, uint8_t* buf) {
+	printf("%08x: ", addr);
+	for (int j = 0; j < 16; j++) {
+		printf("%02x", buf[j]);
+		if (j % 4 == 3) {
+			printf(" ");
+		}
+	}
+	printf("  ");
+	for (int j = 0; j < 16; j++) {
+		if (buf[j] >= 0x20 && buf[j] < 0x7F) {
+			putc(buf[j], stdout);
+		} else {
+			putc('.', stdout);
+		}
+	}
+	printf("\n");
+}
+
+void stream_test() {
+	const char TAG[] = "stream_test";
+	uint8_t* buf = malloc(256);
+	StreamBufferHandle_t h = xStreamBufferCreate(256, 1);
+	uint32_t addr = 0;
+
+	sd_card_mount();
+
+	ESP_LOGI(TAG, "-------- start --------");
+
+	file_reader_open("/sd/rock.gcode");
+	file_reader_subscribe(h);
+	file_reader_start();
+	for (int i = 0; i < 256 / 16; i++) {
+		addr += xStreamBufferReceive(h, buf, 16, portMAX_DELAY);
+		print_hexdump_line(addr, buf);
+	}
+	file_reader_seek(0, SEEK_SET);
+	ESP_LOGI(TAG, "--------- seek --------");
+	for (int i = 0; i < 256 / 16; i++) {
+		addr += xStreamBufferReceive(h, buf, 16, portMAX_DELAY);
+		print_hexdump_line(addr, buf);
+	}
+	file_reader_close();
+
+	ESP_LOGI(TAG, "--------- end ---------");
+}
+
 void app_main(void) {
 	nvs_init();
 	key_init();
 	io_init();
 	audio_init();
-	sdspi_init();
 	gfx_init();
+	sdspi_init();
+	file_init();
 	battery_init();
 	bt_init();
 
 	//xTaskCreate(print_task_list, "task list", 4096, NULL, 1, NULL);
 
-	//play_mp3();
-	//xTaskCreate(play_flac, "flac", 4096, NULL, 5, NULL);
-	//play_mod();
+	stream_test();
 
 	clock_mode();
 }

diff --git a/main/play.c b/main/play.c
line changes: +160/-0
index 0000000..c2cd7fd
--- /dev/null
+++ b/main/play.c
@@ -0,0 +1,160 @@
+#include <sys/time.h>
+#include "freertos/FreeRTOS.h"
+#include "esp_log.h"
+
+#include "bt.h"
+#include "gfx.h"
+#include "keyboard.h"
+#include "play.h"
+
+play_media_type media_type = PLAY_MEDIA_NONE;
+
+void play_set_media_type(play_media_type t) {
+	media_type = t;
+}
+
+play_state get_play_state() {
+	switch (media_type) {
+	case PLAY_MEDIA_BLUETOOTH:
+		return (play_state) bt_get_playback_state()->state;
+	default:
+		ESP_LOGE("PLAY", "Unfinished media type: %d", media_type);
+		return PLAY_STATE_ERROR;
+	}
+}
+
+void play_send_command(play_command c) {
+	switch (media_type) {
+		case PLAY_MEDIA_BLUETOOTH: {
+			esp_avrc_pt_cmd_t button;
+			switch (c) {
+			case PLAY_COMMAND_STOP:
+				button = ESP_AVRC_PT_CMD_STOP;
+				break;
+			case PLAY_COMMAND_PLAY:
+				button = ESP_AVRC_PT_CMD_PLAY;
+				break;
+			case PLAY_COMMAND_PAUSE:
+				button = ESP_AVRC_PT_CMD_PAUSE;
+				break;
+			case PLAY_COMMAND_RTRK:
+				button = ESP_AVRC_PT_CMD_BACKWARD;
+				break;
+			case PLAY_COMMAND_FTRK:
+				button = ESP_AVRC_PT_CMD_FORWARD;
+				break;
+			case PLAY_COMMAND_RREV:
+				button = ESP_AVRC_PT_CMD_REWIND;
+				break;
+			case PLAY_COMMAND_FFWD:
+				button = ESP_AVRC_PT_CMD_FAST_FORWARD;
+				break;
+			default:
+				return;
+			}
+			bt_send_key(button, ESP_AVRC_PT_CMD_STATE_PRESSED);
+			bt_send_key(button, ESP_AVRC_PT_CMD_STATE_RELEASED);
+
+			break;
+		}
+		default:
+			ESP_LOGE("PLAY", "Unfinished media type: %d", media_type);
+	}
+}
+
+void play_controller() {
+	key_event_t ke;
+
+	while (ke = key_get_event(0), !(ke.type == KEY_PRESSED && ke.code == KEY_BAND)) {
+		play_state ps = get_play_state();
+		if (ke.type == KEY_PRESSED) {
+			switch (ke.code) {
+			case KEY_SNOOZE:
+				if (ps == PLAY_STATE_PLAYING) {
+					play_send_command(PLAY_COMMAND_PAUSE);
+				} else {
+					play_send_command(PLAY_COMMAND_PLAY);
+				}
+				break;
+			case KEY_1:
+				play_send_command(PLAY_COMMAND_RTRK);
+				break;
+			case KEY_2:
+				play_send_command(PLAY_COMMAND_RREV);
+				break;
+			case KEY_3:
+				if (ps == PLAY_STATE_PLAYING) {
+					play_send_command(PLAY_COMMAND_PAUSE);
+				} else {
+					play_send_command(PLAY_COMMAND_PLAY);
+				}
+				break;
+			case KEY_4:
+				play_send_command(PLAY_COMMAND_FFWD);
+				break;
+			case KEY_5:
+				play_send_command(PLAY_COMMAND_FTRK);
+				break;
+			default:
+				break;
+			}
+		}
+
+		gfx_data_t* gd = gfx_acquire_data(SCENE_PLAY);
+		gd->play.play_state = ps;
+		gfx_release_data();
+
+		vTaskDelay(50 / portTICK_PERIOD_MS);
+	}
+}
+
+/*
+void play_mode() {
+	TaskHandle_t decoder_task;
+	key_event_t ke;
+	struct timeval tv;
+	float now, t;
+
+	FATFS *fs = sd_card_mount();
+	if (fs == NULL) {
+		u8g2_ClearBuffer(&u8g2);
+		u8g2_DrawStr(&u8g2, 51, 19, "NO SD");
+		u8g2_SendBuffer(&u8g2);
+		vTaskDelay(1000 / portTICK_PERIOD_MS);
+		return;
+	}
+
+	int ret = audio_open();
+	if (!ret) {
+		gfx_message("NO AUDIO");
+		return;
+	}
+
+	SemaphoreHandle_t waiter = xSemaphoreCreateBinary();
+	xTaskCreate(play_flac, "flac", 4096, &waiter, 5, &decoder_task);
+
+	gettimeofday(&tv, NULL);
+	now = (float)tv.tv_sec + (float)tv.tv_usec / 1000000.0;
+
+	while (ke = key_get_event(0), !(ke.type == KEY_PRESSED && ke.code == KEY_BAND)) {
+		if (ke.code == KEY_SNOOZE && ke.type != KEY_NO_EVENT) {
+			if (state == ESP_AVRC_PLAYBACK_PLAYING) {
+				bt_send_key(ESP_AVRC_PT_CMD_PAUSE, ke.type - 1);
+			} else {
+				bt_send_key(ESP_AVRC_PT_CMD_PLAY, ke.type - 1);
+			}
+		}
+
+		vTaskDelay(100 / portTICK_PERIOD_MS);
+	}
+
+	// Kill the decoder
+	xTaskNotifyGive(decoder_task);
+
+	xSemaphoreTake(waiter, portMAX_DELAY);
+
+	sd_card_unmount();
+
+	audio_close();
+}
+*/

diff --git a/main/play.h b/main/play.h
line changes: +31/-0
index 0000000..98413a7
--- /dev/null
+++ b/main/play.h
@@ -0,0 +1,31 @@
+#pragma once
+
+typedef enum {
+	PLAY_MEDIA_NONE,
+	PLAY_MEDIA_BLUETOOTH,
+	PLAY_MEDIA_FLAC,
+	PLAY_MEDIA_MP3,
+} play_media_type;
+
+typedef enum {
+        PLAY_STATE_STOPPED,
+        PLAY_STATE_PLAYING,
+        PLAY_STATE_PAUSED,
+        PLAY_STATE_FFWD,
+        PLAY_STATE_RRWD,
+        PLAY_STATE_ERROR,
+} play_state;
+
+typedef enum {
+	PLAY_COMMAND_STOP,
+	PLAY_COMMAND_PLAY,
+	PLAY_COMMAND_PAUSE,
+	PLAY_COMMAND_FFWD,
+	PLAY_COMMAND_RREV,
+	PLAY_COMMAND_FTRK,
+	PLAY_COMMAND_RTRK,
+} play_command;
+
+void play_set_media_type(play_media_type t);
+void play_send_command(play_command c);
+void play_controller();

diff --git a/main/sprites.h b/main/sprites.h
line changes: +26/-0
index 29be369..39eae1a
--- a/main/sprites.h
+++ b/main/sprites.h
@@ -20,6 +20,32 @@ static uint8_t btn_4_bits[] = {
 #define btn_5_height 7
 static uint8_t btn_5_bits[] = {
   0x1F, 0x11, 0x1D, 0x11, 0x17, 0x11, 0x1F, };
+#define btn_ffwd_4_width 7
+#define btn_ffwd_4_height 7
+static uint8_t btn_ffwd_4_bits[] = {
+  0x0F, 0x15, 0x35, 0x71, 0x37, 0x17, 0x0F, };
+#define btn_ftrk_5_width 9
+#define btn_ftrk_5_height 7
+static uint8_t btn_ftrk_5_bits[] = {
+  0x2F, 0x00, 0x51, 0x00, 0xBD, 0x00, 0x71, 0x01, 0xB7, 0x00, 0x51, 0x00, 
+  0x2F, 0x00, };
+#define btn_pause_3_width 7
+#define btn_pause_3_height 7
+static uint8_t btn_pause_3_bits[] = {
+  0x36, 0x22, 0x2E, 0x22, 0x2E, 0x22, 0x36, };
+#define btn_play_3_width 7
+#define btn_play_3_height 7
+static uint8_t btn_play_3_bits[] = {
+  0x0F, 0x03, 0x2F, 0x63, 0x2F, 0x03, 0x0F, };
+#define btn_rrev_2_width 7
+#define btn_rrev_2_height 7
+static uint8_t btn_rrev_2_bits[] = {
+  0x78, 0x44, 0x5E, 0x47, 0x76, 0x44, 0x78, };
+#define btn_rtrk_1_width 9
+#define btn_rtrk_1_height 7
+static uint8_t btn_rtrk_1_bits[] = {
+  0xE8, 0x01, 0xB4, 0x01, 0xBA, 0x01, 0xBD, 0x01, 0xBA, 0x01, 0xB4, 0x01, 
+  0xE8, 0x01, };
 static uint8_t* btn_list[] = {
 	btn_1_bits,
 	btn_2_bits,