You've already forked M5Unit-RTC
mirror of
https://github.com/m5stack/M5Unit-RTC.git
synced 2026-05-20 10:33:20 -07:00
236 lines
7.4 KiB
C++
236 lines
7.4 KiB
C++
/*
|
|
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
/*
|
|
Example using M5UnitUnified for UnitRTC (PCF8563/BM8563/HYM8563)
|
|
- Countdown timer using writeTimer() convenience API
|
|
- BtnA click: cycle interval and repeat mode
|
|
5s repeat -> 10s repeat -> 30s repeat -> 5s oneshot -> 10s oneshot -> stop -> ...
|
|
*/
|
|
#include <M5Unified.h>
|
|
#include <M5UnitUnified.h>
|
|
#include <M5UnitUnifiedRTC.h>
|
|
#include <M5HAL.hpp> // For NessoN1
|
|
#include <sys/time.h>
|
|
#include <cstdint>
|
|
|
|
namespace {
|
|
|
|
// 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
|
|
|
|
constexpr char WDAY_NAMES[][4] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
|
|
|
|
auto& lcd = M5.Display;
|
|
LGFX_Sprite canvas(&lcd);
|
|
m5::unit::UnitUnified Units;
|
|
m5::unit::UnitPCF8563 unit;
|
|
|
|
volatile uint32_t timer_count{};
|
|
volatile bool timer_fired{};
|
|
bool indicator_lit{};
|
|
|
|
// Mode table: {seconds, repeat}
|
|
struct TimerMode {
|
|
uint8_t seconds;
|
|
bool repeat;
|
|
};
|
|
constexpr TimerMode MODES[] = {
|
|
{5, true}, {10, true}, {30, true}, {5, false}, {10, false}, {0, false},
|
|
};
|
|
size_t mode_idx{};
|
|
|
|
void on_timer()
|
|
{
|
|
timer_count = timer_count + 1;
|
|
timer_fired = true;
|
|
}
|
|
|
|
void canvas_printf(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
|
void canvas_printf(const char* fmt, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
canvas.vprintf(fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
// Draw indicator circle at top-right corner of canvas
|
|
void draw_indicator(bool lit)
|
|
{
|
|
int32_t r = canvas.height() / 20;
|
|
if (r < 4) {
|
|
r = 4;
|
|
}
|
|
int32_t x = canvas.width() - r - 4;
|
|
int32_t y = r + 4;
|
|
canvas.fillCircle(x, y, r, lit ? 2 : 3); // palette: 2=TFT_BLUE, 3=TFT_DARKGREY
|
|
}
|
|
|
|
// Start timer with current mode
|
|
void start_timer()
|
|
{
|
|
auto& m = MODES[mode_idx];
|
|
|
|
if (m.seconds == 0) {
|
|
unit.writeTimer(0);
|
|
M5.Log.printf("Timer stopped\n");
|
|
return;
|
|
}
|
|
|
|
uint32_t actual = unit.writeTimer(static_cast<uint32_t>(m.seconds) * 1000, m.repeat);
|
|
M5.Log.printf("Timer: %lu ms, %s\n", (unsigned long)actual, m.repeat ? "repeat" : "oneshot");
|
|
}
|
|
|
|
} // 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();
|
|
|
|
// Configure timer callback (polling mode)
|
|
auto cfg = unit.config();
|
|
cfg.on_timer = on_timer;
|
|
unit.config(cfg);
|
|
|
|
// 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 system clock from RTC (stored as GMT) and set timezone
|
|
{
|
|
m5::unit::pcf8563::rtc_datetime_t dt{};
|
|
unit.readDateTime(dt);
|
|
struct tm gmt = dt.to_tm();
|
|
time_t t = mktime(&gmt);
|
|
struct timeval tv{t, 0};
|
|
settimeofday(&tv, nullptr);
|
|
setenv("TZ", YOUR_TIMEZONE, 1);
|
|
tzset();
|
|
M5_LOGI("System clock set from RTC (GMT), TZ=%s", YOUR_TIMEZONE);
|
|
}
|
|
|
|
// Create sprite for flicker-free drawing (2-bit palette, internal RAM)
|
|
canvas.setPsram(false);
|
|
canvas.setColorDepth(2);
|
|
int32_t sw = lcd.width() < 320 ? lcd.width() : 320;
|
|
int32_t sh = lcd.height() < 240 ? lcd.height() : 240;
|
|
canvas.createSprite(sw, sh);
|
|
canvas.setFont(lcd.getFont());
|
|
canvas.setTextSize(lcd.getTextSizeX(), lcd.getTextSizeY());
|
|
canvas.setPaletteColor(0, TFT_BLACK);
|
|
canvas.setPaletteColor(1, TFT_WHITE);
|
|
canvas.setPaletteColor(2, TFT_BLUE);
|
|
canvas.setPaletteColor(3, TFT_DARKGREY);
|
|
|
|
// Start with first mode
|
|
start_timer();
|
|
}
|
|
|
|
void loop()
|
|
{
|
|
M5.update();
|
|
Units.update();
|
|
|
|
static time_t prev{};
|
|
time_t now = time(nullptr);
|
|
|
|
// Timer fired: light up indicator + beep
|
|
if (timer_fired) {
|
|
timer_fired = false;
|
|
indicator_lit = true;
|
|
M5.Log.printf("Timer fired! count=%lu\n", (unsigned long)timer_count);
|
|
M5.Speaker.tone(2000, 20);
|
|
prev = 0; // force redraw
|
|
}
|
|
|
|
// BtnA click: cycle mode
|
|
if (M5.BtnA.wasClicked()) {
|
|
mode_idx = (mode_idx + 1) % m5::stl::size(MODES);
|
|
timer_count = 0;
|
|
start_timer();
|
|
prev = 0; // force redraw
|
|
}
|
|
|
|
// Update display every second
|
|
if (now != prev) {
|
|
struct tm local = *localtime(&now);
|
|
|
|
auto& m = MODES[mode_idx];
|
|
uint32_t count = timer_count;
|
|
|
|
// Serial
|
|
M5.Log.printf("%04d-%02d-%02d(%s) %02d:%02d:%02d Timer:%us %s Count:%lu\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, m.seconds, m.repeat ? "repeat" : "oneshot", (unsigned long)count);
|
|
|
|
// Draw to canvas (off-screen), then push to display
|
|
canvas.fillScreen(TFT_BLACK);
|
|
canvas.setCursor(0, 0);
|
|
canvas_printf("%04d-%02d-%02d(%s)\n%02d:%02d:%02d\n\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);
|
|
if (m.seconds == 0) {
|
|
canvas_printf("Timer: OFF\nCount: %lu\n\n[BtnA] Start\n", (unsigned long)count);
|
|
} else {
|
|
canvas_printf("Timer: %u sec\nMode: %s\nCount: %lu\n\n[BtnA] Change\n", m.seconds,
|
|
m.repeat ? "repeat" : "oneshot", (unsigned long)count);
|
|
}
|
|
draw_indicator(indicator_lit);
|
|
indicator_lit = false;
|
|
canvas.pushSprite(0, 0);
|
|
prev = now;
|
|
}
|
|
}
|