// GPS Clock // A clock with GPS accuracy and a touch screen for configuration. // // Arduino UNO, OpenSmart TFT Touch Screen 320x240 shield with LM75 temperature // sensor and NEO 6M GPS Receiver configured for RMC message at 1s interval at 4800bd. // // Note: If the GPS module only supports 3.3V logic (such as the common NEO-6M module, // despite the suppliers' claims) then it can be connected as transmit-only. The UNO // will handle the 3.3V input logic signals correctly. If it is necesssary to send to // the device (for instance, to adjust the baud rate) then a logic level converter must // be used. // #include #include #include SoftwareSerial SerialGPS(3, 2); //Initialize the GPS serial port (UNO Rx,Tx) #include "src/LM75.h" LM75 sensor; // initialize an LM75 object #include "src/MCUFRIEND_kbv.h" #include "src/FreeMonoBold18pt7b.h" // (https://tchapi.github.io/Adafruit-GFX-Font-Customiser/) // (https://rop.nl/truetype2gfx/) MCUFRIEND_kbv tft; // initialize a TFT object #include "src/TouchScreen.h" // copy-paste results from TouchScreen_Calibr_native.ino // (and also landscape orientation, below). const int XP = 6, XM = A2, YP = A1, YM = 7; //240x320 ID=0x9341 const int TS_LEFT = 930, TS_RT = 150, TS_TOP = 950, TS_BOT = 140; TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300); //***********************************************// // The control pins for the LCD can be assigned to any digital or // analog pins...but we'll use the analog pins as this allows us to // also use them for other purposes (they are reset whenever the // Touch detect routine is invoked) //----------------------------------------| // TFT Shield (Red) -- Arduino UNO / Mega2560 / OPEN-SMART UNO // GND -- GND // 5V -- 5V // CS -- A3 // RS -- A2 // WR -- A1 // RD -- A0 // RST -- RESET // DB0 -- 8 // DB1 -- 9 // DB2 -- 10 // DB3 -- 11 // DB4 -- 4 // DB5 -- 13 // DB6 -- 6 // DB7 -- 7 // SDA -- A4 (18) // SCL -- A5 (19) // NC -- 0 // NC -- 1 // NC -- 2 // NC -- 3 // Assign human-readable names to some common 16-bit color values: #define BLACK 0x0000 #define BLUE 0x001F #define RED 0xF800 #define GREEN 0x07E0 #define CYAN 0x07FF #define MAGENTA 0xF81F #define YELLOW 0xFFE0 #define WHITE 0xFFFF #define OLIVE 0x7BE0 #define GREY 0xC618 #define YELLOW 0xFFE0 #define ORANGE 0xFD20 // LCD Type ID uint16_t g_identifier; // Touch int pixel_x, pixel_y; // Touch point. #define MINPRESSURE 200 // To avoid false triggering. #define MAXPRESSURE 1000 // PSRF Defines SiRFStar III // #100 '$PSRF100',Mode(0=NMEA),baud,data,stop,parity,Checksum,'*", // #define PGPS_SET_BAUD_115200 "$PSRF100,1,115200,8,1,0*21" // 115200 bps, 81n, NMEA // #define PGPS_SET_BAUD_57600 "$PSRF100,1,57600,8,1,0*36" // 57600 bps, 81n, NMEA // #define PGPS_SET_BAUD_38400 "$PSRF100,1,38400,8,1,0*3D" // 38400 bps, 81n, NMEA // #define PGPS_SET_BAUD_19200 "$PSRF100,1,19200,8,1,0*1C" // 19200 bps, 81n, NMEA // #define PGPS_SET_BAUD_9600 "$PSRF100,1,9600,8,1,0*29" // 9600 bps, 81n, NMEA // #define PGPS_SET_BAUD_4800 "$PSRF100,1,4800,8,1,02A*" // 4800 bps, 81n, NMEA **DEFAULT // #define PGPS_SET_BAUD_2400 "$PSRF100,1,2400,8,1,0*20" // 2400 bps, 81n, NMEA // #103 '$PSRF103',Msg Id,Mode(0=set),s(0=Off),Checksum,'*' // #define PGPS_SET_NMEA_UPDATE_GGA_OFF "$PSRF103,00,00,00,01*24" // OFF // #define PGPS_SET_NMEA_UPDATE_GLL_OFF "$PSRF103,01,00,00,01*25" // // #define PGPS_SET_NMEA_UPDATE_GSA_OFF "$PSRF103,02,00,00,01*26" // // #define PGPS_SET_NMEA_UPDATE_GSV_OFF "$PSRF103,03,00,00,01*27" // // #define PGPS_SET_NMEA_UPDATE_RMC_OFF "$PSRF103,04,00,00,01*20" // // #define PGPS_SET_NMEA_UPDATE_VTG_OFF "$PSRF103,05,00,00,01*21" // // #define PGPS_SET_NMEA_UPDATE_GGA_1s "$PSRF103,00,00,01,01*25" // 1s (minimum for SiRF) // #define PGPS_SET_NMEA_UPDATE_GLL_1s "$PSRF103,01,00,01,01*24" // // #define PGPS_SET_NMEA_UPDATE_GSA_1s "$PSRF103,02,00,01,01*27" // // #define PGPS_SET_NMEA_UPDATE_GSV_1s "$PSRF103,03,00,01,01*26" // // #define PGPS_SET_NMEA_UPDATE_RMC_1s "$PSRF103,04,00,01,01*21" // // #define PGPS_SET_NMEA_UPDATE_VTG_1s "$PSRF103,05,00,01,01*20" // // #define PGPS_SET_NMEA_UPDATE_GGA_2s "$PSRF103,00,00,02,01*26" // 2s // #define PGPS_SET_NMEA_UPDATE_GLL_2s "$PSRF103,01,00,02,01*27" // // #define PGPS_SET_NMEA_UPDATE_GSA_2s "$PSRF103,02,00,02,01*24" // // #define PGPS_SET_NMEA_UPDATE_GSV_2s "$PSRF103,03,00,02,01*25" // // #define PGPS_SET_NMEA_UPDATE_RMC_2s "$PSRF103,04,00,02,01*22" // // #define PGPS_SET_NMEA_UPDATE_VTG_2s "$PSRF103,05,00,02,01*23" // // etc. // User variables int timeZone; // Timezone (offset from UTC). bool displayMode; // true=24hour, false=12hour. void setup(void) { //Serial.begin(115200); // Enable for console logging. //Serial.println(F("TFT LCD CLOCK")); // Note: initialize at 4800 (default is documented as 9600??). SerialGPS.begin(4800); // Alternative serial port startup if adjustment for a different default is required. // This may not work for baud rates above 57600 // gps.begin(xxxx) // default rate // Set GPS baud rate to 4800 / NMEA // gps.println(PGPS_SET_BAUD_4800); // 9600 bps, 81n, NMEA // delay(200); // gps.end(); // Restart port at 4800 // gps.begin(4800); // delay(200); // Set message select and update rate SerialGPS.println("$PSRF103,00,00,00,01*24"); // GGA OFF SerialGPS.println("$PSRF103,01,00,00,01*25"); // GLL OFF SerialGPS.println("$PSRF103,02,00,00,01*26"); // GSA OFF SerialGPS.println("$PSRF103,03,00,00,01*27"); // GSV OFF SerialGPS.println("$PSRF103,04,00,01,01*21"); // RMC ON 1s SerialGPS.println("$PSRF103,05,00,00,01*21"); // VTG OFF Wire.begin(); g_identifier = tft.readID(); if (g_identifier == 0x00D3 || g_identifier == 0xD3D3) g_identifier = 0x9481; // write-only shield if (g_identifier == 0xFFFF) g_identifier = 0x9341; // serial tft.begin(g_identifier); EEPROM.get(0, timeZone); EEPROM.get(2, displayMode); if ((timeZone < -12) || (timeZone > 12)) timeZone = 0; tft.setRotation(1); // Landscape. // startPage(); // Optional starting message. // delay(200); screenInit(); // Wait for the start of a GPS message while (SerialGPS.available() > 0 ) { if (SerialGPS.peek() == '$') break; SerialGPS.read(); //Serial.print("."); } } const char months[][10] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; const int monthDays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; const char test[] = "$GPRMC"; // Internal Variables bool refresh = true; // Draw screen from scratch. bool isDataValid = false; // True if GPS is sending. int dayRoll = 0; // Previous/Next day indicator. char buff[100]; // GPS Sentence. String SPrint; // HMS print string. int p; // Receive buffer pointer. long millisNext = 0; // Update timing. int prevHr = -1; // Previous value int Hr = 0; // 0 to 23 int localHr = Hr + timeZone; // 0 to 23 int prevMi = -1; int Mi = 0; // 0 to 59 int prevSe = -1; int Se = 0; // 0 to 59 int prevDa = -1; int Da = 1; // 1 to months[Mo-1] int prevMo = -1; int Mo = 1; // 1 to 12 (!) int prevYr = -1; int Yr = 20; // 0 to 99 void loop(void) { // Check if refresh requested if (refresh == true) { // Serial.println("Refreshing"); screenInit(); Hr = -1; // if (!isDataValid) { Mi = -1; Se = -1; } tft.setFont(&FreeMonoBold18pt7b); showHour(); showMinute(); showDate(); showTemp(true); refresh = false; } // Check if user has selected the menu if (Touch_getXY()) { // Check if the menu was selected. if ((pixel_x > 310) && (pixel_y > 200) && (pixel_y < 230)) { // Menu button userMenu(); refresh = true; } } // read data from the GPS and update date/time variables while (SerialGPS.available() > 0) { // Collect all available characters char c = SerialGPS.read(); if (c == '$') { // If start of message, p = 0; // point to start of buffer buff[p++] = c; // and insert this character continue; // and wait for more. } if (c == '\n') { // If this is an end-of-line buff[p] = '\0'; // terminate the string p = 0; // and reset the buffer pointer. bool r = true; // assume it's a $GPRMC for (int i = 0; i < 6; i++) { // Looking at each character if (test[i] != buff[i]) r = false; // and if no match, set flag. } if (r == true) { // If everything matched parseBuffer(); // parse, validate and update. } } else { buff[p++] = c; // or just append to buffer if (p > 98) p = 0; // and reset if overflow. } } //------------------------------------------------------- // Process timer tick if (millis() > millisNext) { // If timer has ticked if (!isDataValid) { // If data is not valid, // then force the update if (++Se > 59) { // Bump the seconds. If it rolls over, if (++Mi > 59) { // bump the minutes and if it rolls over, if (++Hr > 23) { // bump the hours. Hr = 0; // and reset for new day. } Mi = 0; // and reset for new hour. } Se = 0; // and reset for new minute. } } // forced update // Update display (every S). tft.setTextColor(WHITE); tft.setTextSize(2); //-Process Hours Change---------------------------------------------------- if (prevHr != Hr) { // Hours // Serial.println("Hour Change: Hr " + String(Hr) + " PrevHr " + String(prevHr)); localHr = Hr + timeZone; // Add the timezone offset. if (localHr > 23) { // If that becomes next day, localHr -= 24; // adjust for hours roll over if (dayRoll != 1) { // If this is the first dayRoll = 1; // then set for day roll forward if (prevHr != -1) refresh = true;// and flag for date update. else refresh = false; } } if (localHr < 0) { // If that bcomes previous day, localHr += 24; // adjust for hours roll over if (dayRoll != -1) { // If not already flagged dayRoll = -1; // then set for day roll back if (prevHr != -1) refresh = true;// and flag for date update. else refresh = false; } } // Serial.println("localHr= " + String(localHr) + " Refresh= " + refresh); showHour(); showDate(); prevHr = Hr; } //-Process Minutes Change------------------------------------------------------ if (prevMi != Mi) { // Minutes showMinute(); showTemp(false); // Display the temperature (if changed). } //-Process Seconds Change------------------------------------------------------ if (prevSe != Se) { // Seconds SPrint = (Se < 10 ? "0" + String(Se) : String(Se)); tft.setCursor(220, 60); tft.fillRect(220, 10, 80, 52, BLACK); tft.print(SPrint); // Serial.println("Seconds Change: Se " + String(Se) + " PrevSe " + String(prevSe) + " " + SPrint); prevSe = Se; prevMi = Mi; } //-Process Day Change------------------------------------------------------- if (prevDa != Da) { // If day has rolled over // Serial.println("Day Change: Da " + String(Da) + " PrevDa " + String(prevDa)); refresh = true; // then refresh everything. prevDa = Da; // and make same day. } millisNext = millis() + 1000; // Set timer for next tick, isDataValid = false; // Assume GPS update does not occur } } void showMinute() { SPrint = (Mi < 10 ? "0" + String(Mi) : String(Mi)); tft.setCursor(115, 60); tft.fillRect(115, 10, 80, 52, BLACK); tft.print(SPrint); // Serial.println("Minute Change: Mi " + String(Mi) + " PrevMi " + String(prevMi) + " " + SPrint); } void showHour() { // Display the hour // 24-hour mode - 0 to 23. SPrint = (localHr < 10 ? "0" + String(localHr) : String(localHr)); if (!displayMode) { // 12-hour mode 12, 1...11, 12, 1...11. int i = localHr % 12; // Convert to 0-23 to 0-11 if (i == 0) i = 12; // Convert 0:xx to 12:xx SPrint = (i < 10 ? "0" + String(i) : String(i)); // Do AM/PM tft.setTextSize(1); tft.setCursor(132, 110); tft.fillRect(130, 70, 24, 24, BLACK); tft.print((localHr < 12) ? "\077\075" : "\076\075"); // "AM"/"PM" tft.setTextSize(2); } tft.setCursor(10, 60); tft.fillRect(10, 10, 80, 52, BLACK); tft.print(SPrint); // Serial.println("Hour = " + SPrint + String(" Hr ") + String(Hr) + " PrevHr " + String(prevHr) + " " + SPrint); } void showDate() { int i = Da; int j = Mo; int k = Yr; if (dayRoll == 1) { // Roll forward 1 day int l = monthDays[j - 1]; // Get max days in month if ((j == 2) && (isLeapYear(k))) l++; if (++i > l) { // Bump day and test i = 1; // Overflow. Reset day if (++j > 12) { // Bump month and test. j = 1; // Reset month k++; // Bump year } } } else if (dayRoll == -1) { // Roll back 1 day if (--i < 1) { // Debump day and test. if (--j < 1) { // Debump month and test. j = 12; // Reset month. k--; // Debump year. } i = monthDays[j - 1]; // Day is last day in (debumped) month if ((j == 2) && (isLeapYear(k))) i++; } } dayRoll = 0; tft.setFont(); tft.setTextSize(3); tft.fillRect(0, 150, 319, 24, BLACK); tft.setCursor(190 - (strlen(months[j - 1]) + 8) * 11, 150); // Centre date string tft.print(i); tft.print(" " + String(months[j - 1]) + " " + String(k + 2000)); tft.setFont(&FreeMonoBold18pt7b); tft.setTextSize(2); } void showTemp(bool force) { static float oldTemp; float newTemp = sensor.temp(); if ((newTemp != oldTemp) || (force)) { oldTemp = newTemp; tft.setTextSize(1); tft.setCursor(100, 235); tft.fillRect(100, 210, 104, 29, BLACK); tft.print(newTemp, 1); tft.print("\073\074"); tft.setTextSize(2); } } // void startPage() { // tft.setCursor(20, 20); // tft.setTextColor(WHITE); // tft.setTextSize(3); // tft.println("TFT - LCD Clock"); // } void screenInit() { tft.fillScreen(BLACK); tft.setFont(&FreeMonoBold18pt7b); tft.setTextColor(WHITE); tft.setTextSize(2); tft.setCursor(87, 60); tft.print(":"); tft.setCursor(192, 60); tft.print(":"); } void parseBuffer() { // extract date and time from GPRMC Serial.println(buff); if (validateRMC()) { // Validate message by checksum. Hr = (buff[7] - '0') * 10 + (buff[8] - '0'); // Get hour Mi = (buff[9] - '0') * 10 + (buff[10] - '0'); // Get minute Se = (buff[11] - '0') * 10 + (buff[12] - '0'); // Get second int j = 1; int k = 0; while (k < 9) { // Count off 9 commas if (buff[j++] == ',') k++; } Da = (buff[j++] - '0') * 10 + (buff[j++] - '0'); // Get day Mo = (buff[j++] - '0') * 10 + (buff[j++] - '0'); // Get month Yr = (buff[j++] - '0') * 10 + (buff[j++] - '0'); // Get year isDataValid = true; } } bool validateRMC() { int crc = 0; int len = strlen(buff) - 4; int i; for (i = 1; i < len; i++) { crc ^= buff[i]; } byte lNibble = (buff[len + 1] <= '9') ? buff[len + 1] - '0' : buff[len + 1] - '7'; byte rNibble = (buff[len + 2] <= '9') ? buff[len + 2] - '0' : buff[len + 2] - '7'; if (crc == (lNibble << 4) + rNibble) return true; return false; } void userMenu() { //------------------------------------------------------- // Display menu options. tft.fillRect(0, 0, 319, 239, BLACK); tft.setFont(); tft.setTextSize(2); tft.setCursor(220, 15); tft.print ("Restart"); tft.setCursor(140, 65); tft.print ("12/24Hr (" + String(displayMode ? "24Hr" : "12Hr") + ")"); tft.setCursor(180, 110); tft.print ("UTC Offset+"); tft.setCursor(240, 135); tft.print (timeZone); tft.setCursor(180, 160); tft.print ("UTC Offset-"); tft.setCursor(260, 205); tft.print ("END"); delay(200); do { // Check if touched if (Touch_getXY()) { // Restart option. if ((pixel_x > 310) && (pixel_y > 5) && (pixel_y < 35)) { reset(); } // 12/24 display format option. if ((pixel_x > 310) && (pixel_y > 55) && (pixel_y < 85)) { displayMode = !displayMode; tft.fillRect(140, 65, 179, 25, BLACK); tft.setCursor(140, 65); tft.print ("12/24Hr (" + String(displayMode ? "24Hr" : "12Hr") + ")"); } // Offset increment option. if ((pixel_x > 310) && (pixel_y > 105) && (pixel_y < 135)) { timeZone += 1; if (timeZone > 12) timeZone = -12; tft.fillRect(240, 135, 80, 14, BLACK); tft.setCursor(240, 135); tft.print (timeZone); } // Offset decrement option. if ((pixel_x > 310) && (pixel_y > 155) && (pixel_y < 180)) { timeZone -= 1; if (timeZone < -12) timeZone = 12; tft.fillRect(240, 135, 90, 14, BLACK); tft.setCursor(240, 135); tft.print (timeZone); } // End. if ((pixel_x > 310) && (pixel_y > 200) && (pixel_y < 230)) { // Menu button refresh = true; // Refresh is required EEPROM.put(0, timeZone); // User values saved (if changed) EEPROM.put(2, displayMode); // continue loop } // Menu Button delay(200); } } while (refresh == false); } // User menu bool isLeapYear(int year) { if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) return (true); return (false); } // Get touch information and map to pixels. bool Touch_getXY(void) // Touch_getXY() updates global vars pixel_x, pixel_y { TSPoint p = ts.getPoint(); pinMode(YP, OUTPUT); //restore shared pins pinMode(XM, OUTPUT); digitalWrite(YP, HIGH); //because .getPoint() reset them. digitalWrite(XM, HIGH); if (p.z > MINPRESSURE && p.z < MAXPRESSURE) { // Update global touch points // LANDSCAPE CALIBRATION 320 x 240 pixel_x = map(p.y, TS_TOP, TS_BOT, 0, 320); pixel_y = map(p.x, TS_RT, TS_LEFT, 0, 240); return true; } return false; } void reset() { asm volatile ("jmp 0"); }