use std::result::Result::Ok;
use std::str;
use std::{ptr, string::String, thread};
use embedded_svc::wifi::{
self, AuthMethod, ClientConfiguration, ClientConnectionStatus, ClientIpStatus, ClientStatus,
Wifi as _,
};
use esp_idf_svc::{
netif::EspNetifStack, nvs::EspDefaultNvs, sysloop::EspSysLoopStack, wifi::EspWifi,
};
use anyhow::*;
use log::*;
use std::sync::Arc;
use std::time::Duration;
// Common IDF stuff
use esp_idf_hal::prelude::*;
use esp_idf_hal::*;
use esp_idf_sys::*;
use time::macros::offset;
use time::OffsetDateTime;
use esp_idf_svc::sntp;
use esp_idf_svc::sntp::SyncStatus;
// Graphic part
use embedded_graphics::image::Image;
use embedded_graphics::mono_font::MonoTextStyle;
use embedded_graphics::pixelcolor::*;
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::*;
use embedded_graphics::text::*;
// Fonts and image
use profont::{PROFONT_18_POINT, PROFONT_24_POINT};
use tinybmp::Bmp;
// Sensors
use shared_bus::BusManagerSimple;
use shtcx::{shtc3, PowerMode};
// RustZX spectrum stuff
use rustzx_core::zx::video::colors::ZXBrightness;
use rustzx_core::zx::video::colors::ZXColor;
mod display;
const TEXT_STYLE: TextStyle = TextStyleBuilder::new()
.alignment(embedded_graphics::text::Alignment::Center)
.baseline(embedded_graphics::text::Baseline::Middle)
.build();
#[toml_cfg::toml_config]
pub struct Config {
#[default("Wokwi-GUEST")]
wifi_ssid: &'static str,
#[default("")]
wifi_pass: &'static str,
}
pub struct Wifi {
default_nvs: Arc<EspDefaultNvs>,
esp_wifi: EspWifi,
netif_stack: Arc<EspNetifStack>,
sys_loop_stack: Arc<EspSysLoopStack>,
}
const MEASUREMENT_DELAY: i32 = 10;
fn main() -> Result<()> {
esp_idf_sys::link_patches();
// Bind the log crate to the ESP Logging facilities
esp_idf_svc::log::EspLogger::initialize_default();
let app_config = CONFIG;
// Set up peripherals and display
let peripherals = Peripherals::take().unwrap();
let mut dp = display::create!(peripherals)?;
show_logo(&mut dp)?;
wifi_image(&mut dp, false, display::color_conv)?;
wifi_connecting(&mut dp, false, display::color_conv)?;
info!(
"About to initialize WiFi (SSID: {}, PASS: {})",
app_config.wifi_ssid, app_config.wifi_pass
);
let _wifi = wifi(app_config.wifi_ssid, app_config.wifi_pass)?;
wifi_connecting(&mut dp, true, display::color_conv)?;
/* Unsafe section is used since it's required, if you're using C functions and datatypes */
unsafe {
let sntp = sntp::EspSntp::new_default()?;
info!("SNTP initialized, waiting for status!");
while sntp.get_sync_status() != SyncStatus::Completed {}
info!("SNTP status received!");
let timer: *mut time_t = ptr::null_mut();
let mut timestamp = esp_idf_sys::time(timer);
let mut actual_date = OffsetDateTime::from_unix_timestamp(timestamp as i64)?
.to_offset(offset!(+2))
.date();
info!(
"{} - {} - {}",
actual_date.to_calendar_date().2,
actual_date.to_calendar_date().1,
actual_date.to_calendar_date().0
);
let mut date_str = format!(
"{}-{}-{}",
actual_date.to_calendar_date().2,
actual_date.to_calendar_date().1,
actual_date.to_calendar_date().0
);
let _now: u64 = 0;
let _time_buf: u64 = 0;
date_flush(&mut dp, &date_str, display::color_conv)?;
weekday_flush(
&mut dp,
&actual_date.weekday().to_string(),
display::color_conv,
)?;
/* So, this feature means, that you're going to use on-board sensors of your RUST-BOARD */
let i2c = peripherals.i2c0;
let sda = peripherals.pins.gpio10;
let scl = peripherals.pins.gpio8;
let config = <i2c::config::MasterConfig as Default>::default().baudrate(100.kHz().into());
let i2c = i2c::Master::<i2c::I2C0, _, _>::new(i2c, i2c::MasterPins { sda, scl }, config)?;
let bus = BusManagerSimple::new(i2c);
// let mut icm = Icm42670::new(bus.acquire_i2c(), Address::Primary).unwrap(); // TBD
let mut sht = shtc3(bus.acquire_i2c());
let device_id = sht.device_identifier().unwrap();
println!("SHTC3 Device ID: {}", device_id);
sht.start_measurement(PowerMode::NormalMode).unwrap();
let mut stupid_temp_counter = MEASUREMENT_DELAY; // temp and humidity will refresh once at minute (now at 10secs)
loop {
timestamp = esp_idf_sys::time(timer);
let raw_time =
OffsetDateTime::from_unix_timestamp(timestamp as i64)?.to_offset(offset!(+2));
stupid_temp_counter -= 1;
time_flush(
&mut dp,
&raw_time.time().to_string()[0..(raw_time.time().to_string().len() - 2)],
display::color_conv,
)?;
if actual_date != raw_time.date() {
actual_date = raw_time.date();
date_str = format!(
"{}-{}-{}",
actual_date.to_calendar_date().2,
actual_date.to_calendar_date().1,
actual_date.to_calendar_date().0
);
date_flush(&mut dp, &date_str, display::color_conv)?;
weekday_flush(
&mut dp,
&actual_date.weekday().to_string(),
display::color_conv,
)?;
}
if stupid_temp_counter == 0 {
let measurement = sht.get_measurement_result().unwrap();
//info!("About to refresh temperature and humidity.");
info!(
"TEMP = {:+.2} °C\t",
measurement.temperature.as_degrees_celsius() as i32
);
info!("RH = {:+.2} %RH", measurement.humidity.as_percent());
let actual_temp = format!(
"{:+.0}°C",
measurement.temperature.as_degrees_celsius() as i32 - 3
); // magic constant -3, cause sensors shows temp which is 3 more, than real
let actual_hum = format!("{:+.0}%RH", measurement.humidity.as_percent());
measurements_flush(
&mut dp,
&actual_temp,
&actual_hum,
true,
display::color_conv,
)?;
/* If your code is panicking here, consider using LowPower mode since NormalMode may cause
code panicking if you're trying to take measurements "too early" */
sht.start_measurement(PowerMode::NormalMode).unwrap();
stupid_temp_counter = MEASUREMENT_DELAY;
}
thread::sleep(Duration::from_secs(1));
}
} // unsafe section
}
fn time_flush<D>(
display: &mut D,
to_print: &str,
color_conv: fn(ZXColor, ZXBrightness) -> D::Color,
) -> anyhow::Result<()>
where
D: DrawTarget + Dimensions,
{
Rectangle::with_center(
display.bounding_box().center() + Size::new(0, 15),
Size::new(132, 40),
)
.into_styled(
PrimitiveStyleBuilder::new()
.fill_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_width(1)
.build(),
)
.draw(display);
Text::with_text_style(
to_print,
display.bounding_box().center() + Size::new(0, 10), //(display.bounding_box().size.height - 10) as i32 / 2),
MonoTextStyle::new(
&PROFONT_24_POINT,
color_conv(ZXColor::Black, ZXBrightness::Normal),
),
TEXT_STYLE,
)
.draw(display);
Ok(())
}
fn date_flush<D>(
display: &mut D,
to_print: &str,
color_conv: fn(ZXColor, ZXBrightness) -> D::Color,
) -> anyhow::Result<()>
where
D: DrawTarget + Dimensions,
{
Rectangle::new(Point::zero(), Size::new(170, 30))
.into_styled(
PrimitiveStyleBuilder::new()
.fill_color(color_conv(ZXColor::White, ZXBrightness::Normal)) /* for date in top-left of screen*/
.stroke_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_width(1)
.build(),
)
.draw(display);
Text::with_alignment(
to_print,
Point::new(5, 20), //(display.bounding_box().size.height - 10) as i32 / 2),
MonoTextStyle::new(
&PROFONT_18_POINT,
color_conv(ZXColor::Black, ZXBrightness::Normal),
),
Alignment::Left,
)
.draw(display);
Ok(())
}
fn weekday_flush<D>(
display: &mut D,
to_print: &str,
color_conv: fn(ZXColor, ZXBrightness) -> D::Color,
) -> anyhow::Result<()>
where
D: DrawTarget + Dimensions,
{
Rectangle::with_center(
display.bounding_box().center() - Size::new(0, 20),
Size::new(140, 30),
)
.into_styled(
PrimitiveStyleBuilder::new()
.fill_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_width(1)
.build(),
)
.draw(display);
Text::with_text_style(
to_print,
display.bounding_box().center() - Size::new(0, 25), //(display.bounding_box().size.height - 10) as i32 / 2),
MonoTextStyle::new(
&PROFONT_24_POINT,
color_conv(ZXColor::Black, ZXBrightness::Normal),
),
TEXT_STYLE,
)
.draw(display);
Ok(())
}
/* if this bool is true => print humidity */
/* otherwise - print "no data" */
fn measurements_flush<D>(
display: &mut D,
to_print_temp: &str,
to_print_hum: &str,
hum_or_nd: bool,
color_conv: fn(ZXColor, ZXBrightness) -> D::Color,
) -> anyhow::Result<()>
where
D: DrawTarget + Dimensions,
{
// temperature
Rectangle::new(
Point::new(display.bounding_box().size.width as i32 - 80, 0),
Size::new(80, 45),
)
.into_styled(
PrimitiveStyleBuilder::new()
.fill_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_width(1)
.build(),
)
.draw(display);
Text::with_text_style(
to_print_temp,
Point::new(display.bounding_box().size.width as i32 - 35, 13), //(display.bounding_box().size.height - 10) as i32 / 2),
MonoTextStyle::new(
&PROFONT_18_POINT,
color_conv(ZXColor::Black, ZXBrightness::Normal),
),
TEXT_STYLE,
)
.draw(display);
// humidity
Rectangle::new(
Point::new(
display.bounding_box().size.width as i32 - 80,
display.bounding_box().size.height as i32 - 50,
),
Size::new(120, 40),
)
.into_styled(
PrimitiveStyleBuilder::new()
.fill_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_width(1)
.build(),
)
.draw(display);
if hum_or_nd {
//print humidity
Text::with_text_style(
to_print_hum,
Point::new(
display.bounding_box().size.width as i32 - 50,
display.bounding_box().size.height as i32 - 20,
),
MonoTextStyle::new(
&PROFONT_18_POINT,
color_conv(ZXColor::Black, ZXBrightness::Normal),
),
TEXT_STYLE,
)
.draw(display);
} else {
// print "No Data"
Text::with_text_style(
to_print_hum,
Point::new(
display.bounding_box().size.width as i32 - 35,
display.bounding_box().size.height as i32 - 40,
),
MonoTextStyle::new(
&PROFONT_18_POINT,
color_conv(ZXColor::Black, ZXBrightness::Normal),
),
TEXT_STYLE,
)
.draw(display);
}
Ok(())
}
fn show_logo<D>(display: &mut D) -> anyhow::Result<()>
where
D: DrawTarget<Color = embedded_graphics::pixelcolor::Rgb565> + Dimensions,
{
info!("Welcome!");
/* big logo at first */
display.clear(display::color_conv(ZXColor::White, ZXBrightness::Normal));
let bmp = Bmp::<Rgb565>::from_slice(include_bytes!("../assets/esp-rs-big.bmp")).unwrap();
Image::new(&bmp, display.bounding_box().center() - Size::new(100, 100)).draw(display);
thread::sleep(Duration::from_secs(5));
/* than small */
display.clear(display::color_conv(ZXColor::White, ZXBrightness::Normal));
let bmp = Bmp::<Rgb565>::from_slice(include_bytes!("../assets/esp-rs-small.bmp")).unwrap();
Image::new(
&bmp,
Point::new(0, display.bounding_box().size.height as i32 - 50),
)
.draw(display);
Ok(())
}
pub fn wifi(ssid: &str, psk: &str) -> anyhow::Result<Wifi> {
let mut auth_method = AuthMethod::WPA2Personal; // Todo: add this setting - router dependent
if ssid.is_empty() {
anyhow::bail!("missing WiFi name")
}
if psk.is_empty() {
auth_method = AuthMethod::None;
info!("Wifi password is empty");
}
let netif_stack = Arc::new(EspNetifStack::new()?);
let sys_loop_stack = Arc::new(EspSysLoopStack::new()?);
let default_nvs = Arc::new(EspDefaultNvs::new()?);
let mut wifi = EspWifi::new(
netif_stack.clone(),
sys_loop_stack.clone(),
default_nvs.clone(),
)?;
info!("Searching for Wifi network {}", ssid);
let ap_infos = wifi.scan()?;
let ours = ap_infos.into_iter().find(|a| a.ssid == ssid);
let channel = if let Some(ours) = ours {
info!(
"Found configured access point {} on channel {}",
ssid, ours.channel
);
Some(ours.channel)
} else {
info!(
"Configured access point {} not found during scanning, will go with unknown channel",
ssid
);
None
};
info!("setting Wifi configuration");
wifi.set_configuration(&wifi::Configuration::Client(ClientConfiguration {
ssid: ssid.into(),
password: psk.into(),
channel,
auth_method,
..Default::default()
}))?;
info!("getting Wifi status");
wifi.wait_status_with_timeout(Duration::from_secs(2100), |status| {
!status.is_transitional()
})
.map_err(|err| anyhow::anyhow!("Unexpected Wifi status (Transitional state): {:?}", err))?;
let status = wifi.get_status();
if let wifi::Status(
ClientStatus::Started(ClientConnectionStatus::Connected(ClientIpStatus::Done(
_ip_settings,
))),
_,
) = status
{
info!("Wifi connected");
} else {
bail!(
"Could not connect to Wifi - Unexpected Wifi status: {:?}",
status
);
}
let wifi = Wifi {
esp_wifi: wifi,
netif_stack,
sys_loop_stack,
default_nvs,
};
Ok(wifi)
}
/* if this bool is true => wifi connected */
fn wifi_connecting<D>(
display: &mut D,
connected: bool,
color_conv: fn(ZXColor, ZXBrightness) -> D::Color,
) -> anyhow::Result<()>
where
D: DrawTarget<Color = embedded_graphics::pixelcolor::Rgb565> + Dimensions,
{
Rectangle::with_center(
display.bounding_box().center(),
Size::new(display.bounding_box().size.width, 80),
)
.into_styled(
PrimitiveStyleBuilder::new()
.fill_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_width(1)
.build(),
)
.draw(display);
if connected {
Text::with_text_style(
"Wi-Fi connected",
display.bounding_box().center() - Size::new(0, 25), //(display.bounding_box().size.height - 10) as i32 / 2),
MonoTextStyle::new(
&PROFONT_24_POINT,
color_conv(ZXColor::Black, ZXBrightness::Normal),
),
TEXT_STYLE,
)
.draw(display);
wifi_image(display, true, color_conv);
thread::sleep(Duration::from_secs(2));
Rectangle::with_center(
display.bounding_box().center(),
Size::new(display.bounding_box().size.width, 80),
)
.into_styled(
PrimitiveStyleBuilder::new()
.fill_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_width(1)
.build(),
)
.draw(display);
} else {
Text::with_text_style(
"Connecting Wi-Fi...",
display.bounding_box().center() - Size::new(0, 25), //(display.bounding_box().size.height - 10) as i32 / 2),
MonoTextStyle::new(
&PROFONT_24_POINT,
color_conv(ZXColor::Black, ZXBrightness::Normal),
),
TEXT_STYLE,
)
.draw(display);
}
Ok(())
}
/* if this bool is true => draw "WiFi connected image" */
/* otherwise - overcrossed WiFi image */
fn wifi_image<D>(
display: &mut D,
wifi: bool,
color_conv: fn(ZXColor, ZXBrightness) -> D::Color,
) -> anyhow::Result<()>
where
D: DrawTarget<Color = embedded_graphics::pixelcolor::Rgb565> + Dimensions,
{
if wifi {
Rectangle::new(
Point::new(50, display.bounding_box().size.height as i32 - 50),
Size::new(50, 50),
)
.into_styled(
PrimitiveStyleBuilder::new()
.fill_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_color(color_conv(ZXColor::White, ZXBrightness::Normal))
.stroke_width(1)
.build(),
)
.draw(display);
let bmp = Bmp::<Rgb565>::from_slice(include_bytes!("../assets/wifi.bmp")).unwrap();
Image::new(
&bmp,
Point::new(53, display.bounding_box().size.height as i32 - 50),
)
.draw(display);
} else {
let bmp =
Bmp::<Rgb565>::from_slice(include_bytes!("../assets/wifi_not_connected.bmp")).unwrap();
Image::new(
&bmp,
Point::new(53, display.bounding_box().size.height as i32 - 50),
)
.draw(display);
}
Ok(())
}