2026-02-26 19:54:07 +09:00
|
|
|
/*
|
|
|
|
|
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: MIT
|
|
|
|
|
*/
|
|
|
|
|
/*
|
|
|
|
|
Example using M5UnitUnified for UnitRTC (PCF8563/BM8563/HYM8563)
|
|
|
|
|
- Sync time from NTP via WiFi, then set RTC
|
|
|
|
|
*/
|
|
|
|
|
#include <M5Unified.h>
|
|
|
|
|
#include <M5UnitUnified.h>
|
|
|
|
|
#include <M5UnitUnifiedRTC.h>
|
|
|
|
|
#include <M5HAL.hpp> // For NessoN1
|
|
|
|
|
#include <WiFi.h>
|
|
|
|
|
#include <esp_sntp.h>
|
|
|
|
|
#include <esp_random.h>
|
|
|
|
|
#include <algorithm> // std::shuffle
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
// ==============================================================
|
|
|
|
|
// NOTICE: Set your WiFi credentials and timezone before building
|
|
|
|
|
// ==============================================================
|
2026-02-26 20:40:43 +09:00
|
|
|
const char* WIFI_SSID = ""; // Your WiFi SSID
|
2026-02-26 19:54:07 +09:00
|
|
|
const char* WIFI_PASS = ""; // Your WiFi password
|
|
|
|
|
// POSIX timezone string (default: JST-9 = UTC+9, no DST)
|
|
|
|
|
const char* YOUR_TIMEZONE = "JST-9";
|
|
|
|
|
// const char* YOUR_TIMEZONE = "EST5"; // US Eastern (UTC-5, no DST)
|
|
|
|
|
// const char* YOUR_TIMEZONE = "CET-1"; // Central Europe (UTC+1, no DST)
|
|
|
|
|
// const char* YOUR_TIMEZONE = "CST-8"; // China (UTC+8)
|
|
|
|
|
// See: https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
|
|
|
|
|
|
|
|
|
|
// NTP server list
|
|
|
|
|
// China
|
|
|
|
|
constexpr char ntp_cn0[] = "cn.pool.ntp.org";
|
|
|
|
|
constexpr char ntp_cn1[] = "ntp.aliyun.com";
|
|
|
|
|
constexpr char ntp_cn2[] = "time.cloud.tencent.com";
|
|
|
|
|
// Japan
|
|
|
|
|
constexpr char ntp_jp0[] = "ntp.nict.jp";
|
|
|
|
|
constexpr char ntp_jp1[] = "ntp.jst.mfeed.ad.jp";
|
|
|
|
|
constexpr char ntp_jp2[] = "time.google.com";
|
|
|
|
|
// United States
|
|
|
|
|
constexpr char ntp_us0[] = "us.pool.ntp.org";
|
|
|
|
|
constexpr char ntp_us1[] = "time.nist.gov";
|
|
|
|
|
constexpr char ntp_us2[] = "time.cloudflare.com";
|
|
|
|
|
// Europe
|
|
|
|
|
constexpr char ntp_eu0[] = "europe.pool.ntp.org";
|
|
|
|
|
constexpr char ntp_eu1[] = "ntp.ripe.net";
|
|
|
|
|
constexpr char ntp_eu2[] = "ptbtime1.ptb.de";
|
|
|
|
|
|
|
|
|
|
// Select region: jp / us / eu / cn
|
|
|
|
|
const char* ntpURLTable[] = {ntp_jp0, ntp_jp1, ntp_jp2};
|
|
|
|
|
// const char* ntpURLTable[] = {ntp_us0, ntp_us1, ntp_us2};
|
|
|
|
|
// const char* ntpURLTable[] = {ntp_eu0, ntp_eu1, ntp_eu2};
|
|
|
|
|
// const char* ntpURLTable[] = {ntp_cn0, ntp_cn1, ntp_cn2};
|
|
|
|
|
|
|
|
|
|
// UniformRandomBitGenerator using ESP32 hardware RNG
|
|
|
|
|
struct ESP32RNG {
|
|
|
|
|
using result_type = uint32_t;
|
|
|
|
|
static constexpr result_type min()
|
|
|
|
|
{
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
static constexpr result_type max()
|
|
|
|
|
{
|
|
|
|
|
return UINT32_MAX;
|
|
|
|
|
}
|
|
|
|
|
result_type operator()()
|
|
|
|
|
{
|
|
|
|
|
return esp_random();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
constexpr char WDAY_NAMES[][4] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
|
|
|
|
|
|
|
|
|
|
auto& lcd = M5.Display;
|
|
|
|
|
m5::unit::UnitUnified Units;
|
|
|
|
|
m5::unit::UnitPCF8563 unit;
|
|
|
|
|
|
|
|
|
|
void display_printf(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
|
|
|
|
void display_printf(const char* fmt, ...)
|
|
|
|
|
{
|
|
|
|
|
va_list args;
|
|
|
|
|
va_start(args, fmt);
|
|
|
|
|
lcd.vprintf(fmt, args);
|
|
|
|
|
va_end(args);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sync NTP time and set RTC (shows progress on Display)
|
|
|
|
|
bool sync_ntp_to_rtc()
|
|
|
|
|
{
|
|
|
|
|
M5_LOGI("Connecting to WiFi: %s", WIFI_SSID);
|
|
|
|
|
display_printf("WiFi connecting");
|
|
|
|
|
|
|
|
|
|
// Tab5 (ESP32-P4) uses ESP32-C6 co-processor for WiFi via SDIO; set pins before WiFi.begin()
|
|
|
|
|
#if defined(CONFIG_IDF_TARGET_ESP32P4)
|
|
|
|
|
if (M5.getBoard() == m5::board_t::board_M5Tab5) {
|
|
|
|
|
constexpr int SDIO2_CLK = 12;
|
|
|
|
|
constexpr int SDIO2_CMD = 13;
|
|
|
|
|
constexpr int SDIO2_D0 = 11;
|
|
|
|
|
constexpr int SDIO2_D1 = 10;
|
|
|
|
|
constexpr int SDIO2_D2 = 9;
|
|
|
|
|
constexpr int SDIO2_D3 = 8;
|
|
|
|
|
constexpr int SDIO2_RST = 15;
|
|
|
|
|
WiFi.setPins(SDIO2_CLK, SDIO2_CMD, SDIO2_D0, SDIO2_D1, SDIO2_D2, SDIO2_D3, SDIO2_RST);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
WiFi.mode(WIFI_STA);
|
|
|
|
|
if (WIFI_SSID[0] && WIFI_PASS[0]) {
|
|
|
|
|
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
|
|
|
|
} else {
|
|
|
|
|
WiFi.begin();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int retry = 20;
|
|
|
|
|
while (retry-- > 0 && WiFi.status() != WL_CONNECTED) {
|
|
|
|
|
display_printf(".");
|
|
|
|
|
m5::utility::delay(500);
|
|
|
|
|
}
|
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
|
|
|
M5_LOGE("WiFi connection failed");
|
|
|
|
|
display_printf("\nWiFi FAILED\n");
|
|
|
|
|
WiFi.disconnect(true);
|
|
|
|
|
WiFi.mode(WIFI_OFF);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
M5_LOGI("WiFi connected");
|
|
|
|
|
display_printf("\nWiFi OK\n");
|
|
|
|
|
|
|
|
|
|
// Shuffle NTP servers
|
|
|
|
|
std::shuffle(std::begin(ntpURLTable), std::end(ntpURLTable), ESP32RNG{});
|
|
|
|
|
M5_LOGI("NTP: %s / %s / %s", ntpURLTable[0], ntpURLTable[1], ntpURLTable[2]);
|
|
|
|
|
configTzTime(YOUR_TIMEZONE, ntpURLTable[0], ntpURLTable[1], ntpURLTable[2]);
|
|
|
|
|
|
|
|
|
|
// Wait for sync
|
|
|
|
|
display_printf("NTP syncing");
|
|
|
|
|
retry = 10;
|
|
|
|
|
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && --retry >= 0) {
|
|
|
|
|
M5_LOGI(" NTP sync in progress...");
|
|
|
|
|
display_printf(".");
|
|
|
|
|
m5::utility::delay(1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Done with WiFi
|
|
|
|
|
WiFi.disconnect(true);
|
|
|
|
|
WiFi.mode(WIFI_OFF);
|
|
|
|
|
|
2026-02-26 20:40:43 +09:00
|
|
|
struct tm t{};
|
2026-02-26 19:54:07 +09:00
|
|
|
if (!getLocalTime(&t, 10000)) {
|
|
|
|
|
M5_LOGE("Failed to get NTP time");
|
|
|
|
|
display_printf("\nNTP FAILED\n");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
M5_LOGI("NTP time: %04d-%02d-%02d %02d:%02d:%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min,
|
|
|
|
|
t.tm_sec);
|
|
|
|
|
display_printf("\nNTP OK\n");
|
|
|
|
|
|
|
|
|
|
// Set RTC from NTP (use GMT for RTC storage)
|
|
|
|
|
// Stop RTC clock for precise time setting (prescaler held in reset)
|
|
|
|
|
unit.writeStop(true);
|
|
|
|
|
|
|
|
|
|
// Prepare next second's time
|
|
|
|
|
time_t now = time(nullptr) + 1;
|
|
|
|
|
struct tm gmt = *gmtime(&now);
|
|
|
|
|
|
|
|
|
|
// Write time while clock is stopped (no carry race condition)
|
|
|
|
|
if (!unit.writeDateTime(gmt)) {
|
|
|
|
|
unit.writeStop(false);
|
|
|
|
|
M5_LOGE("Failed to set RTC");
|
|
|
|
|
display_printf("RTC set FAILED\n");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait for exact second boundary, then release STOP
|
|
|
|
|
// First 1Hz tick occurs ~0.508s after STOP is released
|
|
|
|
|
while (now > time(nullptr)) {
|
|
|
|
|
;
|
|
|
|
|
}
|
|
|
|
|
unit.writeStop(false);
|
|
|
|
|
|
|
|
|
|
M5_LOGI("RTC set to (GMT): %04d-%02d-%02d %02d:%02d:%02d", gmt.tm_year + 1900, gmt.tm_mon + 1, gmt.tm_mday,
|
|
|
|
|
gmt.tm_hour, gmt.tm_min, gmt.tm_sec);
|
|
|
|
|
display_printf("RTC set OK\n");
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
void setup()
|
|
|
|
|
{
|
|
|
|
|
M5.begin();
|
|
|
|
|
M5.setTouchButtonHeightByRatio(100);
|
|
|
|
|
|
|
|
|
|
// The screen shall be in landscape mode
|
|
|
|
|
if (lcd.height() > lcd.width()) {
|
|
|
|
|
lcd.setRotation(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto board = M5.getBoard();
|
|
|
|
|
|
|
|
|
|
// ESP32-C6 boards have only 1 hardware I2C (Wire), used by M5Unified internally.
|
|
|
|
|
// Use SoftwareI2C via M5HAL for the GROVE port to avoid bus conflict.
|
|
|
|
|
if (board == m5::board_t::board_ArduinoNessoN1 || board == m5::board_t::board_M5NanoC6) {
|
|
|
|
|
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
|
|
|
|
|
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
|
|
|
|
|
if (board == m5::board_t::board_ArduinoNessoN1) {
|
|
|
|
|
pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out);
|
|
|
|
|
pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in);
|
|
|
|
|
}
|
|
|
|
|
M5_LOGI("getPin(M5HAL): SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
|
|
|
|
|
m5::hal::bus::I2CBusConfig i2c_cfg;
|
|
|
|
|
i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda);
|
|
|
|
|
i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl);
|
|
|
|
|
auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg);
|
|
|
|
|
M5_LOGI("Bus:%d", i2c_bus.has_value());
|
|
|
|
|
if (!Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr) || !Units.begin()) {
|
|
|
|
|
M5_LOGE("Failed to begin");
|
|
|
|
|
while (true) {
|
|
|
|
|
m5::utility::delay(10000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
|
|
|
|
|
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
|
|
|
|
|
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
|
|
|
|
|
Wire.begin(pin_num_sda, pin_num_scl, 100 * 1000U);
|
|
|
|
|
if (!Units.add(unit, Wire) || !Units.begin()) {
|
|
|
|
|
M5_LOGE("Failed to begin");
|
|
|
|
|
while (true) {
|
|
|
|
|
m5::utility::delay(10000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
M5_LOGI("M5UnitUnified has been begun");
|
|
|
|
|
M5_LOGI("%s", Units.debugInfo().c_str());
|
|
|
|
|
|
|
|
|
|
// Check voltage low (battery backup lost)
|
|
|
|
|
if (unit.getVoltLow()) {
|
|
|
|
|
M5_LOGW("RTC voltage low - time may be invalid");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sync from NTP if WiFi credentials are configured
|
|
|
|
|
if (!sync_ntp_to_rtc()) {
|
|
|
|
|
M5_LOGW("NTP sync skipped or failed - using current RTC time");
|
|
|
|
|
}
|
|
|
|
|
lcd.startWrite();
|
|
|
|
|
lcd.fillScreen(0);
|
|
|
|
|
lcd.endWrite();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void loop()
|
|
|
|
|
{
|
|
|
|
|
static time_t prev{};
|
|
|
|
|
|
|
|
|
|
M5.update();
|
|
|
|
|
Units.update();
|
|
|
|
|
|
|
|
|
|
// Show local time on Serial and Display every second
|
|
|
|
|
time_t now = time(nullptr);
|
|
|
|
|
if (now != prev) {
|
|
|
|
|
struct tm local = *localtime(&now);
|
|
|
|
|
|
|
|
|
|
// Serial
|
|
|
|
|
M5.Log.printf("%04d-%02d-%02d(%s) %02d:%02d:%02d\n", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday,
|
|
|
|
|
WDAY_NAMES[local.tm_wday], local.tm_hour, local.tm_min, local.tm_sec);
|
|
|
|
|
|
|
|
|
|
// Display
|
|
|
|
|
lcd.setCursor(0, 0);
|
|
|
|
|
lcd.startWrite();
|
|
|
|
|
display_printf("%04d-%02d-%02d(%s)\n%02d:%02d:%02d\n", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday,
|
|
|
|
|
WDAY_NAMES[local.tm_wday], local.tm_hour, local.tm_min, local.tm_sec);
|
|
|
|
|
lcd.endWrite();
|
|
|
|
|
prev = now;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (M5.BtnA.wasClicked()) {
|
|
|
|
|
// Read and show RTC (GMT)
|
|
|
|
|
auto dt = unit.getDateTime();
|
|
|
|
|
M5.Log.printf("RTC(GMT) %04d-%02d-%02d(%s) %02d:%02d:%02d\n", dt.date.year, dt.date.month, dt.date.date,
|
|
|
|
|
WDAY_NAMES[dt.date.weekDay % 7], dt.time.hours, dt.time.minutes, dt.time.seconds);
|
|
|
|
|
lcd.startWrite();
|
|
|
|
|
display_printf("RTC(GMT)\n%04d-%02d-%02d(%s)\n%02d:%02d:%02d\n", dt.date.year, dt.date.month, dt.date.date,
|
|
|
|
|
WDAY_NAMES[dt.date.weekDay % 7], dt.time.hours, dt.time.minutes, dt.time.seconds);
|
|
|
|
|
lcd.endWrite();
|
|
|
|
|
} else if (M5.BtnA.wasHold()) {
|
|
|
|
|
// Re-sync RTC from NTP (progress shown on Display)
|
|
|
|
|
M5_LOGI("Re-syncing NTP...");
|
|
|
|
|
lcd.startWrite();
|
|
|
|
|
lcd.fillScreen(0);
|
|
|
|
|
lcd.endWrite();
|
|
|
|
|
|
|
|
|
|
lcd.setCursor(0, 0);
|
|
|
|
|
if (!sync_ntp_to_rtc()) {
|
|
|
|
|
M5_LOGW("NTP sync failed");
|
|
|
|
|
}
|
|
|
|
|
lcd.startWrite();
|
|
|
|
|
lcd.fillScreen(0);
|
|
|
|
|
lcd.endWrite();
|
|
|
|
|
}
|
|
|
|
|
}
|