657 lines
23 KiB
C++
657 lines
23 KiB
C++
#include <LilyGo_AMOLED.h>
|
|
#include <LV_Helper.h>
|
|
#include <WiFiClientSecure.h>
|
|
#include <WiFi.h>
|
|
#include <HTTPClient.h>
|
|
#include <ArduinoJson.h>
|
|
#include <esp_wifi.h>
|
|
#include <time.h>
|
|
#include <vector>
|
|
#include "spotify_api.h"
|
|
|
|
#ifndef WIFI_SSID
|
|
#define WIFI_SSID "Menahem"
|
|
#endif
|
|
#ifndef WIFI_PASS
|
|
#define WIFI_PASS "203788583"
|
|
#endif
|
|
|
|
// Spotify API credentials
|
|
static String spotifyClientId = "CLIENT_ID";
|
|
static String spotifyClientSecret = "CLIENT_SECRET";
|
|
static String accessToken = "ACCESS_TOKEN";
|
|
static String refreshToken = "REFRESH_TOKEN";
|
|
|
|
const char *spotifyBaseApiUrl = "https://api.spotify.com/v1";
|
|
|
|
const uint32_t C_SPOTIFY_GREEN = 0x1DB954;
|
|
const uint32_t C_SPOTIFY_BLACK = 0x000000;
|
|
const uint32_t C_WHITE = 0xFFFFFF;
|
|
|
|
// Variables to store current track information
|
|
int currentTrackDuration = 0;
|
|
int currentTrackProgress = 0;
|
|
bool isPlaying = false;
|
|
int httpErrorCount = 0;
|
|
const int maxErrorCount = 3;
|
|
String currentTrackImageUrl = "";
|
|
static unsigned long lastActivityTime = millis();
|
|
|
|
// LVGL objects for the display
|
|
lv_obj_t *labelTrack = nullptr;
|
|
lv_obj_t *labelArtist = nullptr;
|
|
lv_obj_t *barProgress = nullptr;
|
|
lv_obj_t *labelCurrentTime = nullptr;
|
|
lv_obj_t *labelTotalTime = nullptr;
|
|
lv_obj_t *btnContainer = nullptr;
|
|
lv_obj_t *btnPlayPause = nullptr;
|
|
lv_obj_t *btnNext = nullptr;
|
|
lv_obj_t *btnPrevious = nullptr;
|
|
lv_obj_t *btnVolumeDown = nullptr;
|
|
lv_obj_t *btnVolumeUp = nullptr;
|
|
lv_obj_t *labelNowPlaying = nullptr;
|
|
lv_obj_t *screen1 = nullptr;
|
|
lv_obj_t *screen2 = nullptr;
|
|
lv_obj_t *labelScreen2 = nullptr;
|
|
lv_obj_t *deviceList = nullptr;
|
|
lv_obj_t *splashScreen = nullptr;
|
|
lv_obj_t *imgTrack = nullptr;
|
|
lv_obj_t *labelUsername = nullptr;
|
|
lv_obj_t *labelBrowse = nullptr;
|
|
lv_obj_t *btnSettings = nullptr;
|
|
lv_obj_t *labelLoadStep = nullptr; // New label for load step
|
|
lv_obj_t *browseScreen = nullptr;
|
|
|
|
// Task handles for updating track information and displaying the current track
|
|
TaskHandle_t updateTrackInfoTaskHandle = NULL;
|
|
TaskHandle_t fetchTrackImageTaskHandle = NULL;
|
|
TaskHandle_t fetchPlaylistImageTaskHandle = NULL;
|
|
|
|
// Create an instance of the LilyGo_Class for the AMOLED display
|
|
LilyGo_Class amoled;
|
|
|
|
// Create an instance of Spotify API
|
|
SpotifyApi apiClient(spotifyClientId, spotifyClientSecret, accessToken, refreshToken);
|
|
|
|
// Function declarations
|
|
void createPlayingNowScreen();
|
|
void createSettingsScreen();
|
|
void createBrowseScreen();
|
|
void WiFiEvent(WiFiEvent_t event);
|
|
void updateTrackInfo(void *parameter);
|
|
void fetchTrackImage(void *parameter);
|
|
void fetchPlaylistImage(void *parameter);
|
|
void connectToWiFi();
|
|
void checkAndReconnectWiFi();
|
|
void dimScreen();
|
|
void wakeScreen();
|
|
|
|
// Helper function to create buttons
|
|
lv_obj_t *createButton(lv_obj_t *parent, const char *symbol, lv_event_cb_t event_cb, int width = 90, int height = 60, lv_color_t bg_color = lv_color_hex(0xFFFFFF)) {
|
|
lv_obj_t *btn = lv_btn_create(parent);
|
|
lv_obj_set_size(btn, width, height);
|
|
lv_obj_set_style_bg_color(btn, bg_color, 0);
|
|
lv_obj_t *label = lv_label_create(btn);
|
|
lv_label_set_text(label, symbol);
|
|
lv_obj_set_style_text_font(label, &lv_font_montserrat_28, 0);
|
|
if (bg_color.full == lv_color_hex(C_WHITE).full) {
|
|
lv_obj_set_style_text_color(label, lv_color_hex(C_SPOTIFY_BLACK), 0);
|
|
}
|
|
lv_obj_center(label);
|
|
lv_obj_add_event_cb(btn, event_cb, LV_EVENT_CLICKED, NULL);
|
|
return btn;
|
|
}
|
|
|
|
// Function to create the "Playing Now" screen
|
|
void createPlayingNowScreen() {
|
|
// Create screen1
|
|
screen1 = lv_obj_create(NULL);
|
|
lv_scr_load(screen1);
|
|
|
|
// Set the LVGL theme to dark
|
|
lv_theme_t *dark_theme = lv_theme_default_init(NULL, lv_palette_main(LV_PALETTE_GREY), lv_palette_main(LV_PALETTE_BLUE), true, LV_FONT_DEFAULT);
|
|
lv_disp_set_theme(NULL, dark_theme);
|
|
|
|
// Create LVGL objects for the display
|
|
lv_obj_t *topContainer = lv_obj_create(screen1);
|
|
lv_obj_set_width(topContainer, lv_obj_get_width(screen1));
|
|
lv_obj_align(topContainer, LV_ALIGN_TOP_MID, 0, 0); // Adjusted alignment to stick to the top
|
|
lv_obj_set_flex_flow(topContainer, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(topContainer, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
|
lv_obj_set_style_bg_color(topContainer, lv_color_hex(C_SPOTIFY_GREEN), 0); // Set background color to the same as the play button
|
|
lv_obj_set_style_radius(topContainer, 0, 0); // Remove border radius
|
|
lv_obj_set_style_border_width(topContainer, 0, 0); // Remove border
|
|
lv_obj_set_height(topContainer, LV_SIZE_CONTENT); // Set height to fit content
|
|
|
|
labelUsername = lv_label_create(topContainer); // Changed parent to topContainer
|
|
lv_obj_set_style_text_font(labelUsername, &lv_font_montserrat_28, 0);
|
|
lv_label_set_text_fmt(labelUsername, "Hello, %s", apiClient.getUsername().c_str());
|
|
|
|
btnSettings = createButton(
|
|
topContainer, LV_SYMBOL_SETTINGS, [](lv_event_t *e) {
|
|
wakeScreen();
|
|
createSettingsScreen();
|
|
},
|
|
40, 40, lv_color_hex(C_WHITE));
|
|
|
|
imgTrack = lv_img_create(screen1);
|
|
lv_obj_set_size(imgTrack, 64, 64);
|
|
lv_obj_align(imgTrack, LV_ALIGN_TOP_LEFT, 10, 135); // Moved 10 px above
|
|
lv_obj_set_style_radius(imgTrack, LV_RADIUS_CIRCLE, 0); // Make it round
|
|
|
|
lv_obj_t *textContainer = lv_obj_create(screen1);
|
|
lv_obj_remove_style_all(textContainer);
|
|
lv_obj_set_width(textContainer, lv_obj_get_width(screen1) - 94);
|
|
lv_obj_align(textContainer, LV_ALIGN_TOP_LEFT, 84, 135); // Moved 10 px above
|
|
|
|
lv_obj_set_flex_flow(textContainer, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(textContainer, LV_FLEX_ALIGN_SPACE_AROUND, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
|
lv_obj_set_height(textContainer, LV_SIZE_CONTENT); // Set height to fit content
|
|
|
|
labelTrack = lv_label_create(textContainer);
|
|
lv_obj_set_style_text_font(labelTrack, &lv_font_montserrat_32, 0);
|
|
lv_label_set_long_mode(labelTrack, LV_LABEL_LONG_SCROLL_CIRCULAR); // Enable marquee effect for long text
|
|
|
|
labelArtist = lv_label_create(textContainer);
|
|
lv_obj_set_style_text_font(labelArtist, &lv_font_montserrat_22, 0); // Set height to fit content
|
|
lv_label_set_long_mode(labelArtist, LV_LABEL_LONG_WRAP); // Enable line wrapping
|
|
|
|
btnContainer = lv_obj_create(screen1);
|
|
lv_obj_set_size(btnContainer, lv_obj_get_width(screen1), 120);
|
|
lv_obj_align(btnContainer, LV_ALIGN_BOTTOM_MID, 0, 0);
|
|
lv_obj_set_flex_flow(btnContainer, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(btnContainer, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_bg_color(btnContainer, lv_color_hex(C_SPOTIFY_BLACK), 0);
|
|
lv_obj_set_style_radius(btnContainer, 0, 0);
|
|
lv_obj_set_style_border_width(btnContainer, 0, 0); // Remove border
|
|
lv_obj_clear_flag(btnContainer, LV_OBJ_FLAG_SCROLLABLE); // Disable scrolling
|
|
|
|
btnVolumeDown = createButton(btnContainer, LV_SYMBOL_VOLUME_MID, [](lv_event_t *e) {
|
|
wakeScreen();
|
|
apiClient.setVolume(0);
|
|
});
|
|
btnPrevious = createButton(btnContainer, LV_SYMBOL_PREV, [](lv_event_t *e) {
|
|
wakeScreen();
|
|
apiClient.controlSpotify("previous");
|
|
});
|
|
btnPlayPause = createButton(
|
|
btnContainer, isPlaying ? LV_SYMBOL_PAUSE : LV_SYMBOL_PLAY, [](lv_event_t *e) {
|
|
wakeScreen();
|
|
apiClient.controlSpotify(isPlaying ? "pause" : "play");
|
|
},
|
|
110, 70, lv_color_hex(C_SPOTIFY_GREEN));
|
|
btnNext = createButton(btnContainer, LV_SYMBOL_NEXT, [](lv_event_t *e) {
|
|
wakeScreen();
|
|
apiClient.controlSpotify("next");
|
|
});
|
|
btnVolumeUp = createButton(btnContainer, LV_SYMBOL_VOLUME_MAX, [](lv_event_t *e) {
|
|
wakeScreen();
|
|
apiClient.setVolume(100);
|
|
});
|
|
|
|
barProgress = lv_bar_create(screen1);
|
|
lv_obj_set_size(barProgress, lv_obj_get_width(screen1) - 20, 20);
|
|
lv_obj_align(barProgress, LV_ALIGN_BOTTOM_MID, 0, -140); // Moved 30 px above
|
|
|
|
lv_obj_t *timeContainer = lv_obj_create(screen1);
|
|
lv_obj_remove_style_all(timeContainer);
|
|
lv_obj_set_size(timeContainer, lv_obj_get_width(screen1) - 20, 40);
|
|
lv_obj_align_to(timeContainer, barProgress, LV_ALIGN_OUT_TOP_MID, 0, 0);
|
|
lv_obj_set_flex_flow(timeContainer, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(timeContainer, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
|
|
labelCurrentTime = lv_label_create(timeContainer);
|
|
lv_obj_set_style_text_font(labelCurrentTime, &lv_font_montserrat_24, 0);
|
|
|
|
labelTotalTime = lv_label_create(timeContainer);
|
|
lv_obj_set_style_text_font(labelTotalTime, &lv_font_montserrat_24, 0);
|
|
|
|
// Create a task to update track information
|
|
if (!updateTrackInfoTaskHandle) {
|
|
xTaskCreate(updateTrackInfo, "UpdateTrackInfo", 5 * 1024, NULL, 12, &updateTrackInfoTaskHandle);
|
|
}
|
|
|
|
// Add swipe gesture to switch between screens
|
|
lv_obj_add_event_cb(
|
|
screen1, [](lv_event_t *e) {
|
|
lv_event_code_t code = lv_event_get_code(e);
|
|
if (code == LV_EVENT_GESTURE) {
|
|
lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_get_act());
|
|
if (dir == LV_DIR_LEFT) {
|
|
createSettingsScreen();
|
|
} else if (dir == LV_DIR_TOP) {
|
|
createBrowseScreen();
|
|
}
|
|
}
|
|
},
|
|
LV_EVENT_GESTURE, NULL);
|
|
|
|
// Add touch event to wake screen
|
|
lv_obj_add_event_cb(
|
|
screen1, [](lv_event_t *e) {
|
|
wakeScreen();
|
|
},
|
|
LV_EVENT_CLICKED, NULL);
|
|
}
|
|
|
|
void createBrowseScreen() {
|
|
browseScreen = lv_obj_create(NULL);
|
|
lv_scr_load(browseScreen);
|
|
|
|
// Create LVGL objects for the display
|
|
lv_obj_t *topBrowseContainer = lv_obj_create(browseScreen);
|
|
lv_obj_set_width(topBrowseContainer, lv_obj_get_width(screen1));
|
|
lv_obj_align(topBrowseContainer, LV_ALIGN_TOP_MID, 0, 0); // Adjusted alignment to stick to the top
|
|
lv_obj_set_flex_flow(topBrowseContainer, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(topBrowseContainer, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
|
lv_obj_set_style_bg_color(topBrowseContainer, lv_color_hex(C_SPOTIFY_GREEN), 0); // Set background color to the same as the play button
|
|
lv_obj_set_style_radius(topBrowseContainer, 0, 0); // Remove border radius
|
|
lv_obj_set_style_border_width(topBrowseContainer, 0, 0); // Remove border
|
|
lv_obj_set_height(topBrowseContainer, LV_SIZE_CONTENT); // Set height to fit content
|
|
|
|
labelBrowse = lv_label_create(topBrowseContainer);
|
|
lv_label_set_text(labelBrowse, "Browse");
|
|
lv_obj_set_style_text_font(labelBrowse, &lv_font_montserrat_28, 0);
|
|
|
|
// Create a list to display featured playlists
|
|
lv_obj_t *playlistList = lv_list_create(browseScreen);
|
|
lv_obj_set_size(playlistList, lv_obj_get_width(browseScreen) - 20, lv_obj_get_height(browseScreen) - 75);
|
|
lv_obj_align(playlistList, LV_ALIGN_TOP_LEFT, 10, 75);
|
|
|
|
// Fetch featured playlists from the API client
|
|
auto [playlists, playlistCount] = apiClient.getFeaturedPlaylists(5, 0);
|
|
checkAndReconnectWiFi();
|
|
if (playlists == nullptr || playlistCount == 0) {
|
|
Serial.println("No playlists found or failed to fetch playlists.");
|
|
return;
|
|
}
|
|
|
|
// Add playlists to the list
|
|
for (size_t i = 0; i < playlistCount; ++i) {
|
|
lv_obj_t *list_btn = lv_list_add_btn(playlistList, NULL, playlists[i].name.c_str());
|
|
char *playlistId = new char[playlists[i].id.length() + 1];
|
|
strcpy(playlistId, playlists[i].id.c_str());
|
|
lv_obj_set_user_data(list_btn, (void *)playlistId); // Use actual id as user data
|
|
|
|
// Create an image object for the playlist cover
|
|
lv_obj_t *img = lv_img_create(list_btn);
|
|
lv_obj_set_size(img, 64, 64);
|
|
lv_obj_align(img, LV_ALIGN_LEFT_MID, 10, 0);
|
|
|
|
// Fetch the image from the remote URL in a non-blocking task
|
|
if (!fetchPlaylistImageTaskHandle) {
|
|
xTaskCreate(fetchPlaylistImage, "FetchPlaylistImage", 5 * 1024, (void *)img, 12, &fetchPlaylistImageTaskHandle);
|
|
}
|
|
|
|
lv_obj_add_event_cb(
|
|
list_btn, [](lv_event_t *e) {
|
|
lv_obj_t *btn = lv_event_get_target(e);
|
|
const char *playlistId = (const char *)lv_obj_get_user_data(btn);
|
|
apiClient.playPlaylist(String(playlistId));
|
|
checkAndReconnectWiFi();
|
|
Serial.println("Selected playlist ID: " + String(playlistId));
|
|
lv_scr_load(screen1); // Move back to playing now screen
|
|
},
|
|
LV_EVENT_CLICKED, NULL);
|
|
}
|
|
|
|
// Clean up allocated memory for playlists
|
|
delete[] playlists;
|
|
|
|
// Add swipe gesture to switch between screens
|
|
lv_obj_add_event_cb(
|
|
browseScreen, [](lv_event_t *e) {
|
|
lv_event_code_t code = lv_event_get_code(e);
|
|
if (code == LV_EVENT_GESTURE) {
|
|
lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_get_act());
|
|
if (dir == LV_DIR_BOTTOM) {
|
|
lv_scr_load(screen1); // Move back to playing now screen
|
|
}
|
|
}
|
|
},
|
|
LV_EVENT_GESTURE, NULL);
|
|
|
|
// Add touch event to wake screen
|
|
lv_obj_add_event_cb(
|
|
browseScreen, [](lv_event_t *e) {
|
|
wakeScreen();
|
|
},
|
|
LV_EVENT_CLICKED, NULL);
|
|
}
|
|
|
|
// Function to create the settings screen
|
|
void createSettingsScreen() {
|
|
// Create screen2
|
|
screen2 = lv_obj_create(NULL);
|
|
lv_scr_load(screen2); // Ensure the screen is loaded before adding objects
|
|
|
|
lv_obj_t *topDevicesContainer = lv_obj_create(screen2);
|
|
lv_obj_set_width(topDevicesContainer, lv_obj_get_width(screen1));
|
|
lv_obj_align(topDevicesContainer, LV_ALIGN_TOP_MID, 0, 0); // Adjusted alignment to stick to the top
|
|
lv_obj_set_flex_flow(topDevicesContainer, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(topDevicesContainer, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
|
lv_obj_set_style_bg_color(topDevicesContainer, lv_color_hex(C_SPOTIFY_GREEN), 0); // Set background color to the same as the play button
|
|
lv_obj_set_style_radius(topDevicesContainer, 0, 0); // Remove border radius
|
|
lv_obj_set_style_border_width(topDevicesContainer, 0, 0); // Remove border
|
|
lv_obj_set_height(topDevicesContainer, LV_SIZE_CONTENT); // Set height to fit content
|
|
|
|
labelScreen2 = lv_label_create(topDevicesContainer);
|
|
lv_label_set_text(labelScreen2, "Available Devices:");
|
|
lv_obj_set_style_text_font(labelScreen2, &lv_font_montserrat_28, 0);
|
|
|
|
deviceList = lv_list_create(screen2);
|
|
if (deviceList == nullptr) {
|
|
Serial.println("Failed to create device list");
|
|
return;
|
|
}
|
|
lv_obj_set_size(deviceList, lv_obj_get_width(screen2) - 20, lv_obj_get_height(screen2) - 75);
|
|
lv_obj_align(deviceList, LV_ALIGN_TOP_LEFT, 10, 75);
|
|
|
|
Serial.println("Fetching devices list...");
|
|
|
|
// Fetch devices from the API client
|
|
auto [devices, deviceCount] = apiClient.getDevicesList();
|
|
checkAndReconnectWiFi();
|
|
if (devices == nullptr || deviceCount == 0) {
|
|
Serial.println("No devices found or failed to fetch devices.");
|
|
return;
|
|
}
|
|
|
|
// Add devices to the list
|
|
for (size_t i = 0; i < deviceCount; ++i) {
|
|
lv_obj_t *list_btn = lv_list_add_btn(deviceList, LV_SYMBOL_AUDIO, devices[i].name.c_str());
|
|
char *deviceId = new char[devices[i].id.length() + 1];
|
|
strcpy(deviceId, devices[i].id.c_str());
|
|
lv_obj_set_user_data(list_btn, (void *)deviceId); // Use actual id as user data
|
|
lv_obj_add_event_cb(
|
|
list_btn, [](lv_event_t *e) {
|
|
lv_obj_t *btn = lv_event_get_target(e);
|
|
const char *deviceId = (const char *)lv_obj_get_user_data(btn);
|
|
apiClient.setActiveDevice(String(deviceId));
|
|
checkAndReconnectWiFi();
|
|
Serial.println("Selected device ID: " + String(deviceId));
|
|
},
|
|
LV_EVENT_CLICKED, NULL);
|
|
}
|
|
|
|
// Clean up allocated memory for devices
|
|
delete[] devices;
|
|
|
|
// Add swipe gesture to switch between screens
|
|
lv_obj_add_event_cb(
|
|
screen2, [](lv_event_t *e) {
|
|
lv_event_code_t code = lv_event_get_code(e);
|
|
if (code == LV_EVENT_GESTURE) {
|
|
lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_get_act());
|
|
if (dir == LV_DIR_RIGHT) {
|
|
lv_scr_load(screen1);
|
|
}
|
|
}
|
|
},
|
|
LV_EVENT_GESTURE, NULL);
|
|
|
|
// Add touch event to wake screen
|
|
lv_obj_add_event_cb(
|
|
screen2, [](lv_event_t *e) {
|
|
wakeScreen();
|
|
},
|
|
LV_EVENT_CLICKED, NULL);
|
|
}
|
|
|
|
// Function to show the splash screen
|
|
void showSplashScreen() {
|
|
splashScreen = lv_obj_create(NULL);
|
|
lv_obj_set_style_bg_color(splashScreen, lv_color_hex(C_SPOTIFY_GREEN), 0);
|
|
lv_obj_t *label = lv_label_create(splashScreen);
|
|
lv_label_set_text(label, "Spotify Remote Controller");
|
|
lv_obj_set_style_text_color(label, lv_color_hex(C_WHITE), 0);
|
|
lv_obj_set_style_text_font(label, &lv_font_montserrat_32, 0);
|
|
lv_obj_center(label);
|
|
|
|
// Create and position the load step label
|
|
labelLoadStep = lv_label_create(splashScreen);
|
|
lv_label_set_text(labelLoadStep, "Loading...");
|
|
lv_obj_set_style_text_color(labelLoadStep, lv_color_hex(C_WHITE), 0);
|
|
lv_obj_set_style_text_font(labelLoadStep, &lv_font_montserrat_24, 0);
|
|
lv_obj_align(labelLoadStep, LV_ALIGN_BOTTOM_MID, 0, -20);
|
|
|
|
lv_scr_load(splashScreen);
|
|
lv_task_handler(); // Ensure the splash screen is rendered
|
|
}
|
|
|
|
// Function to set up the device
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
Serial.println("============================================");
|
|
Serial.println("Welcome to spotify player");
|
|
Serial.println("============================================");
|
|
|
|
// Initialize WiFi in station mode
|
|
WiFi.mode(WIFI_STA);
|
|
|
|
// Initialize the AMOLED display
|
|
bool rslt = false;
|
|
rslt = amoled.beginAMOLED_241();
|
|
|
|
if (!rslt) {
|
|
while (1) {
|
|
Serial.println("The board model cannot be detected, please raise the Core Debug Level to an error");
|
|
delay(500);
|
|
}
|
|
}
|
|
|
|
// Initialize LVGL helper
|
|
beginLvglHelper(amoled);
|
|
|
|
showSplashScreen();
|
|
|
|
// Connect to WiFi
|
|
connectToWiFi();
|
|
|
|
lastActivityTime = millis();
|
|
|
|
createPlayingNowScreen();
|
|
}
|
|
|
|
void updateLoadStep(const char *step) {
|
|
if (labelLoadStep) {
|
|
lv_label_set_text(labelLoadStep, step);
|
|
lv_task_handler();
|
|
}
|
|
}
|
|
|
|
// Function to handle the main loop
|
|
void loop() {
|
|
// Handle LVGL tasks
|
|
lv_task_handler();
|
|
delay(1);
|
|
|
|
// Check for inactivity and dim screen if needed
|
|
if (millis() - lastActivityTime > 20000) {
|
|
dimScreen();
|
|
}
|
|
}
|
|
|
|
// Function to handle WiFi events
|
|
void WiFiEvent(WiFiEvent_t event) {
|
|
switch (event) {
|
|
case ARDUINO_EVENT_WIFI_SCAN_DONE:
|
|
Serial.println("Completed scan for access points");
|
|
break;
|
|
case ARDUINO_EVENT_WIFI_STA_START:
|
|
Serial.println("WiFi client started");
|
|
break;
|
|
case ARDUINO_EVENT_WIFI_STA_STOP:
|
|
Serial.println("WiFi client stopped");
|
|
break;
|
|
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
|
Serial.println("Connected to access point");
|
|
break;
|
|
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
|
Serial.println("Disconnected from WiFi access point");
|
|
break;
|
|
case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE:
|
|
Serial.println("Authentication mode of access point has changed");
|
|
break;
|
|
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
|
Serial.print("Obtained IP address: ");
|
|
Serial.println(WiFi.localIP());
|
|
|
|
if (!updateTrackInfoTaskHandle) {
|
|
xTaskCreate(updateTrackInfo, "UpdateTrackInfo", 5 * 1024, NULL, 12, &updateTrackInfoTaskHandle);
|
|
}
|
|
|
|
break;
|
|
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
|
|
Serial.println("Lost IP address and IP address is reset to 0");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Function to update track information
|
|
void updateTrackInfo(void *parameter) {
|
|
for (;;) {
|
|
TrackInfo trackInfo = apiClient.getCurrentTrackInfo();
|
|
checkAndReconnectWiFi();
|
|
|
|
isPlaying = trackInfo.isPlaying;
|
|
|
|
lv_label_set_text(labelTrack, trackInfo.name.c_str());
|
|
lv_label_set_text(labelArtist, trackInfo.artistName.c_str());
|
|
lv_label_set_text(lv_obj_get_child(btnPlayPause, NULL), trackInfo.isPlaying ? LV_SYMBOL_PAUSE : LV_SYMBOL_PLAY);
|
|
|
|
if (trackInfo.coverUrl != currentTrackImageUrl) {
|
|
currentTrackImageUrl = trackInfo.coverUrl;
|
|
if (!fetchTrackImageTaskHandle) {
|
|
xTaskCreate(fetchTrackImage, "FetchTrackImage", 5 * 1024, (void *)currentTrackImageUrl.c_str(), 12, &fetchTrackImageTaskHandle);
|
|
}
|
|
}
|
|
|
|
currentTrackDuration = trackInfo.duration;
|
|
currentTrackProgress = trackInfo.progress;
|
|
if (currentTrackDuration != 0) {
|
|
lv_bar_set_value(barProgress, (currentTrackProgress * 100) / currentTrackDuration, LV_ANIM_OFF);
|
|
}
|
|
|
|
int currentTimeSec = currentTrackProgress / 1000;
|
|
int totalTimeSec = currentTrackDuration / 1000;
|
|
char currentTimeStr[10];
|
|
char totalTimeStr[10];
|
|
snprintf(currentTimeStr, sizeof(currentTimeStr), "%02d:%02d", currentTimeSec / 60, currentTimeSec % 60);
|
|
snprintf(totalTimeStr, sizeof(totalTimeStr), "%02d:%02d", totalTimeSec / 60, totalTimeSec % 60);
|
|
lv_label_set_text(labelCurrentTime, currentTimeStr);
|
|
lv_label_set_text(labelTotalTime, totalTimeStr);
|
|
|
|
vTaskDelay(500 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
// Function to fetch track image
|
|
void fetchTrackImage(void *parameter) {
|
|
const char *imageUrl = (const char *)parameter;
|
|
HTTPClient http;
|
|
http.begin(imageUrl);
|
|
int httpResponseCode = http.GET();
|
|
if (httpResponseCode == HTTP_CODE_OK) {
|
|
int len = http.getSize();
|
|
uint8_t *buffer = (uint8_t *)malloc(len);
|
|
if (buffer) {
|
|
WiFiClient *stream = http.getStreamPtr();
|
|
stream->readBytes(buffer, len);
|
|
// Save the original image in ROM
|
|
lv_img_dsc_t *img_dsc = (lv_img_dsc_t *)malloc(sizeof(lv_img_dsc_t));
|
|
if (img_dsc) {
|
|
img_dsc->data = buffer;
|
|
img_dsc->data_size = len;
|
|
img_dsc->header.always_zero = 0;
|
|
img_dsc->header.w = 64;
|
|
img_dsc->header.h = 64;
|
|
img_dsc->header.cf = LV_IMG_CF_TRUE_COLOR;
|
|
lv_img_set_src(imgTrack, img_dsc);
|
|
} else {
|
|
free(buffer);
|
|
}
|
|
}
|
|
} else {
|
|
Serial.println("Failed to fetch track image, error: " + String(httpResponseCode));
|
|
if (httpResponseCode == -1) {
|
|
Serial.println("HTTP request failed with error: -1, reconnecting WiFi...");
|
|
connectToWiFi();
|
|
}
|
|
}
|
|
http.end();
|
|
fetchTrackImageTaskHandle = NULL;
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
// Function to fetch playlist image
|
|
void fetchPlaylistImage(void *parameter) {
|
|
lv_obj_t *img = (lv_obj_t *)parameter;
|
|
const char *imageUrl = (const char *)lv_obj_get_user_data(img);
|
|
HTTPClient http;
|
|
http.begin(imageUrl);
|
|
int httpResponseCode = http.GET();
|
|
if (httpResponseCode == HTTP_CODE_OK) {
|
|
int len = http.getSize();
|
|
uint8_t *buffer = (uint8_t *)malloc(len);
|
|
if (buffer) {
|
|
WiFiClient *stream = http.getStreamPtr();
|
|
stream->readBytes(buffer, len);
|
|
// Save the original image in ROM
|
|
lv_img_dsc_t *img_dsc = (lv_img_dsc_t *)malloc(sizeof(lv_img_dsc_t));
|
|
if (img_dsc) {
|
|
img_dsc->data = buffer;
|
|
img_dsc->data_size = len;
|
|
img_dsc->header.always_zero = 0;
|
|
img_dsc->header.w = 64;
|
|
img_dsc->header.h = 64;
|
|
img_dsc->header.cf = LV_IMG_CF_TRUE_COLOR;
|
|
lv_img_set_src(img, img_dsc);
|
|
} else {
|
|
free(buffer);
|
|
}
|
|
}
|
|
} else {
|
|
Serial.println("Failed to fetch playlist image, error: " + String(httpResponseCode));
|
|
if (httpResponseCode == -1) {
|
|
Serial.println("HTTP request failed with error: -1, reconnecting WiFi...");
|
|
connectToWiFi();
|
|
}
|
|
}
|
|
http.end();
|
|
fetchPlaylistImageTaskHandle = NULL;
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
// Function to connect to WiFi
|
|
void connectToWiFi() {
|
|
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
|
while (WiFi.status() != WL_CONNECTED) {
|
|
updateLoadStep("Connecting to WiFi...");
|
|
delay(1000);
|
|
}
|
|
updateLoadStep("Connected to WiFi");
|
|
delay(500);
|
|
}
|
|
|
|
// Function to check and reconnect WiFi if needed
|
|
void checkAndReconnectWiFi() {
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
Serial.println("WiFi disconnected, reconnecting...");
|
|
connectToWiFi();
|
|
}
|
|
}
|
|
|
|
// Function to dim the screen
|
|
void dimScreen() {
|
|
amoled.setBrightness(15);
|
|
}
|
|
|
|
// Function to wake the screen
|
|
void wakeScreen() {
|
|
amoled.setBrightness(100);
|
|
lastActivityTime = millis(); // Reset inactivity timer
|
|
}
|