소스 검색

LMS/BT/AirPlay coexistence + improve display

philippe44 5 년 전
부모
커밋
9d2aa978d5

+ 91 - 62
components/driver_bt/bt_app_sink.c

@@ -64,18 +64,29 @@ static void bt_av_hdl_avrc_ct_evt(uint16_t event, void *p_param);
 static void bt_av_hdl_avrc_tg_evt(uint16_t event, void *p_param);
 static void volume_set_by_local_host(uint8_t volume);
 
-static esp_a2d_audio_state_t s_audio_state = ESP_A2D_AUDIO_STATE_STOPPED;
 static const char *s_a2d_conn_state_str[] = {"Disconnected", "Connecting", "Connected", "Disconnecting"};
 static const char *s_a2d_audio_state_str[] = {"Suspended", "Stopped", "Started"};
 static esp_avrc_rn_evt_cap_mask_t s_avrc_peer_rn_cap;
+
 static _lock_t s_volume_lock;
 static uint8_t s_volume = 0;
 static bool s_volume_notify;
-static esp_avrc_playback_stat_t s_play_status = ESP_AVRC_PLAYBACK_STOPPED;
-static uint8_t s_remote_bda[6];
+static bool s_playing = false; 
+static enum { AUDIO_IDLE, AUDIO_CONNECTED, AUDIO_ACTIVATED } s_audio = AUDIO_IDLE;
+
+static int s_sample_rate;
 static int tl;
 static bt_cmd_vcb_t cmd_handler_chain;
-static char *s_artist, *s_album, *s_title;
+
+#define METADATA_LEN 128
+
+static EXT_RAM_ATTR struct {
+	char artist[METADATA_LEN + 1];
+	char album[METADATA_LEN + 1];
+	char title[METADATA_LEN + 1];
+	int duration;
+	bool updated;
+} s_metadata;	
 
 static void bt_volume_up(void) {
 	// volume UP/DOWN buttons are not supported by iPhone/Android
@@ -91,9 +102,8 @@ static void bt_volume_down(void) {
 }
 
 static void bt_toggle(void) {
-	if (s_play_status != ESP_AVRC_PLAYBACK_PLAYING) esp_avrc_ct_send_passthrough_cmd(tl++, ESP_AVRC_PT_CMD_PLAY, ESP_AVRC_PT_CMD_STATE_PRESSED);
-	else esp_avrc_ct_send_passthrough_cmd(tl++ & 0x0f, ESP_AVRC_PT_CMD_STOP, ESP_AVRC_PT_CMD_STATE_PRESSED);
-	//s_audio_state = ESP_A2D_AUDIO_STATE_STOPPED;
+	if (s_playing) esp_avrc_ct_send_passthrough_cmd(tl++ & 0x0f, ESP_AVRC_PT_CMD_STOP, ESP_AVRC_PT_CMD_STATE_PRESSED);
+	else esp_avrc_ct_send_passthrough_cmd(tl++, ESP_AVRC_PT_CMD_PLAY, ESP_AVRC_PT_CMD_STATE_PRESSED);
 }
 
 static void bt_play(void) {
@@ -124,65 +134,68 @@ const static actrls_t controls = {
 	bt_prev, bt_next,	// prev, next
 };
 
-/* taking/giving audio system's control */
-void bt_master(bool on) {
-	if (on) actrls_set(controls, NULL);
-	else actrls_unset();
-}
-
 /* disconnection */
 void bt_disconnect(void) {
 	displayer_control(DISPLAYER_SHUTDOWN);
-	esp_avrc_ct_send_passthrough_cmd(tl++ & 0x0f, ESP_AVRC_PT_CMD_STOP, ESP_AVRC_PT_CMD_STATE_PRESSED);
-	esp_a2d_sink_disconnect(s_remote_bda);
+	if (s_playing) esp_avrc_ct_send_passthrough_cmd(tl++ & 0x0f, ESP_AVRC_PT_CMD_STOP, ESP_AVRC_PT_CMD_STATE_PRESSED);
 	actrls_unset();
 	ESP_LOGI(BT_AV_TAG, "forced disconnection");
 }
 
+/* update metadata if any */
+void update_metadata(bool force) {
+	if ((s_metadata.updated || force) && s_audio == AUDIO_ACTIVATED) {
+		(*bt_app_a2d_cmd_cb)(BT_SINK_PROGRESS, -1, s_metadata.duration);
+		(*bt_app_a2d_cmd_cb)(BT_SINK_METADATA, s_metadata.artist, s_metadata.album, s_metadata.title);
+		s_metadata.updated = false;
+	} else s_metadata.updated = force;
+}	
+
 /* command handler */
 static bool cmd_handler(bt_sink_cmd_t cmd, ...) {
-	bool chain = true, res = true;
 	va_list args;	
 	
 	va_start(args, cmd);
 	
+	// handle audio event and stop if forbidden
+	if (!cmd_handler_chain(cmd, args)) {
+		va_end(args);
+		return false;
+	}
+
+	// now handle events for display
 	switch(cmd) {
-	case BT_SINK_CONNECTED:
+	case BT_SINK_AUDIO_STARTED:
 		displayer_control(DISPLAYER_ACTIVATE, "BLUETOOTH");
 		break;
+	case BT_SINK_AUDIO_STOPPED:
+		displayer_control(DISPLAYER_SUSPEND);
+		break;		
 	case BT_SINK_PLAY:
-		displayer_control(DISPLAYER_TIMER_RESUME);
+		displayer_control(DISPLAYER_TIMER_RUN);
 		break;		
+	case BT_SINK_STOP:		
+		// not sure of difference between pause and stop for displayer 
 	case BT_SINK_PAUSE:
 		displayer_control(DISPLAYER_TIMER_PAUSE);
 		break;		
-	case BT_SINK_STOP:
-		displayer_control(DISPLAYER_DISABLE);
-		break;
-	case BT_SINK_DISCONNECTED:
-		displayer_control(DISPLAYER_DISABLE);
-		break;
 	case BT_SINK_METADATA: {
 		char *artist = va_arg(args, char*), *album = va_arg(args, char*), *title = va_arg(args, char*);
 		displayer_metadata(artist, album, title);
-		chain = false;
 		break;
 	}	
 	case BT_SINK_PROGRESS: {
 		int elapsed = va_arg(args, int), duration = va_arg(args, int);
 		displayer_timer(DISPLAYER_ELAPSED, elapsed, duration);
-		chain = false;
 		break;
 	}	
 	default: 
 		break;
 	}
 	
-	if (chain) res = cmd_handler_chain(cmd, args);
-	
 	va_end(args);
 	
-	return res;
+	return true;
 }
 
 /* callback for A2DP sink */
@@ -260,26 +273,40 @@ static void bt_av_hdl_a2d_evt(uint16_t event, void *p_param)
         if (a2d->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
             esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
 			(*bt_app_a2d_cmd_cb)(BT_SINK_DISCONNECTED);
-			actrls_unset();
         } else if (a2d->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED){
-			memcpy(s_remote_bda, bda, 6);
             esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
-			if (!(*bt_app_a2d_cmd_cb)(BT_SINK_CONNECTED)){
-				esp_avrc_ct_send_passthrough_cmd(tl++ & 0x0f, ESP_AVRC_PT_CMD_STOP, ESP_AVRC_PT_CMD_STATE_PRESSED);
-				esp_a2d_sink_disconnect(s_remote_bda);
-			}
+			(*bt_app_a2d_cmd_cb)(BT_SINK_CONNECTED);
         }
         break;
     }
     case ESP_A2D_AUDIO_STATE_EVT: {
         a2d = (esp_a2d_cb_param_t *)(p_param);
         ESP_LOGI(BT_AV_TAG, "A2DP audio state: %s", s_a2d_audio_state_str[a2d->audio_stat.state]);
-        s_audio_state = a2d->audio_stat.state;
+
         if (ESP_A2D_AUDIO_STATE_STARTED == a2d->audio_stat.state) {
-			(*bt_app_a2d_cmd_cb)(BT_SINK_PLAY);
+			s_audio = AUDIO_CONNECTED;
+			
+			// verify that we can take control
+			if ((*bt_app_a2d_cmd_cb)(BT_SINK_AUDIO_STARTED, s_sample_rate)) {
+				// resynchronize events as¨PLAY might be sent before STARTED ...
+				s_audio = AUDIO_ACTIVATED;
+				
+				// send PLAY there, in case it was sent before AUDIO_STATE
+				if (s_playing) (*bt_app_a2d_cmd_cb)(BT_SINK_PLAY);
+				
+				// force metadata update
+				update_metadata(true);
+				
+				actrls_set(controls, NULL);
+			} else if (s_playing) {
+				// if decoder is busy but BT is playing, stop it (would be better to not ACK this command, but don't know how)
+				esp_avrc_ct_send_passthrough_cmd(tl++ & 0x0f, ESP_AVRC_PT_CMD_STOP, ESP_AVRC_PT_CMD_STATE_PRESSED);	
+			}	
 		} else if (ESP_A2D_AUDIO_STATE_STOPPED == a2d->audio_stat.state ||
 				   ESP_A2D_AUDIO_STATE_REMOTE_SUSPEND == a2d->audio_stat.state) {
-			(*bt_app_a2d_cmd_cb)(BT_SINK_STOP);
+			(*bt_app_a2d_cmd_cb)(BT_SINK_AUDIO_STOPPED);
+			s_audio = AUDIO_IDLE;
+			actrls_unset();
 		}	
         break;
     }
@@ -288,23 +315,23 @@ static void bt_av_hdl_a2d_evt(uint16_t event, void *p_param)
         ESP_LOGI(BT_AV_TAG, "A2DP audio stream configuration, codec type %d", a2d->audio_cfg.mcc.type);
         // for now only SBC stream is supported
         if (a2d->audio_cfg.mcc.type == ESP_A2D_MCT_SBC) {
-            int sample_rate = 16000;
+            s_sample_rate = 16000;
             char oct0 = a2d->audio_cfg.mcc.cie.sbc[0];
             if (oct0 & (0x01 << 6)) {
-                sample_rate = 32000;
+                s_sample_rate = 32000;
             } else if (oct0 & (0x01 << 5)) {
-                sample_rate = 44100;
+                s_sample_rate = 44100;
             } else if (oct0 & (0x01 << 4)) {
-                sample_rate = 48000;
+                s_sample_rate = 48000;
             }
-			(*bt_app_a2d_cmd_cb)(BT_SINK_RATE, sample_rate);
+			(*bt_app_a2d_cmd_cb)(BT_SINK_RATE, s_sample_rate);
             
             ESP_LOGI(BT_AV_TAG, "Configure audio player %x-%x-%x-%x",
                      a2d->audio_cfg.mcc.cie.sbc[0],
                      a2d->audio_cfg.mcc.cie.sbc[1],
                      a2d->audio_cfg.mcc.cie.sbc[2],
                      a2d->audio_cfg.mcc.cie.sbc[3]);
-            ESP_LOGI(BT_AV_TAG, "Audio player configured, sample rate=%d", sample_rate);
+            ESP_LOGI(BT_AV_TAG, "Audio player configured, sample rate=%d", s_sample_rate);
         }
         break;
     }
@@ -347,12 +374,26 @@ void bt_av_notify_evt_handler(uint8_t event_id, esp_avrc_rn_param_t *event_param
 {
     switch (event_id) {
     case ESP_AVRC_RN_TRACK_CHANGE:
+		ESP_LOGI(BT_AV_TAG, "Track changed");
         bt_av_new_track();
 		(*bt_app_a2d_cmd_cb)(BT_SINK_PROGRESS, 0, 0);
         break;
     case ESP_AVRC_RN_PLAY_STATUS_CHANGE:
         ESP_LOGI(BT_AV_TAG, "Playback status changed: 0x%x", event_parameter->playback);
-		s_play_status = event_parameter->playback;
+		// re-synchronize events
+		s_playing = (event_parameter->playback == ESP_AVRC_PLAYBACK_PLAYING);
+		if (event_parameter->playback == ESP_AVRC_PLAYBACK_PLAYING && s_audio != AUDIO_IDLE) {
+			// if decoder is busy then stop (would be better to not ACK this command, but don't know how)
+			if (s_audio == AUDIO_CONNECTED || !(*bt_app_a2d_cmd_cb)(BT_SINK_PLAY)) {
+				esp_avrc_ct_send_passthrough_cmd(tl++ & 0x0f, ESP_AVRC_PT_CMD_STOP, ESP_AVRC_PT_CMD_STATE_PRESSED);
+			} else {
+				update_metadata(false);
+			}	
+		} else if (event_parameter->playback == ESP_AVRC_PLAYBACK_PAUSED) (*bt_app_a2d_cmd_cb)(BT_SINK_PAUSE);
+		else if (event_parameter->playback == ESP_AVRC_PLAYBACK_STOPPED) {
+			(*bt_app_a2d_cmd_cb)(BT_SINK_PROGRESS, 0, -1);			
+			(*bt_app_a2d_cmd_cb)(BT_SINK_STOP);
+		}	
         bt_av_playback_changed();
         break;
     case ESP_AVRC_RN_PLAY_POS_CHANGED:
@@ -387,25 +428,13 @@ static void bt_av_hdl_avrc_ct_evt(uint16_t event, void *p_param)
         break;
     }
     case ESP_AVRC_CT_METADATA_RSP_EVT: {
-		char **p = NULL;
         ESP_LOGI(BT_RC_CT_TAG, "AVRC metadata rsp: attribute id 0x%x, %s", rc->meta_rsp.attr_id, rc->meta_rsp.attr_text);
 		
-		if (rc->meta_rsp.attr_id == ESP_AVRC_MD_ATTR_TITLE) p = &s_title;
-		else if (rc->meta_rsp.attr_id == ESP_AVRC_MD_ATTR_ARTIST) p = &s_artist;
-		else if (rc->meta_rsp.attr_id == ESP_AVRC_MD_ATTR_ALBUM) p = &s_album;
-		else if (rc->meta_rsp.attr_id == ESP_AVRC_MD_ATTR_PLAYING_TIME) (*bt_app_a2d_cmd_cb)(BT_SINK_PROGRESS, -1, atoi((char*) rc->meta_rsp.attr_text));
-				
-		// not very pretty, but this is bluetooth anyway
-		if (p) {
-			if (*p) free(*p);
-			*p = strdup((char*) rc->meta_rsp.attr_text);
-		
-			if (s_artist && s_album && s_title) {
-				(*bt_app_a2d_cmd_cb)(BT_SINK_METADATA, s_artist, s_album, s_title);
-				free(s_artist); free(s_album); free(s_title);
-				s_artist = s_album = s_title = NULL;
-			}
-		}	
+		if (rc->meta_rsp.attr_id == ESP_AVRC_MD_ATTR_PLAYING_TIME) s_metadata.duration = atoi((char*) rc->meta_rsp.attr_text);
+		else if (rc->meta_rsp.attr_id == ESP_AVRC_MD_ATTR_TITLE) strncpy(s_metadata.title, (char*) rc->meta_rsp.attr_text, METADATA_LEN);
+		else if (rc->meta_rsp.attr_id == ESP_AVRC_MD_ATTR_ARTIST) strncpy(s_metadata.artist, (char*) rc->meta_rsp.attr_text, METADATA_LEN);
+		else if (rc->meta_rsp.attr_id == ESP_AVRC_MD_ATTR_ALBUM) strncpy(s_metadata.album, (char*) rc->meta_rsp.attr_text, METADATA_LEN);
+		update_metadata(true);
 		
         free(rc->meta_rsp.attr_text);
         break;

+ 1 - 6
components/driver_bt/bt_app_sink.h

@@ -11,7 +11,7 @@
 
 #include <stdint.h>
 
-typedef enum { 	BT_SINK_CONNECTED, BT_SINK_DISCONNECTED, BT_SINK_PLAY, BT_SINK_STOP, BT_SINK_PAUSE, 
+typedef enum { 	BT_SINK_CONNECTED, BT_SINK_DISCONNECTED, BT_SINK_AUDIO_STARTED, BT_SINK_AUDIO_STOPPED, BT_SINK_PLAY, BT_SINK_STOP, BT_SINK_PAUSE, 
 				BT_SINK_RATE, BT_SINK_VOLUME, BT_SINK_METADATA, BT_SINK_PROGRESS } bt_sink_cmd_t;
 				
 typedef bool (*bt_cmd_vcb_t)(bt_sink_cmd_t cmd, va_list args);
@@ -27,11 +27,6 @@ void bt_sink_init(bt_cmd_vcb_t cmd_cb, bt_data_cb_t data_cb);
  */
 void bt_sink_deinit(void);
 
-/**
- * @brief     * @brief     do what's necessary when becoming in charge
- */
-void bt_master(bool on);
-
 /**
  * @brief     force disconnection
  */

+ 6 - 2
components/raop/raop.c

@@ -661,8 +661,12 @@ static bool handle_rtsp(raop_ctx_t *ctx, int sock)
 	kd_add(resp, "Audio-Jack-Status", "connected; type=analog");
 	kd_add(resp, "CSeq", kd_lookup(headers, "CSeq"));
 
-	if (success) buf = http_send(sock, "RTSP/1.0 200 OK", resp);
-	else buf = http_send(sock, "RTSP/1.0 500 ERROR", NULL);
+	if (success) {
+		buf = http_send(sock, "RTSP/1.0 200 OK", resp);
+	} else {
+		buf = http_send(sock, "RTSP/1.0 503 ERROR", NULL);
+		closesocket(sock);
+	}	
 
 	if (strcmp(method, "OPTIONS")) {
 		LOG_INFO("[%p]: responding:\n%s", ctx, buf ? buf : "<void>");

+ 12 - 16
components/raop/raop_sink.c

@@ -83,53 +83,49 @@ const static actrls_t controls = {
  * Command handler
  */
 static bool cmd_handler(raop_event_t event, ...) {
-	bool chain = true, res = true;
 	va_list args;	
 	
 	va_start(args, event);
 	
+	// handle audio event and stop if forbidden
+	if (!cmd_handler_chain(event, args)) {
+		va_end(args);
+		return false;
+	}
+
+	// now handle events for display
 	switch(event) {
 	case RAOP_SETUP:
+		actrls_set(controls, NULL);
 		displayer_control(DISPLAYER_ACTIVATE, "AIRPLAY");
 		break;
 	case RAOP_PLAY:
-		displayer_control(DISPLAYER_TIMER_RESUME);
+		displayer_control(DISPLAYER_TIMER_RUN);
 		break;		
 	case RAOP_FLUSH:
 		displayer_control(DISPLAYER_TIMER_PAUSE);
 		break;		
 	case RAOP_STOP:
-		displayer_control(DISPLAYER_DISABLE);
+		actrls_unset();
+		displayer_control(DISPLAYER_SUSPEND);
 		break;
 	case RAOP_METADATA: {
 		char *artist = va_arg(args, char*), *album = va_arg(args, char*), *title = va_arg(args, char*);
 		displayer_metadata(artist, album, title);
-		chain = false;
 		break;
 	}	
 	case RAOP_PROGRESS: {
 		int elapsed = va_arg(args, int), duration = va_arg(args, int);
 		displayer_timer(DISPLAYER_ELAPSED, elapsed, duration);
-		chain = false;
 		break;
 	}	
 	default: 
 		break;
 	}
 	
-	if (chain) res = cmd_handler_chain(event, args);
-	
 	va_end(args);
 	
-	return res;
-}
-
-/****************************************************************************************
- * Airplay taking/giving audio system's control 
- */
-void raop_master(bool on) {
-	if (on) actrls_set(controls, NULL);
-	else actrls_unset();
+	return true;
 }
 
 /****************************************************************************************

+ 0 - 5
components/raop/raop_sink.h

@@ -32,11 +32,6 @@ void raop_sink_init(raop_cmd_vcb_t cmd_cb, raop_data_cb_t data_cb);
  */
 void raop_sink_deinit(void);
 
-/**
- * @brief     do what's necessary when becoming in charge
- */
-void raop_master(bool on);
-
 /**
  * @brief     force disconnection
  */

+ 40 - 37
components/services/display.c

@@ -34,23 +34,23 @@ struct display_s *display;
 static const char *TAG = "display";
 
 #define min(a,b) (((a) < (b)) ? (a) : (b))
+#define max(a,b) (((a) > (b)) ? (a) : (b))
 
 #define DISPLAYER_STACK_SIZE 	2048
 #define SCROLLABLE_SIZE			384
 #define HEADER_SIZE				64
 #define	DEFAULT_SLEEP			3600
 
-
 static EXT_RAM_ATTR struct {
 	TaskHandle_t task;
 	SemaphoreHandle_t mutex;
 	int pause, speed, by;
-	enum { DISPLAYER_DISABLED, DISPLAYER_AUTO_DISABLE, DISPLAYER_ACTIVE } state;
+	enum { DISPLAYER_DOWN, DISPLAYER_IDLE, DISPLAYER_ACTIVE } state;
 	char header[HEADER_SIZE + 1];
 	char string[SCROLLABLE_SIZE + 1];
 	int offset, boundary;
 	char *metadata_config;
-	bool timer;
+	bool timer, refresh;
 	uint32_t elapsed, duration;
 	TickType_t tick;
 } displayer;
@@ -109,56 +109,58 @@ void display_init(char *welcome) {
  */
 static void displayer_task(void *args) {
 	int scroll_sleep = 0, timer_sleep;
-	
+		
 	while (1) {
 		// suspend ourselves if nothing to do
-		if (displayer.state == DISPLAYER_DISABLED) {
+		if (displayer.state < DISPLAYER_ACTIVE) {
+			if (displayer.state == DISPLAYER_IDLE) display->line(2, 0, DISPLAY_CLEAR | DISPLAY_UPDATE, displayer.string);
 			vTaskSuspend(NULL);
+			scroll_sleep = 0;
 			display->clear();
 			display->line(1, DISPLAY_LEFT, DISPLAY_UPDATE, displayer.header);
-			scroll_sleep = 0;
-		}	
+		} else if (displayer.refresh) {
+			// little trick when switching master while in IDLE and missing it
+			display->line(1, DISPLAY_LEFT, DISPLAY_CLEAR | DISPLAY_UPDATE, displayer.header);	
+			displayer.refresh = false;			
+		}
 		
 		// we have been waken up before our requested time
 		if (scroll_sleep <= 10) {
 			// something to scroll (or we'll wake-up every pause ms ... no big deal)
 			if (*displayer.string && displayer.state == DISPLAYER_ACTIVE) {
 				display->line(2, -displayer.offset, DISPLAY_CLEAR | DISPLAY_UPDATE, displayer.string);
-			
 				xSemaphoreTake(displayer.mutex, portMAX_DELAY);
 				scroll_sleep = displayer.offset ? displayer.speed : displayer.pause;
 				displayer.offset = displayer.offset >= displayer.boundary ? 0 : (displayer.offset + min(displayer.by, displayer.boundary - displayer.offset));			
 				xSemaphoreGive(displayer.mutex);
-			} else if (displayer.state == DISPLAYER_AUTO_DISABLE) {
-				display->line(2, DISPLAY_LEFT, DISPLAY_CLEAR | DISPLAY_UPDATE, displayer.string);
-				xSemaphoreTake(displayer.mutex, portMAX_DELAY);
-				displayer.state = DISPLAYER_DISABLED;
-				xSemaphoreGive(displayer.mutex);
-			} else scroll_sleep = DEFAULT_SLEEP;
+			} else {
+				scroll_sleep = DEFAULT_SLEEP;
+			}	
 		}	
 		
-		// handler timer
+		// handler elapsed track time
 		if (displayer.timer && displayer.state == DISPLAYER_ACTIVE) {
 			char counter[12];
 			TickType_t tick = xTaskGetTickCount();
 			uint32_t elapsed = (tick - displayer.tick) * portTICK_PERIOD_MS;
 			
 			if (elapsed >= 1000) {
+				xSemaphoreTake(displayer.mutex, portMAX_DELAY);
 				displayer.tick = tick;
 				displayer.elapsed += elapsed / 1000;
+				xSemaphoreGive(displayer.mutex);				
 				if (displayer.elapsed < 3600) sprintf(counter, "%2u:%02u", displayer.elapsed / 60, displayer.elapsed % 60);
 				else sprintf(counter, "%2u:%2u:%02u", displayer.elapsed / 3600, (displayer.elapsed % 3600) / 60, displayer.elapsed % 60);
 				display->line(1, DISPLAY_RIGHT, (DISPLAY_CLEAR | DISPLAY_ONLY_EOL) | DISPLAY_UPDATE, counter);
 				timer_sleep = 1000;
-			} else timer_sleep = 1000 - elapsed;	
+			} else timer_sleep = max(1000 - elapsed, 0);	
 		} else timer_sleep = DEFAULT_SLEEP;
 		
-		// don't bother sleeping if we are disactivated
-		if (displayer.state == DISPLAYER_ACTIVE) {
-			int sleep = min(scroll_sleep, timer_sleep);
-			scroll_sleep -= sleep;
-			vTaskDelay( sleep / portTICK_PERIOD_MS);
-		}	
+		// then sleep the min amount of time
+		int sleep = min(scroll_sleep, timer_sleep);
+		ESP_LOGD(TAG, "timers s:%d t:%d", scroll_sleep, timer_sleep);
+		scroll_sleep -= sleep;
+		vTaskDelay(sleep / portTICK_PERIOD_MS);
 	}
 }	
 
@@ -203,9 +205,9 @@ void displayer_metadata(char *artist, char *album, char *title) {
 			}	
 
 			// then copy token's content
-			if (strcasestr(q, "artist") && artist) strncat(string, p = artist, space);
-			else if (strcasestr(q, "album") && album) strncat(string, p = album, space);
-			else if (strcasestr(q, "title") && title) strncat(string, p = title, space);
+			if (!strncasecmp(q + 1, "artist", 6) && artist) strncat(string, p = artist, space);
+			else if (!strncasecmp(q + 1, "album", 5) && album) strncat(string, p = album, space);
+			else if (!strncasecmp(q + 1, "title", 5) && title) strncat(string, p = title, space);
 			space = len - strlen(string);
 				
 			// flag to skip the data following an empty field
@@ -224,6 +226,7 @@ void displayer_metadata(char *artist, char *album, char *title) {
 	
 	displayer.offset = 0;	
 	utf8_decode(displayer.string);
+	ESP_LOGI(TAG, "playing %s", displayer.string);
 	displayer.boundary = display->stretch(2, displayer.string, SCROLLABLE_SIZE);
 		
 	xSemaphoreGive(displayer.mutex);
@@ -252,9 +255,8 @@ void displayer_timer(enum displayer_time_e mode, int elapsed, int duration) {
 	xSemaphoreTake(displayer.mutex, portMAX_DELAY);
 
 	if (elapsed >= 0) displayer.elapsed = elapsed;	
-	if (duration >= 0) displayer.duration = duration;	
-	displayer.timer = true;
-	displayer.tick = xTaskGetTickCount();
+	if (duration >= 0) displayer.duration = duration;
+	if (displayer.timer) displayer.tick = xTaskGetTickCount();
 		
 	xSemaphoreGive(displayer.mutex);
 }	
@@ -267,10 +269,7 @@ void displayer_control(enum displayer_cmd_e cmd, ...) {
 	
 	va_start(args, cmd);
 	xSemaphoreTake(displayer.mutex, portMAX_DELAY);
-	
-	displayer.offset = 0;	
-	displayer.boundary = 0;
-	
+		
 	switch(cmd) {
 	case DISPLAYER_ACTIVATE: {	
 		char *header = va_arg(args, char*);
@@ -278,18 +277,22 @@ void displayer_control(enum displayer_cmd_e cmd, ...) {
 		displayer.header[HEADER_SIZE] = '\0';
 		displayer.state = DISPLAYER_ACTIVE;
 		displayer.timer = false;
+		displayer.refresh = true;
 		displayer.string[0] = '\0';
+		displayer.elapsed = displayer.duration = 0;
+		displayer.offset = displayer.boundary = 0;
 		vTaskResume(displayer.task);
 		break;
 	}	
-	case DISPLAYER_DISABLE:		
-		displayer.state = DISPLAYER_AUTO_DISABLE;
+	case DISPLAYER_SUSPEND:		
+		// task will display the line 2 from beginning and suspend
+		displayer.state = DISPLAYER_IDLE;
 		break;		
 	case DISPLAYER_SHUTDOWN:
-		displayer.state = DISPLAYER_DISABLED;
-		vTaskSuspend(displayer.task);
+		// let the task self-suspend (we might be doing i2c_write)
+		displayer.state = DISPLAYER_DOWN;
 		break;
-	case DISPLAYER_TIMER_RESUME:
+	case DISPLAYER_TIMER_RUN:
 		if (!displayer.timer) {
 			displayer.timer = true;		
 			displayer.tick = xTaskGetTickCount();		

+ 13 - 1
components/services/display.h

@@ -18,6 +18,18 @@
 
 #pragma once
 
+/* 
+ The displayer is not thread-safe and the caller must ensure use its own 
+ mutexes if it wants something better. Especially, text() line() and draw()
+ are not protected against each other.
+ In text mode (text/line) when using DISPLAY_SUSPEND, the displayer will 
+ refreshed line 2 one last time before suspending itself. As a result if it
+ is in a long sleep (scrolling pause), the refresh will happen after wakeup. 
+ So it can conflict with other display direct writes that have been made during
+ sleep. Note that if DISPLAY_SHUTDOWN has been called meanwhile, it (almost) 
+ never happens
+*/ 
+  
 #define DISPLAY_CLEAR 		0x01
 #define	DISPLAY_ONLY_EOL	0x02
 #define DISPLAY_UPDATE		0x04
@@ -33,7 +45,7 @@ enum display_font_e { DISPLAY_FONT_DEFAULT,
 					  DISPLAY_FONT_LINE_1, DISPLAY_FONT_LINE_2, DISPLAY_FONT_SEGMENT, 
 					  DISPLAY_FONT_TINY, DISPLAY_FONT_SMALL,  DISPLAY_FONT_MEDIUM, DISPLAY_FONT_LARGE, DISPLAY_FONT_HUGE };
 					  
-enum displayer_cmd_e 	{ DISPLAYER_SHUTDOWN, DISPLAYER_ACTIVATE, DISPLAYER_DISABLE, DISPLAYER_TIMER_PAUSE, DISPLAYER_TIMER_RESUME };
+enum displayer_cmd_e 	{ DISPLAYER_SHUTDOWN, DISPLAYER_ACTIVATE, DISPLAYER_SUSPEND, DISPLAYER_TIMER_PAUSE, DISPLAYER_TIMER_RUN };
 enum displayer_time_e 	{ DISPLAYER_ELAPSED, DISPLAYER_REMAINING };
 
 // don't change anything there w/o changing all drivers init code

+ 7 - 7
components/squeezelite/decode_external.c

@@ -113,19 +113,19 @@ static bool bt_sink_cmd_handler(bt_sink_cmd_t cmd, va_list args)
 	if (cmd != BT_SINK_VOLUME) LOCK_O;
 		
 	switch(cmd) {
-	case BT_SINK_CONNECTED:
+	case BT_SINK_AUDIO_STARTED:
+		output.next_sample_rate = output.current_sample_rate = va_arg(args, u32_t);
 		output.external = DECODE_BT;
 		output.state = OUTPUT_STOPPED;
 		output.frames_played = 0;
 		_buf_flush(outputbuf);
 		if (decode.state != DECODE_STOPPED) decode.state = DECODE_ERROR;
-		bt_master(true);
 		LOG_INFO("BT sink started");
 		break;
-	case BT_SINK_DISCONNECTED:	
+	case BT_SINK_AUDIO_STOPPED:	
+		// do we still need that?
 		if (output.external == DECODE_BT) {
 			output.state = OUTPUT_OFF;
-			bt_master(false);
 			LOG_INFO("BT sink stopped");
 		}	
 		break;
@@ -135,10 +135,12 @@ static bool bt_sink_cmd_handler(bt_sink_cmd_t cmd, va_list args)
 		break;
 	case BT_SINK_STOP:		
 		_buf_flush(outputbuf);
-	case BT_SINK_PAUSE:		
 		output.state = OUTPUT_STOPPED;
 		LOG_INFO("BT sink stopped");
 		break;
+	case BT_SINK_PAUSE:		
+		LOG_INFO("BT sink paused, just silence");
+		break;
 	case BT_SINK_RATE:
 		output.next_sample_rate = output.current_sample_rate = va_arg(args, u32_t);
 		LOG_INFO("Setting BT sample rate %u", output.next_sample_rate);
@@ -239,7 +241,6 @@ static bool raop_sink_cmd_handler(raop_event_t event, va_list args)
 			output.external = DECODE_RAOP;
 			output.state = OUTPUT_STOPPED;
 			if (decode.state != DECODE_STOPPED) decode.state = DECODE_ERROR;
-			raop_master(true);
 			LOG_INFO("resizing buffer %u", outputbuf->size);
 			break;
 		case RAOP_STREAM:
@@ -256,7 +257,6 @@ static bool raop_sink_cmd_handler(raop_event_t event, va_list args)
 			output.state = OUTPUT_OFF;
 			output.frames_played = 0;
 			raop_state = event;
-			raop_master(false);
 			break;
 		case RAOP_FLUSH:
 			LOG_INFO("Flush", NULL);