/ Feather_ePaper_Quotes / adafruit_feather_quote / adafruit_feather_quote.ino
adafruit_feather_quote.ino
  1  // SPDX-FileCopyrightText: 2019 Dan Cogliano for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  
  5  /***************************************************
  6   * Quote Display for Adafruit ePaper FeatherWings
  7   * For use with Adafruit tricolor and monochrome ePaper FeatherWings
  8   * 
  9   * Adafruit invests time and resources providing this open source code.
 10   * Please support Adafruit and open source hardware by purchasing
 11   * products from Adafruit!
 12   * 
 13   * Written by Dan Cogliano for Adafruit Industries
 14   * Copyright (c) 2019 Adafruit Industries
 15   * 
 16   * Notes: 
 17   * Update the secrets.h file with your WiFi details
 18   * Uncomment the ePaper display type you are using below.
 19   * Change the SLEEP setting to define the time between quotes
 20   */
 21   
 22  #include <Adafruit_GFX.h>    // Core graphics library
 23  #include <HTTPClient.h>
 24  #include <ArduinoJson.h>     //https://github.com/bblanchon/ArduinoJson
 25  #include <Adafruit_EPD.h>
 26  #include "secrets.h"
 27  
 28  // define the # of seconds to sleep before waking up and getting a new quote
 29  #define SLEEP 3600 // 1 hour in seconds
 30  
 31  // WiFi timeout in seconds
 32  #define WIFI_TIMEOUT 30
 33  
 34  // What fonts do you want to use?
 35  #include <Fonts/FreeSans9pt7b.h>
 36  //#include <Fonts/FreeSans12pt7b.h> // a font option for larger screens
 37  
 38  // qfont is the font for the quote
 39  const GFXfont *qfont = &FreeSans9pt7b;
 40  //const GFXfont *qfont = &FreeSans12pt7b;
 41  
 42  // afont is the font for the author's name
 43  const GFXfont *afont = &FreeSans9pt7b;
 44  //const GFXfont *afont = &FreeSans12pt7b;
 45  
 46  // ofont is the font for the origin of the feed
 47  const GFXfont *ofont = NULL;
 48  
 49  // font to use if qfont is too large
 50  // NULL means use the system font (which is small)
 51  const GFXfont *smallfont = NULL;
 52  
 53  
 54  // ESP32 settings
 55  #define SD_CS       14
 56  #define SRAM_CS     32
 57  #define EPD_CS      15
 58  #define EPD_DC      33 
 59  #define LEDPIN      13
 60  #define LEDPINON    HIGH
 61  #define LEDPINOFF   LOW 
 62  
 63  #define EPD_RESET   -1 // can set to -1 and share with microcontroller Reset!
 64  #define EPD_BUSY    -1 // can set to -1 to not use a pin (will wait a fixed delay)
 65  
 66  // Uncomment the following line if you are using 2.13" tricolor 212*104 EPD
 67  //Adafruit_IL0373 epd(212, 104 ,EPD_DC, EPD_RESET, EPD_CS, SRAM_CS, EPD_BUSY);
 68  // Uncomment the following line if you are using 2.13" monochrome 250*122 EPD
 69  Adafruit_SSD1675 epd(250, 122, EPD_DC, EPD_RESET, EPD_CS, SRAM_CS, EPD_BUSY);
 70  
 71  // get string length in pixels
 72  // set text font prior to calling this
 73  int getStringLength(const char *str, int strlength = 0)
 74  {
 75    char buff[1024];
 76    int16_t x, y;
 77    uint16_t w, h;
 78    if(strlength == 0)
 79    {
 80      strcpy(buff, str);
 81    }
 82    else
 83    {
 84      strncpy(buff, str, strlength);
 85      buff[strlength] = '\0';
 86    }
 87    epd.getTextBounds(buff, 0, 0, &x, &y, &w, &h);
 88    return(w);  
 89  }
 90  
 91  // word wrap routine
 92  // first time send string to wrap
 93  // 2nd and additional times: use empty string
 94  // returns substring of wrapped text.
 95  char *wrapWord(const char *str, int linesize)
 96  {
 97    static char buff[1024];
 98    int linestart = 0;
 99    static int lineend = 0;
100    static int bufflen = 0;
101    if(strlen(str) == 0)
102    {
103      // additional line from original string
104      linestart = lineend + 1;
105      lineend = bufflen;
106      Serial.println("existing string to wrap, starting at position " + String(linestart) + ": " + String(&buff[linestart]));
107    }
108    else
109    {
110      Serial.println("new string to wrap: " + String(str));
111      memset(buff,0,sizeof(buff));
112      // new string to wrap
113      linestart = 0;
114      strcpy(buff,str);
115      lineend = strlen(buff);
116      bufflen = strlen(buff);
117    }
118    uint16_t w;
119    int lastwordpos = linestart;
120    int wordpos = linestart + 1;
121    while(true)
122    {
123      while(buff[wordpos] == ' ' && wordpos < bufflen)
124        wordpos++;
125      while(buff[wordpos] != ' ' && wordpos < bufflen)
126        wordpos++;
127      if(wordpos < bufflen)
128        buff[wordpos] = '\0';
129      w = getStringLength(&buff[linestart]);
130      if(wordpos < bufflen)
131        buff[wordpos] = ' ';
132      if(w > linesize)
133      {
134        buff[lastwordpos] = '\0';
135        lineend = lastwordpos;
136        return &buff[linestart];
137      }
138      else if(wordpos >= bufflen)
139      {
140        // first word too long or end of string, send it anyway
141        buff[wordpos] = '\0';
142        lineend = wordpos;
143        return &buff[linestart];        
144      }
145      lastwordpos = wordpos;
146      wordpos++;
147    }
148  }
149  
150  // return # of lines created from word wrap
151  int getLineCount(const char *str, int scrwidth)
152  {
153    int linecount = 0;
154    String line = wrapWord(str,scrwidth);
155  
156    while(line.length() > 0)
157    {
158      linecount++;
159      line = wrapWord("",scrwidth);
160    }
161    return linecount;  
162  }
163  
164  int getLineHeight(const GFXfont *font = NULL)
165  {
166    int height;
167    if(font == NULL)
168    {
169      height = 12;
170    }
171    else
172    {
173      height = (uint8_t)pgm_read_byte(&font->yAdvance);
174    }
175    return height;
176  }
177  
178  // Retrieve page response from given URL
179  String getURLResponse(String url)
180  {
181    HTTPClient http;
182    String jsonstring = "";
183    Serial.println("getting url: " + url);
184    if(http.begin(url))
185    {
186      Serial.print("[HTTP] GET...\n");
187      // start connection and send HTTP header
188      int httpCode = http.GET();
189  
190      // httpCode will be negative on error
191      if (httpCode > 0) {
192        // HTTP header has been sent and Server response header has been handled
193        Serial.println("[HTTP] GET... code: " + String(httpCode));
194  
195        // file found at server
196        if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
197        jsonstring = http.getString();
198        // use this string for testing very long quotes
199        //jsonstring = "[{\"text\":\"Don't worry about what anybody else is going to do… The best way to predict the future is to invent it. Really smart people with reasonable funding can do just about anything that doesn't violate too many of Newton's Laws!\",\"author\":\"Alan Kay\"}]";
200        Serial.println(jsonstring);
201      }
202      } else {
203        Serial.println("[HTTP] GET... failed, error: " + http.errorToString(httpCode));
204      }
205      http.end();
206    }
207    else {
208      Serial.println("[HTTP] Unable to connect");
209    }
210    return jsonstring;
211  }
212  
213  void getQuote(String &quote, String &author)
214  {
215    StaticJsonDocument<1024> doc;
216    String url = "https://www.adafruit.com/api/quotes.php";
217    String jsonquote = getURLResponse(url);
218    if(jsonquote.length() > 0)
219    {
220      // remove start and end brackets, jsonBuffer is confused by them
221      jsonquote = jsonquote.substring(1,jsonquote.length()-1);
222      Serial.println("using: " + jsonquote);
223      DeserializationError error = deserializeJson(doc, jsonquote);
224      if (error) 
225      {
226        Serial.println("json parseObject() failed");
227        Serial.println("bad json: " + jsonquote);
228        quote = "json parseObject() failed";
229      }
230      else
231      {
232        String tquote = doc["text"];
233        String tauthor = doc["author"];
234        quote = tquote;
235        author = tauthor;
236      }
237    }
238    else
239    {
240      quote = "Error retrieving URL";
241    }
242  }
243  
244  void printQuote(String &quote)
245  {
246    int x = 0;
247    int y = 0;
248    bool bsmallfont = false;
249    epd.setTextColor(EPD_BLACK);
250    epd.setFont(qfont);
251    epd.setTextSize(1);
252  
253    int scrwidth = epd.width() - 8;
254    Serial.println("Screen width is " + String(scrwidth));
255    Serial.println("Screen height is " + String(epd.height()));
256    int linecount = getLineCount(quote.c_str(),scrwidth);
257    int lineheightquote = getLineHeight(qfont);
258    int lineheightauthor = getLineHeight(afont);
259    int lineheightother = getLineHeight(ofont);
260    int maxlines = (epd.height() - (lineheightauthor + lineheightother)) / lineheightquote;
261    Serial.println("maxlines is " + String(maxlines));
262    Serial.println("line height is " +String(lineheightquote));
263    Serial.println("linecount is " +String(linecount));
264    int topmargin = 0;
265    if(linecount > maxlines)
266    {
267      // too long for default font size
268      // next attempt, reduce lineheight to .8 size
269      lineheightquote = .8 * lineheightquote;
270      maxlines = (epd.height() - (lineheightauthor + lineheightother)) / lineheightquote;
271      if(linecount > maxlines)
272      {
273        // next attempt, use small font
274        epd.setFont(smallfont);
275        bsmallfont = true;
276        epd.setTextSize(1);
277        lineheightquote = getLineHeight(smallfont);
278        maxlines = (epd.height() - (lineheightauthor + lineheightother)) / lineheightquote;
279        linecount = getLineCount(quote.c_str(),scrwidth);
280        if(linecount > maxlines)
281        {
282          // final attempt, last resort is to reduce the lineheight to make it fit
283          lineheightquote = (epd.height() - (lineheightauthor + lineheightother)) / linecount;
284        }
285      }
286      Serial.println("maxlines has changed to " + String(maxlines));
287      Serial.println("line height has changed to " +String(lineheightquote));
288      Serial.println("linecount has changed to " +String(linecount));
289    }
290    if(linecount <= maxlines)
291    {
292  
293      topmargin = (epd.height() - (lineheightauthor + lineheightother) - linecount*lineheightquote)/2;
294      if(!bsmallfont)
295        topmargin+=lineheightquote-4;
296      //Serial.println("topmargin = " + String(topmargin));
297    }
298    String line = wrapWord(quote.c_str(),scrwidth);
299  
300    int counter = 0;
301    epd.setTextColor(EPD_BLACK);
302    while(line.length() > 0)
303    {
304      counter++;
305      Serial.println("printing line " + String(counter) + ": '" + line + String("'"));
306      epd.setCursor(x +4, y + topmargin);
307      epd.print(line);
308      y += lineheightquote;
309      line = wrapWord("",scrwidth);
310    }
311  }
312  
313  void printAuthor(String author)
314  {
315    epd.setTextColor(EPD_BLACK);
316    epd.setFont(afont);
317    int lineheightauthor = getLineHeight(afont);
318    int lineheightother = getLineHeight(ofont);
319    int x = getStringLength(author.c_str());
320    // draw line above author
321    epd.drawLine(epd.width() - x - 10, epd.height() - (lineheightauthor + lineheightother) + 2, epd.width(), epd.height() - (lineheightauthor + lineheightother) + 2, EPD_RED);
322    epd.drawLine(epd.width() - x - 10, epd.height() - (lineheightauthor + lineheightother) + 2, epd.width() - x - 10,epd.height() - lineheightother - lineheightauthor/3, EPD_RED);
323    epd.drawLine(0, epd.height() - lineheightother - lineheightauthor/3, epd.width() - x - 10,epd.height() - lineheightother - lineheightauthor/3, EPD_RED);
324    // draw author text
325    int cursorx = epd.width() - x - 4;
326    int cursory = epd.height() - lineheightother - 2;
327    if(afont == NULL)
328    {
329      cursory = epd.height() - lineheightother - lineheightauthor - 2 ;
330    }
331    epd.setCursor(cursorx, cursory);
332    epd.print(author);
333  }
334  
335  void printOther(String other)
336  {
337    epd.setFont(ofont);
338    int lineheightother = getLineHeight(ofont);
339    int ypos = epd.height() - 2;
340    if (ofont == NULL)
341      ypos = epd.height() - (lineheightother - 2);
342    epd.setTextColor(EPD_BLACK);
343    epd.setCursor(4,ypos);
344    epd.print(other);
345  }
346  
347  void setup() {
348    Serial.begin(115200);
349    //while(!Serial);
350  
351    pinMode(LEDPIN, OUTPUT);
352    digitalWrite(LEDPIN, LEDPINON);
353  
354    epd.begin();
355    Serial.println("ePaper display initialized");
356    epd.clearBuffer();
357    epd.setTextWrap(false);
358  
359    Serial.print("Connecting to WiFi ");
360    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
361    int counter = 0;
362    while (WiFi.status() != WL_CONNECTED && counter < WIFI_TIMEOUT) {
363      delay(1000);
364      Serial.print(".");
365      counter++;
366    }
367    
368    String quote, author;
369    if(WiFi.status() == WL_CONNECTED)
370    {
371      Serial.println("connected");
372      getQuote(quote, author);  
373    }
374    else
375    {
376      quote = "WiFi connection timed out, try again later";
377      Serial.println(quote);
378    }
379    
380    printQuote(String("\"") + quote + String("\""));
381    printAuthor(author);
382    printOther("adafruit.com/quotes");
383    
384    epd.display();
385    Serial.println("done, going to sleep...");
386    // power down ePaper display
387    epd.powerDown();
388    // put microcontroller to sleep, wake up after specified time
389    ESP.deepSleep(SLEEP * 1e6);
390  }
391  
392  void loop() {
393    // should never get here, setup() puts the CPU to sleep
394  }