Potential for LCD clock, time pulled from network

This commit is contained in:
Wyatt Marchand
2026-08-26 09:19:40 -07:00
parent 5a02d0d359
commit db00c57cb6
3 changed files with 102 additions and 23 deletions
+1
View File
@@ -21,6 +21,7 @@ opt-level = "z"
esp-idf-svc = { version = "0.51", features = ["std"] }
anyhow = "1"
log = "0.4"
hd44780-driver = "0.4"
[build-dependencies]
embuild = "0.33"
+3 -2
View File
@@ -2,11 +2,12 @@
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=4096
# You might have to increase this further if you spawn your own Rust threads
# that allocate large stack variables; or better yet - use
# You might have to increase this further if you spawn your own Rust threads
# that allocate large stack variables; or better yet - use
# `std::thread::Builder::new().stack_size(XXX)` for spawning
CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=4096
CONFIG_ETH_SPI_ETHERNET_W5500=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
CONFIG_LWIP_SNTP_MAX_SERVERS=1
+98 -21
View File
@@ -1,14 +1,47 @@
use std::ffi::CString;
use std::thread;
use std::time::Duration;
use esp_idf_svc::eth::{BlockingEth, EspEth, EthDriver, SpiEthChipset};
use esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::hal::delay::Ets;
use esp_idf_svc::hal::i2c::{I2cConfig, I2cDriver};
use esp_idf_svc::hal::peripherals::Peripherals;
use esp_idf_svc::hal::prelude::*;
use esp_idf_svc::hal::spi::{config::DriverConfig, Dma, SpiDriver};
use esp_idf_svc::handle::RawHandle; // brings .handle() into scope for EspNetif
use esp_idf_svc::handle::RawHandle;
use esp_idf_svc::sntp::{EspSntp, SyncStatus};
use hd44780_driver::HD44780;
use log::info;
use std::ffi::CString;
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
// Row start addresses for 24x4 HD44780-compatible displays.
// Common split, but NOT universal — check your display's datasheet.
const ROW_ADDR: [u8; 4] = [0x00, 0x40, 0x18, 0x58];
fn lcd_set_cursor(
lcd: &mut HD44780<impl hd44780_driver::bus::DataBus>,
row: u8,
col: u8,
delay: &mut Ets,
) {
let addr = ROW_ADDR[row as usize] + col;
lcd.set_cursor_pos(addr, delay).ok();
}
fn lcd_print_line(
lcd: &mut HD44780<impl hd44780_driver::bus::DataBus>,
row: u8,
text: &str,
delay: &mut Ets,
) {
lcd_set_cursor(lcd, row, 0, delay);
// pad/truncate to 24 chars so stale characters don't linger
let mut line = text.to_string();
line.truncate(24);
while line.len() < 24 {
line.push(' ');
}
lcd.write_str(&line, delay).ok();
}
fn main() -> anyhow::Result<()> {
esp_idf_svc::sys::link_patches();
@@ -20,49 +53,93 @@ fn main() -> anyhow::Result<()> {
let spi_driver = SpiDriver::new(
peripherals.spi2,
pins.gpio13, // SCLK
pins.gpio11, // MOSI
Some(pins.gpio12), // MISO
pins.gpio13,
pins.gpio11,
Some(pins.gpio12),
&DriverConfig {
dma: Dma::Auto(4096), // required — default (no DMA) can't move full ethernet frames
dma: Dma::Auto(4096),
..Default::default()
},
)?;
let eth_driver = EthDriver::new_spi(
spi_driver,
pins.gpio10, // INT
Some(pins.gpio14), // CS
Some(pins.gpio9), // RST
pins.gpio10,
Some(pins.gpio14),
Some(pins.gpio9),
SpiEthChipset::W5500,
20.MHz().into(),
Some(&[0x02, 0x00, 0x00, 0x12, 0x34, 0x56]), // locally-administered MAC
None, // default PHY address
Some(&[0x02, 0x00, 0x00, 0x12, 0x34, 0x56]),
None,
sysloop.clone(),
)?;
let eth = EspEth::wrap(eth_driver)?;
// Set hostname BEFORE starting — esp-idf-svc's safe NetifConfiguration has no
// hostname field, so this goes through the raw ESP-IDF binding. It must happen
// before eth.start(), otherwise it won't be included in the DHCP request and
// your router/OS will show a generic name instead (max 32 bytes).
let hostname = CString::new("esp32-eth-device")?;
unsafe {
esp_idf_svc::sys::esp_netif_set_hostname(eth.netif().handle(), hostname.as_ptr());
}
let mut eth = BlockingEth::wrap(eth, sysloop)?;
info!("Starting eth...");
eth.start()?;
info!("Waiting for DHCP lease...");
eth.wait_netif_up()?;
let ip_info = eth.eth().netif().get_ip_info()?;
info!("Got IP: {:?}", ip_info);
// --- SNTP time sync ---
info!("Starting SNTP sync...");
let sntp = EspSntp::new_default()?;
while sntp.get_sync_status() != SyncStatus::Completed {
thread::sleep(Duration::from_millis(200));
}
info!("Time synced.");
// --- I2C LCD setup (SDA=GPIO17, SCL=GPIO16, adjust addr if needed) ---
let i2c_config = I2cConfig::new().baudrate(100.kHz().into());
let i2c = I2cDriver::new(peripherals.i2c0, pins.gpio17, pins.gpio16, &i2c_config)?;
let mut delay = Ets;
let mut lcd = HD44780::new_i2c(i2c, 0x27, &mut delay) // 0x27 is common PCF8574 default; some boards use 0x3F
.map_err(|_| anyhow::anyhow!("LCD init failed"))?;
lcd.reset(&mut delay).ok();
lcd.clear(&mut delay).ok();
loop {
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let (date_str, time_str) = format_utc(now);
lcd_print_line(&mut lcd, 0, "ESP32-S3-ETH", &mut delay);
lcd_print_line(&mut lcd, 1, &date_str, &mut delay);
lcd_print_line(&mut lcd, 2, &time_str, &mut delay);
lcd_print_line(&mut lcd, 3, "", &mut delay);
thread::sleep(Duration::from_secs(1));
}
}
// Minimal UTC breakdown without pulling in chrono — good enough for display.
fn format_utc(unix_secs: u64) -> (String, String) {
let days_since_epoch = unix_secs / 86400;
let secs_of_day = unix_secs % 86400;
let h = secs_of_day / 3600;
let m = (secs_of_day % 3600) / 60;
let s = secs_of_day % 60;
// Civil-from-days algorithm (Howard Hinnant), epoch = 1970-01-01
let z = days_since_epoch as i64 + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let mth = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if mth <= 2 { y + 1 } else { y };
(
format!("{:04}-{:02}-{:02} UTC", y, mth, d),
format!("{:02}:{:02}:{:02}", h, m, s),
)
}