// =====================================================
// BIBLIOTECAS
// =====================================================
#include <Wire.h> // Comunicacao I2C (usada pelo RTC)
#include <RTClib.h> // Biblioteca do modulo RTC
#include <LiquidCrystal.h> // Biblioteca do LCD em modo paralelo
// =====================================================
// DEFINES - VALORES FIXOS DO PROJETO
// =====================================================
// Pinos do LCD
#define LCD_RS 12
#define LCD_E 11
#define LCD_D4 10
#define LCD_D5 9
#define LCD_D6 8
#define LCD_D7 7
// Pinos do encoder
#define ENCODER_CLK 2 //Sinal de referência para detectar giro no encoder
#define ENCODER_DT 3 //Sinal que informa o sentido da rotação
#define ENCODER_SW 4 //Ligado ao botão do eixo
// Pino do buzzer
#define BUZZER_PIN 5
// Tamanho do LCD
#define LCD_COLUMNS 20
#define LCD_ROWS 4
// Modos de tela
#define SCREEN_UTC 0
#define SCREEN_MENU 1
#define SCREEN_CITY 2
// Quantidade de cidades
#define CITY_COUNT 9
// Tempo do debounce do botao
#define DEBOUNCE_TIME 180
// Intervalo de atualizacao do display
#define SCREEN_UPDATE_INTERVAL 250
// Intervalo do pisca do nome da cidade
#define BLINK_INTERVAL 1000
// Como o RTC esta ajustado em Sao Paulo,
// somamos +3 horas para obter UTC
#define UTC_REFERENCE_OFFSET 3
// =====================================================
// OBJETOS PRINCIPAIS
// =====================================================
// Cria o objeto ldc, que faz referência ao nosso LCD
LiquidCrystal lcd(LCD_RS, LCD_E, LCD_D4, LCD_D5, LCD_D6, LCD_D7);
// Cria o objeto rtc para coletarmos informações de data e hora no RTC DS1307
RTC_DS1307 rtc;
// =====================================================
// LISTA DE CIDADES E OFFSETS
// =====================================================
// Lista de nomes das cidades
const char *cityNames[CITY_COUNT] = {
"UTC",
"Sao Paulo",
"Buenos Aires",
"New York",
"Lisboa",
"Paris",
"Dubai",
"Tokyo",
"Sydney"
};
// Lista de offsets em relacao ao UTC
const int cityOffsets[CITY_COUNT] = {
0,
-3,
-3,
-5,
0,
1,
4,
9,
10
};
// =====================================================
// VARIAVEIS GERAIS
// =====================================================
int screenMode = SCREEN_UTC; // Tela atual
int menuIndex = 0; // Cidade destacada no menu
int selectedCity = 0; // Cidade confirmada pelo usuario
int lastClkState; // Ultimo estado do CLK do encoder
int currentClkState; // Estado atual do CLK do encoder
int lastButtonState = HIGH; // Ultimo estado do botao
unsigned long lastButtonTime = 0; // Ultimo clique valido
unsigned long lastScreenUpdate = 0; // Ultima atualizacao da tela
unsigned long lastBlinkTime = 0; // Ultima troca do pisca
bool cityNameVisible = true; // Controla se o nome da cidade aparece ou nao
// =====================================================
// SETUP
// =====================================================
void setup() {
// Inicializa o LCD com 20 colunas e 4 linhas
lcd.begin(LCD_COLUMNS, LCD_ROWS);
// Configura os pinos do encoder como entrada com pull-up interno
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
pinMode(ENCODER_SW, INPUT_PULLUP);
// Configura o buzzer como saida
pinMode(BUZZER_PIN, OUTPUT);
// Inicia o barramento I2C
Wire.begin();
// Inicia o RTC, se não encontrar o módulo, mostra uma mensagem de erro no monitor serial
if (!rtc.begin()) {
printTextLine(0, "Erro no RTC");
printTextLine(1, "Verifique modulo");
printTextLine(2, "");
printTextLine(3, "");
while (true) {
delay(100);
}
}
// -------------------------------------------------
// Use apenas uma vez para ajustar o RTC
// Depois comente esta linha e grave novamente
// -------------------------------------------------
// rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
// Lemos o estado inicial do encoder
lastClkState = digitalRead(ENCODER_CLK);
// Mostra a tela inicial
showUtcScreen();
}
// =====================================================
// LOOP PRINCIPAL
// =====================================================
void loop() {
readEncoder();
readButton();
// Controla o pisca do nome da cidade usando millis()
// millis() retorna o tempo, em milissegundos, desde que o Arduino ligou
if (millis() - lastBlinkTime >= BLINK_INTERVAL) {
lastBlinkTime = millis();
cityNameVisible = !cityNameVisible; // Inverte true/false
}
// Atualiza a tela em intervalos controlados
if (millis() - lastScreenUpdate >= SCREEN_UPDATE_INTERVAL) {
lastScreenUpdate = millis();
if (screenMode == SCREEN_UTC) {
showUtcScreen();
}
if (screenMode == SCREEN_CITY) {
showSelectedCityScreen();
}
}
delay(2);
}
// =====================================================
// LEITURA DO ENCODER
// =====================================================
void readEncoder() {
currentClkState = digitalRead(ENCODER_CLK);
// Se o estado mudou, o encoder girou
if (currentClkState != lastClkState) {
// Usamos a transicao para LOW como referencia
if (currentClkState == LOW) {
// Se DT estiver diferente de CLK, gira em um sentido
// Caso contrario, gira no outro
if (digitalRead(ENCODER_DT) != currentClkState) {
menuIndex++;
} else {
menuIndex--;
}
// So mudamos a cidade se estivermos no menu
if (screenMode == SCREEN_MENU) {
if (menuIndex >= CITY_COUNT) {
menuIndex = 0;
}
if (menuIndex < 0) {
menuIndex = CITY_COUNT - 1;
}
beep();
showMenuScreen();
}
}
}
lastClkState = currentClkState;
}
// =====================================================
// LEITURA DO BOTAO
// =====================================================
void readButton() {
int buttonState = digitalRead(ENCODER_SW);
// Detecta quando o botao foi pressionado
if (buttonState == LOW && lastButtonState == HIGH) {
// Debounce simples
if (millis() - lastButtonTime > DEBOUNCE_TIME) {
lastButtonTime = millis();
beep();
// Sai da tela UTC e entra no menu
if (screenMode == SCREEN_UTC) {
screenMode = SCREEN_MENU;
showMenuScreen();
}
// Confirma a cidade destacada no menu
else if (screenMode == SCREEN_MENU) {
selectedCity = menuIndex;
screenMode = SCREEN_CITY;
cityNameVisible = true;
lastBlinkTime = millis();
showSelectedCityScreen();
}
// Volta da tela da cidade para o menu
else if (screenMode == SCREEN_CITY) {
screenMode = SCREEN_MENU;
showMenuScreen();
}
}
}
lastButtonState = buttonState;
}
// =====================================================
// TELA UTC
// =====================================================
void showUtcScreen() {
DateTime rtcNow = rtc.now();
DateTime utcNow = convertRtcToUtc(rtcNow);
printTextLine(0, "World Time Terminal");
printTextLine(1, "Referencia: UTC");
printDateLine(2, utcNow.day(), utcNow.month(), utcNow.year());
printTimeLine(3, utcNow.hour(), utcNow.minute(), utcNow.second());
}
// =====================================================
// TELA DE MENU
// =====================================================
void showMenuScreen() {
printTextLine(0, "Selecione a cidade");
printTextLine(1, "Gire o encoder");
lcd.setCursor(0, 2);
lcd.print("> ");
lcd.print(cityNames[menuIndex]);
// Completa o resto da linha com espacos
fillLineWithSpaces(2, 2 + textLength(cityNames[menuIndex]));
printTextLine(3, "Clique confirma");
}
// =====================================================
// TELA DA CIDADE ESCOLHIDA
// =====================================================
void showSelectedCityScreen() {
DateTime rtcNow = rtc.now();
DateTime utcNow = convertRtcToUtc(rtcNow);
DateTime localTime = convertUtcToCityTime(utcNow, cityOffsets[selectedCity]);
printTextLine(0, "Cidade selecionada");
// Linha da cidade + offset fixo no final
printCityAndOffsetLine(1, cityNames[selectedCity], cityOffsets[selectedCity], cityNameVisible);
printDateLine(2, localTime.day(), localTime.month(), localTime.year());
printTimeLine(3, localTime.hour(), localTime.minute(), localTime.second());
}
// =====================================================
// CONVERSAO DE HORARIOS
// =====================================================
// Converte o horario do RTC (Sao Paulo) para UTC
DateTime convertRtcToUtc(DateTime rtcTime) {
// TimeSpan representa um intervalo de tempo
// Aqui estamos somando 3 horas
return rtcTime + TimeSpan(0, UTC_REFERENCE_OFFSET, 0, 0);
}
// Aplica o offset da cidade sobre o UTC
DateTime convertUtcToCityTime(DateTime utcTime, int cityOffset) {
return utcTime + TimeSpan(0, cityOffset, 0, 0);
}
// =====================================================
// FUNCOES AUXILIARES DE TEXTO E DISPLAY
// =====================================================
// Escreve uma linha de texto e limpa o restante da linha
void printTextLine(int row, const char *text) {
lcd.setCursor(0, row);
lcd.print(text);
fillLineWithSpaces(row, textLength(text));
}
// Escreve a data na linha indicada
void printDateLine(int row, int dayValue, int monthValue, int yearValue) {
lcd.setCursor(0, row);
printTwoDigits(dayValue);
lcd.print("/");
printTwoDigits(monthValue);
lcd.print("/");
lcd.print(yearValue);
fillLineWithSpaces(row, 10);
}
// Escreve a hora na linha indicada
void printTimeLine(int row, int hourValue, int minuteValue, int secondValue) {
lcd.setCursor(0, row);
printTwoDigits(hourValue);
lcd.print(":");
printTwoDigits(minuteValue);
lcd.print(":");
printTwoDigits(secondValue);
fillLineWithSpaces(row, 8);
}
// Escreve o nome da cidade piscando e o offset fixo no final da linha
void printCityAndOffsetLine(int row, const char *cityName, int offsetValue, bool visible) {
int citySize = textLength(cityName);
int offsetSize = getOffsetSize(offsetValue);
// Coluna onde o offset deve começar
int offsetStartColumn = LCD_COLUMNS - offsetSize;
// Primeiro limpamos toda a linha
lcd.setCursor(0, row);
for (int i = 0; i < LCD_COLUMNS; i++) {
lcd.print(" ");
}
// Se a cidade estiver visivel, imprime o nome
if (visible) {
lcd.setCursor(0, row);
lcd.print(cityName);
}
// Imprime o offset fixo no final da linha
lcd.setCursor(offsetStartColumn, row);
printOffset(offsetValue);
}
// Preenche com espacos o restante de uma linha
void fillLineWithSpaces(int row, int usedColumns) {
lcd.setCursor(usedColumns, row);
for (int i = usedColumns; i < LCD_COLUMNS; i++) {
lcd.print(" ");
}
}
// Imprime um numero com 2 digitos
// Exemplo: 7 vira 07
void printTwoDigits(int value) {
if (value < 10) {
lcd.print("0");
}
lcd.print(value);
}
// Imprime o offset no formato UTC+X ou UTC-X
void printOffset(int offsetValue) {
lcd.print("UTC");
if (offsetValue >= 0) {
lcd.print("+");
} else {
lcd.print("-");
}
lcd.print(abs(offsetValue));
}
// Calcula o tamanho do texto do offset
// UTC+0 -> 5 caracteres
// UTC-3 -> 5 caracteres
// UTC+10 -> 6 caracteres
int getOffsetSize(int offsetValue) {
if (abs(offsetValue) >= 10) {
return 6;
} else {
return 5;
}
}
// Conta quantos caracteres existem em um texto
int textLength(const char *text) {
int count = 0;
while (text[count] != '\0') {
count++;
}
return count;
}
// =====================================================
// BUZZER
// =====================================================
void beep() {
// tone(pino, frequencia, duracao)
tone(BUZZER_PIN, 2000, 70);
}