LMS/BT/AirPlay coexistence + improve display

This commit is contained in:
philippe44
2020-01-28 22:54:34 -08:00
parent f9c30733e0
commit 9d2aa978d5
8 changed files with 171 additions and 137 deletions

View File

@@ -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 bt_av_hdl_avrc_tg_evt(uint16_t event, void *p_param);
static void volume_set_by_local_host(uint8_t volume); 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_conn_state_str[] = {"Disconnected", "Connecting", "Connected", "Disconnecting"};
static const char *s_a2d_audio_state_str[] = {"Suspended", "Stopped", "Started"}; 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 esp_avrc_rn_evt_cap_mask_t s_avrc_peer_rn_cap;
static _lock_t s_volume_lock; static _lock_t s_volume_lock;
static uint8_t s_volume = 0; static uint8_t s_volume = 0;
static bool s_volume_notify; static bool s_volume_notify;
static esp_avrc_playback_stat_t s_play_status = ESP_AVRC_PLAYBACK_STOPPED; static bool s_playing = false;
static uint8_t s_remote_bda[6]; static enum { AUDIO_IDLE, AUDIO_CONNECTED, AUDIO_ACTIVATED } s_audio = AUDIO_IDLE;
static int s_sample_rate;
static int tl; static int tl;
static bt_cmd_vcb_t cmd_handler_chain; 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) { static void bt_volume_up(void) {
// volume UP/DOWN buttons are not supported by iPhone/Android // 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) { 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); 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++ & 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);
//s_audio_state = ESP_A2D_AUDIO_STATE_STOPPED;
} }
static void bt_play(void) { static void bt_play(void) {
@@ -124,65 +134,68 @@ const static actrls_t controls = {
bt_prev, bt_next, // prev, next 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 */ /* disconnection */
void bt_disconnect(void) { void bt_disconnect(void) {
displayer_control(DISPLAYER_SHUTDOWN); displayer_control(DISPLAYER_SHUTDOWN);
esp_avrc_ct_send_passthrough_cmd(tl++ & 0x0f, ESP_AVRC_PT_CMD_STOP, ESP_AVRC_PT_CMD_STATE_PRESSED); if (s_playing) 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);
actrls_unset(); actrls_unset();
ESP_LOGI(BT_AV_TAG, "forced disconnection"); 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 */ /* command handler */
static bool cmd_handler(bt_sink_cmd_t cmd, ...) { static bool cmd_handler(bt_sink_cmd_t cmd, ...) {
bool chain = true, res = true;
va_list args; va_list args;
va_start(args, cmd); 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) { switch(cmd) {
case BT_SINK_CONNECTED: case BT_SINK_AUDIO_STARTED:
displayer_control(DISPLAYER_ACTIVATE, "BLUETOOTH"); displayer_control(DISPLAYER_ACTIVATE, "BLUETOOTH");
break; break;
case BT_SINK_PLAY: case BT_SINK_AUDIO_STOPPED:
displayer_control(DISPLAYER_TIMER_RESUME); displayer_control(DISPLAYER_SUSPEND);
break; break;
case BT_SINK_PAUSE: case BT_SINK_PLAY:
displayer_control(DISPLAYER_TIMER_PAUSE); displayer_control(DISPLAYER_TIMER_RUN);
break; break;
case BT_SINK_STOP: case BT_SINK_STOP:
displayer_control(DISPLAYER_DISABLE); // not sure of difference between pause and stop for displayer
break; case BT_SINK_PAUSE:
case BT_SINK_DISCONNECTED: displayer_control(DISPLAYER_TIMER_PAUSE);
displayer_control(DISPLAYER_DISABLE);
break; break;
case BT_SINK_METADATA: { case BT_SINK_METADATA: {
char *artist = va_arg(args, char*), *album = va_arg(args, char*), *title = va_arg(args, char*); char *artist = va_arg(args, char*), *album = va_arg(args, char*), *title = va_arg(args, char*);
displayer_metadata(artist, album, title); displayer_metadata(artist, album, title);
chain = false;
break; break;
} }
case BT_SINK_PROGRESS: { case BT_SINK_PROGRESS: {
int elapsed = va_arg(args, int), duration = va_arg(args, int); int elapsed = va_arg(args, int), duration = va_arg(args, int);
displayer_timer(DISPLAYER_ELAPSED, elapsed, duration); displayer_timer(DISPLAYER_ELAPSED, elapsed, duration);
chain = false;
break; break;
} }
default: default:
break; break;
} }
if (chain) res = cmd_handler_chain(cmd, args);
va_end(args); va_end(args);
return res; return true;
} }
/* callback for A2DP sink */ /* 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) { if (a2d->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE); esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
(*bt_app_a2d_cmd_cb)(BT_SINK_DISCONNECTED); (*bt_app_a2d_cmd_cb)(BT_SINK_DISCONNECTED);
actrls_unset();
} else if (a2d->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED){ } 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); esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
if (!(*bt_app_a2d_cmd_cb)(BT_SINK_CONNECTED)){ (*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);
}
} }
break; break;
} }
case ESP_A2D_AUDIO_STATE_EVT: { case ESP_A2D_AUDIO_STATE_EVT: {
a2d = (esp_a2d_cb_param_t *)(p_param); 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]); 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) { 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 || } else if (ESP_A2D_AUDIO_STATE_STOPPED == a2d->audio_stat.state ||
ESP_A2D_AUDIO_STATE_REMOTE_SUSPEND == 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; 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); ESP_LOGI(BT_AV_TAG, "A2DP audio stream configuration, codec type %d", a2d->audio_cfg.mcc.type);
// for now only SBC stream is supported // for now only SBC stream is supported
if (a2d->audio_cfg.mcc.type == ESP_A2D_MCT_SBC) { 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]; char oct0 = a2d->audio_cfg.mcc.cie.sbc[0];
if (oct0 & (0x01 << 6)) { if (oct0 & (0x01 << 6)) {
sample_rate = 32000; s_sample_rate = 32000;
} else if (oct0 & (0x01 << 5)) { } else if (oct0 & (0x01 << 5)) {
sample_rate = 44100; s_sample_rate = 44100;
} else if (oct0 & (0x01 << 4)) { } 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", 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[0],
a2d->audio_cfg.mcc.cie.sbc[1], a2d->audio_cfg.mcc.cie.sbc[1],
a2d->audio_cfg.mcc.cie.sbc[2], a2d->audio_cfg.mcc.cie.sbc[2],
a2d->audio_cfg.mcc.cie.sbc[3]); 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; 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) { switch (event_id) {
case ESP_AVRC_RN_TRACK_CHANGE: case ESP_AVRC_RN_TRACK_CHANGE:
ESP_LOGI(BT_AV_TAG, "Track changed");
bt_av_new_track(); bt_av_new_track();
(*bt_app_a2d_cmd_cb)(BT_SINK_PROGRESS, 0, 0); (*bt_app_a2d_cmd_cb)(BT_SINK_PROGRESS, 0, 0);
break; break;
case ESP_AVRC_RN_PLAY_STATUS_CHANGE: case ESP_AVRC_RN_PLAY_STATUS_CHANGE:
ESP_LOGI(BT_AV_TAG, "Playback status changed: 0x%x", event_parameter->playback); 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(); bt_av_playback_changed();
break; break;
case ESP_AVRC_RN_PLAY_POS_CHANGED: 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; break;
} }
case ESP_AVRC_CT_METADATA_RSP_EVT: { 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); 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; 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_ARTIST) p = &s_artist; 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_ALBUM) p = &s_album; 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_PLAYING_TIME) (*bt_app_a2d_cmd_cb)(BT_SINK_PROGRESS, -1, atoi((char*) rc->meta_rsp.attr_text)); 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);
// 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;
}
}
free(rc->meta_rsp.attr_text); free(rc->meta_rsp.attr_text);
break; break;

View File

@@ -11,7 +11,7 @@
#include <stdint.h> #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; 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); 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); void bt_sink_deinit(void);
/**
* @brief * @brief do what's necessary when becoming in charge
*/
void bt_master(bool on);
/** /**
* @brief force disconnection * @brief force disconnection
*/ */

View File

@@ -661,8 +661,12 @@ static bool handle_rtsp(raop_ctx_t *ctx, int sock)
free_metadata(&metadata); free_metadata(&metadata);
} }
} }
} }
// don't need to free "buf" because kd_lookup return a pointer, not a strdup
kd_add(resp, "Audio-Jack-Status", "connected; type=analog");
kd_add(resp, "CSeq", kd_lookup(headers, "CSeq"));
if (success) { if (success) {
buf = http_send(sock, "RTSP/1.0 200 OK", resp); buf = http_send(sock, "RTSP/1.0 200 OK", resp);
} else { } else {

View File

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

View File

@@ -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); void raop_sink_deinit(void);
/**
* @brief do what's necessary when becoming in charge
*/
void raop_master(bool on);
/** /**
* @brief force disconnection * @brief force disconnection
*/ */

View File

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

View File

@@ -18,6 +18,18 @@
#pragma once #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_CLEAR 0x01
#define DISPLAY_ONLY_EOL 0x02 #define DISPLAY_ONLY_EOL 0x02
#define DISPLAY_UPDATE 0x04 #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_LINE_1, DISPLAY_FONT_LINE_2, DISPLAY_FONT_SEGMENT,
DISPLAY_FONT_TINY, DISPLAY_FONT_SMALL, DISPLAY_FONT_MEDIUM, DISPLAY_FONT_LARGE, DISPLAY_FONT_HUGE }; 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 }; enum displayer_time_e { DISPLAYER_ELAPSED, DISPLAYER_REMAINING };
// don't change anything there w/o changing all drivers init code // don't change anything there w/o changing all drivers init code

View File

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