diff --git a/README.md b/README.md index 8928ff6..74cc8b7 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,25 @@ Your project should now be setup and will start displaying your currently playin ## Code -If you want to program the devices yourself using the Arudino IDE, you will need to do the following to get it working +If you want to program this project manually, there are two options + +### PlatformIO + +PlatformIO is the easiest way to code this project. + +In the [platformio.ini](platformio.ini), there are several environments defined for the different boards + +| Environment | Description | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------- | +| env:cyd | For the [Cheap Yellow Display](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display) | +| env:cyd2usb | For the Cheap Yellow Display with two USB ports | +| env:trinity | For the [ESP32 Trinity](https://github.com/witnessmenow/ESP32-Trinity) (or generic ESP32 wired to the matrix panel the same) | + +When you select the environment, it will automatically install the right libraries and set the configurations in the code. + +### Arduino IDE + +If you want to use the Arduino IDE, you will need to do the following to get it working The following libraries need to be installed for this project to work: @@ -168,26 +186,26 @@ The following libraries need to be installed for this project to work: | [ESP_DoubleResetDetector](https://github.com/khoih-prog/ESP_DoubleResetDetector) | Detecting double pressing the reset button | Yes ("ESP_DoubleResetDetector") | | [Seeed_Arduino_NFC](https://github.com/witnessmenow/Seeed_Arduino_NFC) | For the NFC reader | No (it's a modified version) | -### Cheap Yellow Display Specific libraries +#### Cheap Yellow Display Specific libraries | Library Name/Link | Purpose | Library manager | | ---------------------------------------------- | ------------------------------- | ---------------- | | [TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) | For controlling the LCD Display | Yes ("tft_espi") | -### Matrix Panel Specific libraries +#### Matrix Panel Specific libraries | Library Name/Link | Purpose | Library manager | | ------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------ | | [ESP32-HUB75-MatrixPanel-I2S-DMA](https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA) | For controlling the LED Matrix | Yes ("ESP32 MATRIX DMA") | | [Adafruit GFX library](https://github.com/adafruit/Adafruit-GFX-Library) | Dependency of the Matrix library | Yes ("Adafruit GFX") | -### Cheap Yellow Display Display Config +#### Cheap Yellow Display Display Config The CYD version of the project makes use of [TFT_eSPI library by Bodmer](https://github.com/Bodmer/TFT_eSPI). TFT_eSPI is configured using a "User_Setup.h" file in the library folder, you will need to replace this file with the one in the `DisplayConfig` folder of this repo. -### Display Selection +#### Display Selection At the top of the `SpotifyDiyThing.ino` file, there is a section labeled "Display Type", follow the instructions there for how to enable the different displays. diff --git a/SpotifyDiyThing/SpotifyDiyThing.ino b/SpotifyDiyThing/SpotifyDiyThing.ino index c5ef034..51fd9c7 100644 --- a/SpotifyDiyThing/SpotifyDiyThing.ino +++ b/SpotifyDiyThing/SpotifyDiyThing.ino @@ -132,7 +132,7 @@ void setup() spotifyDisplay->displaySetup(&spotify); #ifdef NFC_ENABLED - if (nfcSetup(&spotify)) + if (nfcSetup(&spotify, spotifyDisplay)) { Serial.println("NFC Good"); } @@ -211,7 +211,7 @@ void loop() spotifyDisplay->checkForInput(); #ifdef NFC_ENABLED - bool forceUpdate = nfcLoop(); + bool forceUpdate = nfcLoop(lastTrackUri); #else bool forceUpdate = false; #endif diff --git a/SpotifyDiyThing/cheapYellowLCD.h b/SpotifyDiyThing/cheapYellowLCD.h index 214b35d..673fcab 100644 --- a/SpotifyDiyThing/cheapYellowLCD.h +++ b/SpotifyDiyThing/cheapYellowLCD.h @@ -6,7 +6,7 @@ // A library for checking if the reset button has been pressed twice // Can be used to enable config mode // Can be installed from the library manager (Search for "ESP_DoubleResetDetector") -//https://github.com/khoih-prog/ESP_DoubleResetDetector +// https://github.com/khoih-prog/ESP_DoubleResetDetector #include // Library for decoding Jpegs from the API responses @@ -23,7 +23,7 @@ TFT_eSPI tft = TFT_eSPI(); JPEGDEC jpeg; -const char* ALBUM_ART = "/album.jpg"; +const char *ALBUM_ART = "/album.jpg"; // This next function will be called during decoding of the jpeg file to // render each block to the Matrix. If you use a different display @@ -31,7 +31,8 @@ const char* ALBUM_ART = "/album.jpg"; int JPEGDraw(JPEGDRAW *pDraw) { // Stop further decoding as image is running off bottom of screen - if ( pDraw->y >= tft.height() ) return 0; + if (pDraw->y >= tft.height()) + return 0; tft.pushImage(pDraw->x, pDraw->y, pDraw->iWidth, pDraw->iHeight, pDraw->pPixels); return 1; @@ -39,270 +40,302 @@ int JPEGDraw(JPEGDRAW *pDraw) fs::File myfile; -void * myOpen(const char *filename, int32_t *size) { +void *myOpen(const char *filename, int32_t *size) +{ myfile = SPIFFS.open(filename); *size = myfile.size(); return &myfile; } -void myClose(void *handle) { - if (myfile) myfile.close(); +void myClose(void *handle) +{ + if (myfile) + myfile.close(); } -int32_t myRead(JPEGFILE *handle, uint8_t *buffer, int32_t length) { - if (!myfile) return 0; +int32_t myRead(JPEGFILE *handle, uint8_t *buffer, int32_t length) +{ + if (!myfile) + return 0; return myfile.read(buffer, length); } -int32_t mySeek(JPEGFILE *handle, int32_t position) { - if (!myfile) return 0; +int32_t mySeek(JPEGFILE *handle, int32_t position) +{ + if (!myfile) + return 0; return myfile.seek(position); } -class CheapYellowDisplay: public SpotifyDisplay { - public: +class CheapYellowDisplay : public SpotifyDisplay +{ +public: + void displaySetup(SpotifyArduino *spotifyObj) + { - void displaySetup(SpotifyArduino *spotifyObj) { + spotify_display = spotifyObj; - spotify_display = spotifyObj; + touchSetup(spotifyObj); - touchSetup(spotifyObj); + Serial.println("cyd display setup"); + setWidth(320); + setHeight(240); - Serial.println("cyd display setup"); - setWidth(320); - setHeight(240); + setImageHeight(150); + setImageWidth(150); - setImageHeight(150); - setImageWidth(150); + // Start the tft display and set it to black + tft.init(); + tft.setRotation(1); + tft.fillScreen(TFT_BLACK); + } - // Start the tft display and set it to black - tft.init(); - tft.setRotation(1); - tft.fillScreen(TFT_BLACK); - } + void showDefaultScreen() + { + tft.fillScreen(TFT_BLACK); - void showDefaultScreen() { - tft.fillScreen(TFT_BLACK); + drawTouchButtons(false, false); + } - drawTouchButtons(false, false); - } + void displayTrackProgress(long progress, long duration) + { - void displayTrackProgress(long progress, long duration) { + // Serial.print("Elapsed time of song (ms): "); + // Serial.print(progress); + // Serial.print(" of "); + // Serial.println(duration); + // Serial.println(); - // Serial.print("Elapsed time of song (ms): "); - // Serial.print(progress); - // Serial.print(" of "); - // Serial.println(duration); - // Serial.println(); + float percentage = ((float)progress / (float)duration) * 100; + int clampedPercentage = (int)percentage; + // Serial.println(clampedPercentage); + int barXWidth = map(clampedPercentage, 0, 100, 0, screenWidth - 40); + // Serial.println(barXWidth); - float percentage = ((float)progress / (float)duration) * 100; - int clampedPercentage = (int)percentage; - //Serial.println(clampedPercentage); - int barXWidth = map(clampedPercentage, 0, 100, 0, screenWidth - 40); - //Serial.println(barXWidth); + int progressStartY = 150 + 5; - int progressStartY = 150 + 5; + // Draw outer Rectangle, in theory we only need to do this once! + tft.drawRect(19, progressStartY, screenWidth - 38, 20, TFT_WHITE); - // Draw outer Rectangle, in theory we only need to do this once! - tft.drawRect(19, progressStartY, screenWidth - 38, 20, TFT_WHITE); + // Draw the white portion of the filled bar + tft.fillRect(20, progressStartY + 1, barXWidth, 18, TFT_WHITE); - // Draw the white portion of the filled bar - tft.fillRect(20, progressStartY + 1, barXWidth, 18, TFT_WHITE); + // Fill whats left black + tft.fillRect(20 + barXWidth, progressStartY + 1, (screenWidth - 20) - (20 + barXWidth), 18, TFT_BLACK); + } - // Fill whats left black - tft.fillRect(20 + barXWidth, progressStartY + 1, (screenWidth - 20) - (20 + barXWidth), 18, TFT_BLACK); - } - - void printCurrentlyPlayingToScreen(CurrentlyPlaying currentlyPlaying) { - // Clear the text - int textStartY = 150 + 30; - tft.fillRect(0, textStartY, screenWidth, screenHeight - textStartY, TFT_BLACK); + void printCurrentlyPlayingToScreen(CurrentlyPlaying currentlyPlaying) + { + // Clear the text + int textStartY = 150 + 30; + tft.fillRect(0, textStartY, screenWidth, screenHeight - textStartY, TFT_BLACK); - tft.drawCentreString(currentlyPlaying.trackName, screenCenterX, textStartY, 2); - tft.drawCentreString(currentlyPlaying.artists[0].artistName, screenCenterX, textStartY + 18, 2); - tft.drawCentreString(currentlyPlaying.albumName, screenCenterX, textStartY + 36, 2); - } + tft.drawCentreString(currentlyPlaying.trackName, screenCenterX, textStartY, 2); + tft.drawCentreString(currentlyPlaying.artists[0].artistName, screenCenterX, textStartY + 18, 2); + tft.drawCentreString(currentlyPlaying.albumName, screenCenterX, textStartY + 36, 2); + } - void checkForInput() { - if (millis() > touchScreenCoolDownTime && handleTouched()) { - drawTouchButtons(previousTrackStatus, nextTrackStatus); - if (previousTrackStatus) { - spotify_display->previousTrack(); - } else if (nextTrackStatus) { - spotify_display->nextTrack(); - } - drawTouchButtons(false, false); - requestDueTime = 0; //Some button has been pressed and acted on, it surely impacts the status so force a refresh - touchScreenCoolDownTime = millis() + touchScreenCoolDownInterval; //Cool the touch off + void checkForInput() + { + if (millis() > touchScreenCoolDownTime && handleTouched()) + { + drawTouchButtons(previousTrackStatus, nextTrackStatus); + if (previousTrackStatus) + { + spotify_display->previousTrack(); + } + else if (nextTrackStatus) + { + spotify_display->nextTrack(); } + drawTouchButtons(false, false); + requestDueTime = 0; // Some button has been pressed and acted on, it surely impacts the status so force a refresh + touchScreenCoolDownTime = millis() + touchScreenCoolDownInterval; // Cool the touch off } - - //Image Related - void clearImage() { - int imagePosition = screenCenterX - (imageWidth / 2); - tft.fillRect(imagePosition, 0, imageWidth, imageHeight, TFT_BLACK); + } + + // Image Related + void clearImage() + { + int imagePosition = screenCenterX - (imageWidth / 2); + tft.fillRect(imagePosition, 0, imageWidth, imageHeight, TFT_BLACK); + } + + boolean processImageInfo(CurrentlyPlaying currentlyPlaying) + { + SpotifyImage currentlyPlayingMedImage = currentlyPlaying.albumImages[currentlyPlaying.numImages - 2]; + if (!albumDisplayed || !isSameAlbum(currentlyPlayingMedImage.url)) + { + // We have a differenent album than we currently have displayed + albumDisplayed = false; + setImageHeight(currentlyPlayingMedImage.height / 2); // medium image is 300, we are going to scale it to half + setImageWidth(currentlyPlayingMedImage.width / 2); + setAlbumArtUrl(currentlyPlayingMedImage.url); + return true; } - boolean processImageInfo(CurrentlyPlaying currentlyPlaying) { - SpotifyImage currentlyPlayingMedImage = currentlyPlaying.albumImages[currentlyPlaying.numImages - 2]; - if (!albumDisplayed || !isSameAlbum(currentlyPlayingMedImage.url)) { - // We have a differenent album than we currently have displayed - albumDisplayed = false; - setImageHeight(currentlyPlayingMedImage.height / 2); //medium image is 300, we are going to scale it to half - setImageWidth(currentlyPlayingMedImage.width / 2); - setAlbumArtUrl(currentlyPlayingMedImage.url); - return true; - } + return false; + } - return false; + int displayImage() + { + int imageStatus = displayImageUsingFile(_albumArtUrl); + Serial.print("imageStatus: "); + Serial.println(imageStatus); + if (imageStatus == 1) + { + albumDisplayed = true; + return imageStatus; } - int displayImage() { - int imageStatus = displayImageUsingFile(_albumArtUrl); - Serial.print("imageStatus: "); - Serial.println(imageStatus); - if (imageStatus) { - albumDisplayed = true; - return imageStatus; - } + return imageStatus; + } + + // NFC tag messages + void markDisplayAsTagRead() + { + int imagePosition = screenCenterX - (imageWidth / 2); + tft.drawRect(imagePosition, 0, imageWidth, imageHeight, TFT_BLUE); + tft.drawRect(imagePosition + 2, 2, imageWidth - 4, imageHeight - 4, TFT_RED); + } + void markDisplayAsTagWritten() + { + int imagePosition = screenCenterX - (imageWidth / 2); + tft.drawRect(imagePosition, 0, imageWidth, imageHeight, TFT_RED); + tft.drawRect(imagePosition + 2, 2, imageWidth - 4, imageHeight - 4, TFT_GREEN); + } + + void drawWifiManagerMessage(WiFiManager *myWiFiManager) + { + Serial.println("Entered Conf Mode"); + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.drawCentreString("Entered Conf Mode:", screenCenterX, 5, 2); + tft.drawString("Connect to the following WIFI AP:", 5, 28, 2); + tft.setTextColor(TFT_BLUE, TFT_BLACK); + tft.drawString(myWiFiManager->getConfigPortalSSID(), 20, 48, 2); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.drawString("Password:", 5, 64, 2); + tft.setTextColor(TFT_BLUE, TFT_BLACK); + tft.drawString("thing123", 20, 82, 2); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + + tft.drawString("If it doesn't AutoConnect, use this IP:", 5, 110, 2); + tft.setTextColor(TFT_BLUE, TFT_BLACK); + tft.drawString(WiFi.softAPIP().toString(), 20, 128, 2); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + } + + void drawRefreshTokenMessage() + { + Serial.println("Refresh Token Mode"); + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.drawCentreString("Refresh Token Mode:", screenCenterX, 5, 2); + tft.drawString("You need to authorize this device to use", 5, 28, 2); + tft.drawString("your spotify account.", 5, 46, 2); + + tft.drawString("Visit the following address and follow", 5, 82, 2); + tft.drawString("the instrucitons:", 5, 100, 2); + tft.setTextColor(TFT_BLUE, TFT_BLACK); + tft.drawString(WiFi.localIP().toString(), 10, 128, 2); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + } + +private: + unsigned long touchScreenCoolDownInterval = 200; // How long after a touch press do we accept another (0.2 seconds). There is also an APi request inbetween + unsigned long touchScreenCoolDownTime; // time when cool down has expired + + int displayImageUsingFile(char *albumArtUrl) + { + + // In this example I reuse the same filename + // over and over, maybe saving the art using + // the album URI as the name would be better + // as you could save having to download them each + // time, but this seems to work fine. + if (SPIFFS.exists(ALBUM_ART) == true) + { + Serial.println("Removing existing image"); + SPIFFS.remove(ALBUM_ART); + } - return imageStatus; + fs::File f = SPIFFS.open(ALBUM_ART, "w+"); + if (!f) + { + Serial.println("file open failed"); + return -1; } + // Spotify uses a different cert for the Image server, so we need to swap to that for the call + client.setCACert(spotify_image_server_cert); + bool gotImage = spotify_display->getImage(albumArtUrl, &f); + // Swapping back to the main spotify cert + client.setCACert(spotify_server_cert); - //NFC tag messages - void markDisplayAsTagRead() { + // Make sure to close the file! + f.close(); + if (gotImage) + { + return drawImagefromFile(ALBUM_ART); } - void markDisplayAsTagWritten() { - + else + { + return -2; } - - void drawWifiManagerMessage(WiFiManager *myWiFiManager) { - Serial.println("Entered Conf Mode"); - tft.fillScreen(TFT_BLACK); - tft.setTextColor(TFT_WHITE, TFT_BLACK); - tft.drawCentreString("Entered Conf Mode:", screenCenterX, 5, 2); - tft.drawString("Connect to the following WIFI AP:", 5, 28, 2); - tft.setTextColor(TFT_BLUE, TFT_BLACK); - tft.drawString(myWiFiManager->getConfigPortalSSID(), 20, 48, 2); - tft.setTextColor(TFT_WHITE, TFT_BLACK); - tft.drawString("Password:", 5, 64, 2); - tft.setTextColor(TFT_BLUE, TFT_BLACK); - tft.drawString("thing123", 20, 82, 2); - tft.setTextColor(TFT_WHITE, TFT_BLACK); - - tft.drawString("If it doesn't AutoConnect, use this IP:", 5, 110, 2); - tft.setTextColor(TFT_BLUE, TFT_BLACK); - tft.drawString(WiFi.softAPIP().toString(), 20, 128, 2); - tft.setTextColor(TFT_WHITE, TFT_BLACK); + } + + int drawImagefromFile(const char *imageFileUri) + { + unsigned long lTime = millis(); + lTime = millis(); + jpeg.open((const char *)imageFileUri, myOpen, myClose, myRead, mySeek, JPEGDraw); + jpeg.setPixelType(1); + int imagePosition = screenCenterX - (imageWidth / 2); + // decode will return 1 on sucess and 0 on a failure + int decodeStatus = jpeg.decode(imagePosition, 0, JPEG_SCALE_HALF); + // jpeg.decode(45, 0, 0); + jpeg.close(); + Serial.print("Time taken to decode and display Image (ms): "); + Serial.println(millis() - lTime); + + return decodeStatus; + } + + void drawTouchButtons(bool backStatus, bool forwardStatus) + { + + int buttonCenterY = 75; + int leftButtonCenterX = 40; + int rightButtonCenterX = screenWidth - leftButtonCenterX; + + // Draw back Button + tft.fillCircle(leftButtonCenterX, buttonCenterY, 16, TFT_BLACK); + tft.drawCircle(leftButtonCenterX, buttonCenterY, 16, TFT_WHITE); + if (backStatus) + { + tft.fillCircle(leftButtonCenterX, buttonCenterY, 15, TFT_GREEN); } - - void drawRefreshTokenMessage() { - Serial.println("Refresh Token Mode"); - tft.fillScreen(TFT_BLACK); - tft.setTextColor(TFT_WHITE, TFT_BLACK); - tft.drawCentreString("Refresh Token Mode:", screenCenterX, 5, 2); - tft.drawString("You need to authorize this device to use", 5, 28, 2); - tft.drawString("your spotify account.", 5, 46, 2); - - tft.drawString("Visit the following address and follow", 5, 82, 2); - tft.drawString("the instrucitons:", 5, 100, 2); - tft.setTextColor(TFT_BLUE, TFT_BLACK); - tft.drawString(WiFi.localIP().toString(), 10, 128, 2); - tft.setTextColor(TFT_WHITE, TFT_BLACK); + else + { + tft.drawCircle(leftButtonCenterX, buttonCenterY, 15, TFT_WHITE); } - private: - - unsigned long touchScreenCoolDownInterval = 200; // How long after a touch press do we accept another (0.2 seconds). There is also an APi request inbetween - unsigned long touchScreenCoolDownTime; // time when cool down has expired + tft.fillTriangle(leftButtonCenterX - 4, buttonCenterY, leftButtonCenterX + 6, buttonCenterY - 10, leftButtonCenterX + 6, buttonCenterY + 10, TFT_WHITE); + tft.drawRect(leftButtonCenterX - 6, buttonCenterY - 10, 2, 20, TFT_WHITE); - int displayImageUsingFile(char *albumArtUrl) + // Draw forward Button + tft.fillCircle(rightButtonCenterX, buttonCenterY, 16, TFT_BLACK); + tft.drawCircle(rightButtonCenterX, buttonCenterY, 16, TFT_WHITE); + if (forwardStatus) { - - // In this example I reuse the same filename - // over and over, maybe saving the art using - // the album URI as the name would be better - // as you could save having to download them each - // time, but this seems to work fine. - if (SPIFFS.exists(ALBUM_ART) == true) - { - Serial.println("Removing existing image"); - SPIFFS.remove(ALBUM_ART); - } - - fs::File f = SPIFFS.open(ALBUM_ART, "w+"); - if (!f) - { - Serial.println("file open failed"); - return -1; - } - - // Spotify uses a different cert for the Image server, so we need to swap to that for the call - client.setCACert(spotify_image_server_cert); - bool gotImage = spotify_display->getImage(albumArtUrl, &f); - - // Swapping back to the main spotify cert - client.setCACert(spotify_server_cert); - - // Make sure to close the file! - f.close(); - - if (gotImage) - { - return drawImagefromFile(ALBUM_ART); - } - else - { - return -2; - } - } - - int drawImagefromFile(const char *imageFileUri) { - unsigned long lTime = millis(); - lTime = millis(); - jpeg.open((const char *) imageFileUri, myOpen, myClose, myRead, mySeek, JPEGDraw); - jpeg.setPixelType(1); - int imagePosition = screenCenterX - (imageWidth / 2); - // decode will return 1 on sucess and 0 on a failure - int decodeStatus = jpeg.decode(imagePosition, 0, JPEG_SCALE_HALF); - //jpeg.decode(45, 0, 0); - jpeg.close(); - Serial.print("Time taken to decode and display Image (ms): "); - Serial.println(millis() - lTime); - - return decodeStatus; + tft.fillCircle(rightButtonCenterX, buttonCenterY, 15, TFT_GREEN); } - - void drawTouchButtons(bool backStatus, bool forwardStatus) { - - int buttonCenterY = 75; - int leftButtonCenterX = 40; - int rightButtonCenterX = screenWidth - leftButtonCenterX; - - // Draw back Button - tft.fillCircle(leftButtonCenterX, buttonCenterY, 16, TFT_BLACK); - tft.drawCircle(leftButtonCenterX, buttonCenterY, 16, TFT_WHITE); - if (backStatus) { - tft.fillCircle(leftButtonCenterX, buttonCenterY, 15, TFT_GREEN); - } else { - tft.drawCircle(leftButtonCenterX, buttonCenterY, 15, TFT_WHITE); - } - - tft.fillTriangle(leftButtonCenterX - 4, buttonCenterY, leftButtonCenterX + 6, buttonCenterY - 10, leftButtonCenterX + 6, buttonCenterY + 10, TFT_WHITE); - tft.drawRect(leftButtonCenterX - 6, buttonCenterY - 10, 2, 20, TFT_WHITE); - - // Draw forward Button - tft.fillCircle(rightButtonCenterX, buttonCenterY, 16, TFT_BLACK); - tft.drawCircle(rightButtonCenterX, buttonCenterY, 16, TFT_WHITE); - if (forwardStatus) { - tft.fillCircle(rightButtonCenterX, buttonCenterY, 15, TFT_GREEN); - } else { - tft.drawCircle(rightButtonCenterX, buttonCenterY, 15, TFT_WHITE); - } - - - tft.fillTriangle(rightButtonCenterX + 4, buttonCenterY, rightButtonCenterX - 6, buttonCenterY - 10, rightButtonCenterX - 6, buttonCenterY + 10, TFT_WHITE); - tft.drawRect(rightButtonCenterX + 6, buttonCenterY - 10, 2, 20, TFT_WHITE); + else + { + tft.drawCircle(rightButtonCenterX, buttonCenterY, 15, TFT_WHITE); } + tft.fillTriangle(rightButtonCenterX + 4, buttonCenterY, rightButtonCenterX - 6, buttonCenterY - 10, rightButtonCenterX - 6, buttonCenterY + 10, TFT_WHITE); + tft.drawRect(rightButtonCenterX + 6, buttonCenterY - 10, 2, 20, TFT_WHITE); + } }; diff --git a/SpotifyDiyThing/nfc.h b/SpotifyDiyThing/nfc.h index 8bd1399..57f37e5 100644 --- a/SpotifyDiyThing/nfc.h +++ b/SpotifyDiyThing/nfc.h @@ -54,14 +54,18 @@ NfcAdapter nfc = NfcAdapter(pn532spi); SpotifyArduino *spotify_nfc; +SpotifyDisplay *nfc_Display; + unsigned long delayBetweenNfcReads = 200; // Time between NFC reads (.2 seconds) -unsigned long nfcDueTime; //time when NFC read is due +unsigned long nfcDueTime; // time when NFC read is due bool forceUpdate = false; -bool nfcSetup(SpotifyArduino *spotifyObj) { +bool nfcSetup(SpotifyArduino *spotifyObj, SpotifyDisplay *theDisplay) +{ spotify_nfc = spotifyObj; + nfc_Display = theDisplay; #ifdef MATRIX_DISPLAY // matrix display uses custom pins so we need to specify them @@ -69,51 +73,66 @@ bool nfcSetup(SpotifyArduino *spotifyObj) { #endif nfc.begin(); bool nfcStatus = !nfc.fail; - if (nfcStatus) { + if (nfcStatus) + { // Could while loop here if you wanted - //dma_display->print("NO!"); + // dma_display->print("NO!"); Serial.println("NFC reader - OK!"); - } else { + } + else + { Serial.println("NFC reader - not working!!!"); - //dma_display->print("OK!"); + // dma_display->print("OK!"); } return nfcStatus; } -bool handleSpotifyUrl(char *tagContent) { +bool handleSpotifyUrl(char *tagContent) +{ char body[200]; char contextUri[50]; char trackUri[50]; bool isTrack = false; - //open.spotify.com/album/47lgREYotnsiuddvu6dXlk?si=F0r50tIETo-BffEB-HSpng&utm_source=copy-link&dl_branch=1 - // Note: the "https://" is stripped by the tag - if (strncmp(tagContent, "open.spotify.com/album/", 23) == 0) { + // open.spotify.com/album/47lgREYotnsiuddvu6dXlk?si=F0r50tIETo-BffEB-HSpng&utm_source=copy-link&dl_branch=1 + // Note: the "https://" is stripped by the tag + if (strncmp(tagContent, "open.spotify.com/album/", 23) == 0) + { sprintf(contextUri, "spotify:album:%.*s", 22, tagContent + 23); // 22 is length of code, 23 is offset to get to the code (47lgREYotnsiuddvu6dXlk in above example) - } else if (strncmp(tagContent, "open.spotify.com/playlist/", 26) == 0) { + } + else if (strncmp(tagContent, "open.spotify.com/playlist/", 26) == 0) + { sprintf(contextUri, "spotify:playlist:%.*s", 22, tagContent + 26); - } else if (strncmp(tagContent, "open.spotify.com/track/", 23) == 0) { + } + else if (strncmp(tagContent, "open.spotify.com/track/", 23) == 0) + { isTrack = true; sprintf(trackUri, "spotify:track:%.*s", 22, tagContent + 23); - } else { + } + else + { Serial.print("Unknown URL: "); Serial.println(tagContent); return false; } - if (isTrack) { + if (isTrack) + { Serial.print("track: "); Serial.println(tagContent); sprintf(body, "{\"uris\" : [\"%s\"]}", trackUri); - } else { + } + else + { Serial.print("context: "); Serial.println(tagContent); sprintf(body, "{\"context_uri\" : \"%s\"}", contextUri); } - if (spotify_nfc->playAdvanced(body)) { + if (spotify_nfc->playAdvanced(body)) + { Serial.println("done!"); forceUpdate = true; // force it to update return true; @@ -122,18 +141,20 @@ bool handleSpotifyUrl(char *tagContent) { return false; } -bool handleSpotifyUri(char *tagContent) { +bool handleSpotifyUri(char *tagContent) +{ char body[200]; // First let's check if we have a comma char *commaLocation = NULL; - commaLocation = strchr (tagContent, ','); + commaLocation = strchr(tagContent, ','); - if (commaLocation != NULL) { + if (commaLocation != NULL) + { // We have a comma, this means its a track with a context. uint8_t lengthOfString = strlen(tagContent); - uint8_t contextIndex = commaLocation - tagContent + 1; //don't want the comma + uint8_t contextIndex = commaLocation - tagContent + 1; // don't want the comma uint8_t contextLength = lengthOfString - contextIndex; char context[contextLength + 1]; strncpy(context, commaLocation + 1, contextLength); @@ -151,22 +172,27 @@ bool handleSpotifyUri(char *tagContent) { Serial.println(track); sprintf(body, "{\"context_uri\" : \"%s\", \"offset\": {\"uri\": \"%s\"}}", context, track); - - } else { + } + else + { char *isTrack = NULL; - isTrack = strstr (tagContent, "track"); - if (isTrack) { + isTrack = strstr(tagContent, "track"); + if (isTrack) + { Serial.print("track: "); Serial.println(tagContent); sprintf(body, "{\"uris\" : [\"%s\"]}", tagContent); - } else { + } + else + { Serial.print("context: "); Serial.println(tagContent); sprintf(body, "{\"context_uri\" : \"%s\"}", tagContent); } } - if (spotify_nfc->playAdvanced(body)) { + if (spotify_nfc->playAdvanced(body)) + { Serial.println("done!"); forceUpdate = true; // force it to update return true; @@ -175,69 +201,77 @@ bool handleSpotifyUri(char *tagContent) { return false; } -bool updateSpotify(char *tagContent) { +bool updateSpotify(char *tagContent) +{ - if (strncmp(tagContent + 1, "open.spotify.com", 16) == 0) { // The +1 is cause first charcter indicated protocol (I think), not needed anyways + if (strncmp(tagContent + 1, "open.spotify.com", 16) == 0) + { // The +1 is cause first charcter indicated protocol (I think), not needed anyways return handleSpotifyUrl(tagContent + 1); - } else if (strncmp(tagContent + 8, "open.spotify.com", 16) == 0) { // In case it's written as plain text, skipping the "https://" + } + else if (strncmp(tagContent + 8, "open.spotify.com", 16) == 0) + { // In case it's written as plain text, skipping the "https://" return handleSpotifyUrl(tagContent + 8); - } else if (strncmp(tagContent, "spotify:", 8) == 0) { + } + else if (strncmp(tagContent, "spotify:", 8) == 0) + { // Probably in the format: spotify:track:4mCsFkDzm6z8j0glKdE164 return handleSpotifyUri(tagContent); } // Not reconginized format. // Should maybe flash a square or something - //refreshArt = true; + // refreshArt = true; return false; } -void markDisplayAsTagRead() { - //dma_display->drawRect(1, 1, dma_display->width() - 2, dma_display->height() - 2, dma_display->color444(0, 0, 255)); - //dma_display->drawRect(2, 2, dma_display->width() - 4, dma_display->height() - 4, dma_display->color444(255, 0, 0)); -} - -void markDisplayAsTagWritten() { - //dma_display->drawRect(1, 1, dma_display->width() - 2, dma_display->height() - 2, dma_display->color444(255, 0, 255)); - //dma_display->drawRect(2, 2, dma_display->width() - 4, dma_display->height() - 4, dma_display->color444(0, 255, 0)); -} - -bool handleTag() { +bool handleTag(const char *trackUri) +{ NfcTag tag = nfc.read(); bool writeTag = false; bool formatTag = false; Serial.println(tag.getTagType()); - Serial.print("UID: "); Serial.println(tag.getUidString()); + Serial.print("UID: "); + Serial.println(tag.getUidString()); - if (!tag.isFormatted) { + if (!tag.isFormatted) + { writeTag = true; formatTag = true; - } else if (tag.hasNdefMessage()) { // every tag won't have a message + } + else if (tag.hasNdefMessage()) + { // every tag won't have a message NdefMessage message = tag.getNdefMessage(); Serial.print("\nThis NFC Tag contains an NDEF Message with "); Serial.print(message.getRecordCount()); Serial.print(" NDEF Record"); - if (message.getRecordCount() != 1) { + if (message.getRecordCount() != 1) + { Serial.print("s"); } Serial.println("."); // cycle through the records, printing some info from each int recordCount = message.getRecordCount(); - if (recordCount > 0) { - for (int i = 0; i < recordCount; i++) { - Serial.print("\nNDEF Record "); Serial.println(i + 1); + if (recordCount > 0) + { + for (int i = 0; i < recordCount; i++) + { + Serial.print("\nNDEF Record "); + Serial.println(i + 1); NdefRecord record = message.getRecord(i); - Serial.print(" TNF: "); Serial.println(record.getTnf()); - Serial.print(" Type: "); Serial.println(record.getType()); // will be "" for TNF_EMPTY + Serial.print(" TNF: "); + Serial.println(record.getTnf()); + Serial.print(" Type: "); + Serial.println(record.getType()); // will be "" for TNF_EMPTY // The TNF and Type should be used to determine how your application processes the payload // There's no generic processing for the payload, it's returned as a byte[] int payloadLength = record.getPayloadLength(); - if (payloadLength > 0) { + if (payloadLength > 0) + { byte payload[payloadLength]; record.getPayload(payload); @@ -247,94 +281,118 @@ bool handleTag() { // id is probably blank and will return "" String uid = record.getId(); - if (uid != "") { - Serial.print(" ID: "); Serial.println(uid); + if (uid != "") + { + Serial.print(" ID: "); + Serial.println(uid); } // Force the data into a String (might work depending on the content) // Real code should use smarter processing char payloadAsString[payloadLength + 1]; int numChars = 0; - for (int c = 0; c < payloadLength; c++) { - if ((char)payload[c] != '\0') { + for (int c = 0; c < payloadLength; c++) + { + if ((char)payload[c] != '\0') + { payloadAsString[numChars] = (char)payload[c]; numChars++; } } payloadAsString[numChars] = '\0'; - markDisplayAsTagRead(); - //refreshArt = true; // update the art to remove the mark, even if the art doesnt change. + nfc_Display->markDisplayAsTagRead(); + // refreshArt = true; // update the art to remove the mark, even if the art doesnt change. Serial.print(" Payload (String): "); Serial.println(payloadAsString); return updateSpotify(payloadAsString); - } else { - //At least one of the records we had was not valid + } + else + { + // At least one of the records we had was not valid writeTag = true; } } - } else { - //Card has no records + } + else + { + // Card has no records writeTag = true; } } - if (formatTag && nfc.tagPresent()) { + if (formatTag && nfc.tagPresent()) + { bool success = nfc.format(); - if (success) { + if (success) + { SERIAL.println("\nSuccess, tag formatted as NDEF."); - } else { + } + else + { SERIAL.println("\nFormat failed."); } delay(100); } - if (writeTag && nfc.tagPresent()) { - Serial.println("Would write now"); -// -// NdefMessage message = NdefMessage(); -// //This seems to be a blank card, lets write to it -// NdefRecord r = NdefRecord(); -// r.setTnf(TNF_WELL_KNOWN); -// -// String mimeType = "U"; -// byte type[mimeType.length() + 1]; -// mimeType.getBytes(type, sizeof(type)); -// r.setType(type, mimeType.length()); -// -// // One for new line, one for the 0x00 needed at the start -// byte payloadBytes[currentTrackUri.length() + 2]; -// //Write to the new buffer offset by one -// currentTrackUri.getBytes(&payloadBytes[1], currentTrackUri.length() + 1); -// payloadBytes[0] = 0; -// -// r.setPayload(payloadBytes, currentTrackUri.length() + 1); -// -// message.addRecord(r); -// boolean success = nfc.write(message); -// if (success) { -// markDisplayAsTagWritten(); -// Serial.println("Success. Try reading this tag with your phone."); -// } else { -// Serial.println("Write failed"); -// } -// -// return true; + if (writeTag && nfc.tagPresent()) + { + Serial.println("Writing track"); + + int trackLength = strlen(trackUri); + + NdefMessage message = NdefMessage(); + // This seems to be a blank card, lets write to it + NdefRecord r = NdefRecord(); + r.setTnf(TNF_WELL_KNOWN); + + String mimeType = "U"; + byte type[mimeType.length() + 1]; + mimeType.getBytes(type, sizeof(type)); + r.setType(type, mimeType.length()); + + // One for new line, one for the 0x00 needed at the start + byte payloadBytes[trackLength + 2]; + // Write to the new buffer offset by one + // currentTrackUri.getBytes(&payloadBytes[1], trackLength + 1); + memcpy(payloadBytes + sizeof(byte), trackUri, trackLength + 1); + payloadBytes[0] = 0; + + r.setPayload(payloadBytes, trackLength + 1); + + message.addRecord(r); + boolean success = nfc.write(message); + if (success) + { + nfc_Display->markDisplayAsTagWritten(); + Serial.println("Success. Try reading this tag with your phone."); + forceUpdate = true; // To remove the squares + } + else + { + Serial.println("Write failed"); + } + + return true; } return false; } -bool nfcLoop() { +bool nfcLoop(const char *trackUri) +{ forceUpdate = false; if (millis() > nfcDueTime) { - if (nfc.tagPresent() && handleTag()) { - Serial.println("Succesful Read - Back to loop:"); + if (nfc.tagPresent() && handleTag(trackUri)) + { + Serial.println("Successful Read - Back to loop:"); nfcDueTime = millis() + 5000; // 5 second cool down on NFC tag if succesful - } else { + } + else + { nfcDueTime = millis() + delayBetweenNfcReads; - //Serial.println("Failed - Back to loop:"); + // Serial.println("Failed - Back to loop:"); } } return forceUpdate; diff --git a/SpotifyDiyThing/spotifyLogic.h b/SpotifyDiyThing/spotifyLogic.h index 042ad52..e6e8f15 100644 --- a/SpotifyDiyThing/spotifyLogic.h +++ b/SpotifyDiyThing/spotifyLogic.h @@ -1,38 +1,41 @@ - -// so we can compare and not download the same image if we already have it. -String lastAlbumArtUrl; - -String lastTrackUri; - -SpotifyDisplay* sp_Display; - -// so we can store the song name and artist name -char *songName; -char *songArtist; +SpotifyDisplay *sp_Display; SpotifyArduino spotify(client, NULL, NULL); -boolean albumArtChanged = false; +bool albumArtChanged = false; long songStartMillis; long songDuration; +char lastTrackUri[200]; + // You might want to make this much smaller, so it will update responsively unsigned long delayBetweenRequests = 5000; // Time between requests (5 seconds) -unsigned long requestDueTime; // time when request due +unsigned long requestDueTime; // time when request due unsigned long delayBetweenProgressUpdates = 500; // Time between requests (0.5 seconds) -unsigned long progressDueTime; // time when request due +unsigned long progressDueTime; // time when request due -void spotifySetup(SpotifyDisplay* theDisplay, const char *clientId, const char *clientSecret) { +void spotifySetup(SpotifyDisplay *theDisplay, const char *clientId, const char *clientSecret) +{ sp_Display = theDisplay; client.setCACert(spotify_server_cert); spotify.lateInit(clientId, clientSecret); +} + +bool isSameTrack(const char *trackUri) +{ + return strcmp(lastTrackUri, trackUri) == 0; +} +void setTrackUri(const char *trackUri) +{ + strcpy(lastTrackUri, trackUri); } -void spotifyRefreshToken(const char *refreshToken) { +void spotifyRefreshToken(const char *refreshToken) +{ spotify.setRefreshToken(refreshToken); // If you want to enable some extra debugging @@ -45,38 +48,44 @@ void spotifyRefreshToken(const char *refreshToken) { } } -void handleCurrentlyPlaying(CurrentlyPlaying currentlyPlaying) { +void handleCurrentlyPlaying(CurrentlyPlaying currentlyPlaying) +{ - //printCurrentlyPlayingToSerial(currentlyPlaying); + // printCurrentlyPlayingToSerial(currentlyPlaying); - String newTrackUri = String(currentlyPlaying.trackUri); - if (newTrackUri != lastTrackUri) { - lastTrackUri = newTrackUri; + if (!isSameTrack(currentlyPlaying.trackUri)) + { + setTrackUri(currentlyPlaying.trackUri); // We have a new Song, need to update the text sp_Display->printCurrentlyPlayingToScreen(currentlyPlaying); - } albumArtChanged = sp_Display->processImageInfo(currentlyPlaying); sp_Display->displayTrackProgress(currentlyPlaying.progressMs, currentlyPlaying.durationMs); - if (currentlyPlaying.isPlaying) { + if (currentlyPlaying.isPlaying) + { // If we know at what millis the song started at, we can make a good guess // at updating the progress bar more often than checking the API songStartMillis = millis() - currentlyPlaying.progressMs; songDuration = currentlyPlaying.durationMs; - } else { + } + else + { // Song doesn't seem to be playing, do not update the progress songStartMillis = 0; } } -void updateProgressBar() { - if (songStartMillis != 0 && millis() > progressDueTime) { +void updateProgressBar() +{ + if (songStartMillis != 0 && millis() > progressDueTime) + { long songProgress = millis() - songStartMillis; - if (songProgress > songDuration) { + if (songProgress > songDuration) + { songProgress = songDuration; } sp_Display->displayTrackProgress(songProgress, songDuration); @@ -84,11 +93,16 @@ void updateProgressBar() { } } -void updateCurrentlyPlaying(boolean forceUpdate) { +void updateCurrentlyPlaying(boolean forceUpdate) +{ if (forceUpdate || millis() > requestDueTime) { - //Serial.print("Free Heap: "); - //Serial.println(ESP.getFreeHeap()); + if (forceUpdate) + { + Serial.println("forcing an update"); + } + // Serial.print("Free Heap: "); + // Serial.println(ESP.getFreeHeap()); Serial.println("getting currently playing song:"); // Check if music is playing currently on the account. @@ -96,7 +110,7 @@ void updateCurrentlyPlaying(boolean forceUpdate) { if (status == 200) { Serial.println("Successfully got currently playing"); - if (albumArtChanged) + if (albumArtChanged || forceUpdate) { sp_Display->clearImage(); int displayImageResult = sp_Display->displayImage();