/* Tide Clock by Mariya Lupandina 25.04.21
Tide prediction code based of:
Tide_calculator.ino by Luke Miller, copyright (c) 2019
This code calculates the current tide height for the pre-programmed site.
It requires a real time clock (DS1307 or DS3231 chips) to generate a time for the calculation.
The site is set by the name of the included library, for other sites use this link: // Other sites available at http://github.com/millerlp/Tide_calculator.
Written under version 1.6.4 of the Arduino IDE.
The harmonic constituents used here were originally derived from
the Center for Operational Oceanic Products and Services (CO-OPS),
National Ocean Service (NOS), National Oceanic and Atmospheric
Administration, U.S.A.
The display code is based of:
Arduino UNO and 128x128 OLED Display for analog clock by upir, 2023
*/
//Initial setup
//Header files for talking to real time clock
#include <Wire.h> // Required for RTClib
#include <SPI.h> // Required for RTClib to compile properly
#include <RTClib.h> // From https://github.com/millerlp/RTClib
//Header file for tide prediction at given site
#include "TidelibTheBatteryNewYorkHarborNewYork.h"
//Header file for talking with OLED display
#include <Arduino.h>
#include <U8g2lib.h> // u8g2 library for drawing on OLED display - needs to be installed in Arduino IDE first
// ---------------------- Display & Tide Calc Setup ----------------------
U8G2_SH1107_128X128_1_HW_I2C u8g2(U8G2_R0); // For 128x128 SH1107
//#define CS 10
//#define DC 9
//#define RST 8
//U8G2_SH1107_128X128_1_4W_HW_SPI u8g2(U8G2_R0, 10, 9, 8); //[page buffer, size = 128 bytes]
RTC_DS1307 RTC;
TideCalc myTideCalc;
const int center_x = 64;
const int center_y = 64;
const int min_radius = 4;
const int max_radius = 31;
int current_radius = min_radius;
bool growing = true;
// ---------------------- Pulse Animation State ----------------------
unsigned long lastUpdate = 0; // Time of last animation update
const unsigned long animationInterval = 1000; // Update every 1 second
// ---------------------- Tide Event Containers ----------------------
DateTime tideTimes[4]; // Stores up to 4 tide times (2 high + 2 low)
int tideType[4]; // 1 = High tide, 0 = Low tide
DateTime lastPredictionTime;
int currDay;
/// ---------------------- Setup ----------------------
void setup() {
Wire.begin(); // Start I2C communication
RTC.begin(); // Start RTC
u8g2.begin(); // Start display
Serial.begin(57600); // Initialize serial monitor for debugging
u8g2.setContrast(255); // Set OLED brightness
DateTime now = RTC.now();
currDay = now.day();
lastPredictionTime = now;
Serial.println("Starting TideClock");
Serial.print("Station: ");
Serial.print(myTideCalc.returnStationID());
Serial.print(" (ID ");
Serial.print(myTideCalc.returnStationIDnumber());
Serial.println(")");
DateTime startOfDay(now.year(), now.month(), now.day(), 0, 0, 0);
updateTidePredictions(startOfDay);
}
// ---------------------- Loop ----------------------
void loop() {
DateTime now = RTC.now();
// Update tide predictions at start of a new day
if (now.day() != currDay) {
currDay = now.day();
updateTidePredictions(now);
}
// Update the pulse animation based on tide phase
updatePulse(now);
// Render display
drawDisplay(now);
}
// 1. Tide Prediction Function — Called Once Per Day
void updateTidePredictions(DateTime now) {
Serial.println("\n==========================");
Serial.print("Generating tide predictions for ");
Serial.print(now.month()); Serial.print("/");
Serial.print(now.day()); Serial.print("/");
Serial.println(now.year());
Serial.println("==========================");
DateTime startOfDay(now.year(), now.month(), now.day(), 0, 0, 0);
const int interval = 5; // Sample every 5 minutes
DateTime prevTime = startOfDay;
DateTime currTime = startOfDay + TimeSpan(0, 0, interval, 0);
float prevTide = myTideCalc.currentTide(prevTime);
float currTide = myTideCalc.currentTide(currTime);
int count = 0;
while ((currTime.day() == startOfDay.day()) && count < 4) {
DateTime nextTime = currTime + TimeSpan(0, 0, interval, 0);
float nextTide = myTideCalc.currentTide(nextTime);
// Detect high tide: rising to falling
if (prevTide < currTide && currTide > nextTide && count < 4) {
tideTimes[count] = currTime;
tideType[count] = 1; // High tide
Serial.print("High Tide at ");
printTime(currTime);
count++;
}
// Detect low tide: falling to rising
if (prevTide > currTide && currTide < nextTide && count < 4) {
tideTimes[count] = currTime;
tideType[count] = 0; // Low tide
Serial.print("Low Tide at ");
printTime(currTime);
count++;
}
prevTime = currTime;
prevTide = currTide;
currTime = nextTime;
currTide = nextTide;
}
if (count < 4) {
Serial.println("⚠️ Warning: Less than 4 tide events found.");
}
Serial.println("==========================\n");
}
// 2. Pulse Animation — Grows/Shrinks Toward Tide Events
void updatePulse(DateTime now) {
if (millis() - lastUpdate < animationInterval) return;
lastUpdate = millis();
// Find the current interval (between two known tide events)
DateTime previous, next;
bool growing = true;
for (int i = 0; i < 4; i++) {
if (tideTimes[i] > now) {
next = tideTimes[i];
previous = tideTimes[(i - 1 + 4) % 4]; // Wrap around
growing = (tideType[i] == 1); // If next is high tide, pulse is growing
break;
}
}
float totalTime = (next - previous).totalseconds();
float elapsedTime = (now - previous).totalseconds();
float phase = constrain(elapsedTime / totalTime, 0, 1); // Normalize
// Linearly interpolate pulse radius
if (growing) {
current_radius = min_radius + (max_radius - min_radius) * phase;
} else {
current_radius = max_radius - (max_radius - min_radius) * phase;
}
}
// 3. Drawing Function — Display Background, Pulse, Ticks
void drawDisplay(DateTime now) {
u8g2.firstPage();
do {
draw_background();
draw_tides(current_radius);
draw_ticks();
} while (u8g2.nextPage());
}
// Draw outer circle and center point
void draw_background() {
u8g2.setDrawColor(1);
u8g2.drawCircle(center_x, center_y, 63, U8G2_DRAW_ALL);
u8g2.drawPixel(center_x, center_y); // Center mark
}
// Draw the pulse disc that grows/shrinks
void draw_tides(int radius) {
u8g2.setDrawColor(1);
u8g2.drawDisc(center_x, center_y, radius, U8G2_DRAW_ALL);
}
// Draw tick marks for tide events
void draw_ticks() {
for (int i = 0; i < 4; i++) {
float hour = tideTimes[i].hour() + tideTimes[i].minute() / 60.0;
float angle_deg = (hour / 24.0) * 360.0;
float angle_rad = angle_deg * PI / 180.0;
float x1 = center_x + sin(angle_rad) * 50;
float y1 = center_y - cos(angle_rad) * 50;
float x2 = center_x + sin(angle_rad) * 63;
float y2 = center_y - cos(angle_rad) * 63;
u8g2.drawLine(x1, y1, x2, y2); // Draw tick mark
}
}
// 4. Serial Print Function
void printTime(DateTime now) {
if (now.hour() < 10) Serial.print("0");
Serial.print(now.hour()); Serial.print(":");
if (now.minute() < 10) Serial.print("0");
Serial.print(now.minute()); Serial.print(":");
if (now.second() < 10) Serial.print("0");
Serial.println(now.second());
}