Skip to main content

embedded_wg_display/
main.rs

1#![no_std]
2#![no_main]
3#![feature(impl_trait_in_assoc_type)]
4#![deny(
5    clippy::mem_forget,
6    reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
7    holding buffers for the duration of a data transfer."
8)]
9#![deny(clippy::large_stack_frames)]
10#![recursion_limit = "256"]
11
12use defmt::error;
13use defmt::info;
14use embassy_executor::Spawner;
15use embassy_time::{Duration, Timer};
16use esp_hal::clock::CpuClock;
17use esp_hal::gpio::{Input, InputConfig, Pull};
18use esp_hal::interrupt::software::SoftwareInterruptControl;
19use esp_hal::system::Stack as CoreStack;
20use esp_hal::system::software_reset;
21use esp_hal::timer::timg::TimerGroup;
22use esp_println as _;
23use esp_rtos::embassy::Executor;
24use static_cell::StaticCell;
25
26mod wifi;
27use crate::display::DisplayPeripherals;
28use crate::wifi::Wifi;
29
30mod util;
31mod widget;
32use crate::util::globals;
33
34mod display;
35use crate::display::Display;
36
37mod runtime;
38
39mod storage;
40use crate::storage::Storage;
41
42mod renderer;
43
44mod http_client;
45
46mod http_server;
47
48use crate::alloc::string::ToString;
49use crate::util::esptime::EspTime;
50
51#[panic_handler]
52fn panic(info: &core::panic::PanicInfo) -> ! {
53    if let Some(location) = info.location() {
54        error!(
55            "Panic occurred at {=str}:{=u32}",
56            location.file(),
57            location.line()
58        );
59    } else {
60        error!("Panic occurred at unknown location");
61    }
62
63    esp_println::println!("panic info: {}", info);
64
65    software_reset();
66}
67
68extern crate alloc;
69
70// This creates a default app-descriptor required by the esp-idf bootloader.
71// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
72esp_bootloader_esp_idf::esp_app_desc!();
73
74#[allow(
75    clippy::large_stack_frames,
76    reason = "it's not unusual to allocate larger buffers etc. in main"
77)]
78#[esp_rtos::main]
79async fn main(spawner: Spawner) -> ! {
80    // generator version: 1.2.0
81
82    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
83    let peripherals = esp_hal::init(config);
84
85    // initalizeing PSRAM before heap fixes widget http host function access for some reason
86    esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram);
87    esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 73744);
88    //  esp_alloc::heap_allocator!(size: 73 * 1024);
89
90    // Setup software interrupts for executors
91    let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
92
93    let timg0 = TimerGroup::new(peripherals.TIMG0);
94    // Note: On Xtensa, esp_rtos::start doesn't take a software interrupt parameter
95    esp_rtos::start(timg0.timer0);
96
97    info!("Embassy initialized!");
98
99    // -- Storage setup --
100    let storage =
101        Storage::new(peripherals.FLASH, peripherals.SHA).expect("Failed to initialize storage");
102    globals::init_storage(storage).await;
103
104    // -- Display setup --
105    let display = Display::new(DisplayPeripherals {
106        spi2: peripherals.SPI2,
107        dma_ch0: peripherals.DMA_CH0,
108        gpio4: peripherals.GPIO4,
109        gpio5: peripherals.GPIO5,
110        gpio6: peripherals.GPIO6,
111        gpio7: peripherals.GPIO7,
112        gpio47: peripherals.GPIO47,
113        gpio48: peripherals.GPIO48,
114    });
115
116    globals::init_display(display).await;
117
118    globals::console_println("WG-Display starting up").await;
119
120    // -- Wifi setup --
121    let wifi_creds = globals::with_storage(|storage| storage.get_wifi_credentials()).await;
122
123    // check current wifi mode, if nothing is set (first boot) or wifi connection fails this will be set to ap (Access Point) mode
124    // otherwise the device will start in station mode and try to connect to the set wifi network, will switch back to ap mode if this fails
125    let wifi_mode = globals::with_storage(|storage| storage.config_get("wifi_mode"))
126        .await
127        .unwrap_or_else(|_| alloc::string::String::from("ap"));
128    let force_ap_mode = wifi_mode == "ap";
129
130    let wifi_peripheral = peripherals.WIFI;
131
132    spawner.spawn(reset_button_poll(peripherals.GPIO0)).ok();
133
134    // start in station mode
135    if !force_ap_mode {
136        // check if wifi credentials are present, otherwise swicht to ap mode
137        match wifi_creds {
138            Ok(creds) => {
139                start_station_mode(wifi_peripheral, &spawner, creds.ssid, creds.password).await;
140                // -- Init and start widget runner on second core --
141                static APP_CORE_STACK: StaticCell<CoreStack<32768>> = StaticCell::new();
142                let app_core_stack = APP_CORE_STACK.init(CoreStack::new());
143
144                esp_rtos::start_second_core(
145                    peripherals.CPU_CTRL,
146                    sw_int.software_interrupt0,
147                    sw_int.software_interrupt1,
148                    app_core_stack,
149                    || {
150                        static CORE1_EXECUTOR: StaticCell<Executor> = StaticCell::new();
151                        let executor = CORE1_EXECUTOR.init(Executor::new());
152
153                        executor.run(|core1_spawner| {
154                            core1_spawner
155                                .spawn(widget_runner())
156                                .expect("Failed to spawn widget runner on core1");
157                            info!("Widget runner task spawned on core1");
158                        });
159                    },
160                );
161            }
162            Err(_) => {
163                info!("WiFi credentials not found in storage, starting in AP mode");
164                start_ap_mode(wifi_peripheral, &spawner).await;
165            }
166        }
167    } else {
168        info!("WiFi mode is set to AP, starting in AP mode");
169        start_ap_mode(wifi_peripheral, &spawner).await;
170    }
171
172    loop {
173        Timer::after(Duration::from_millis(500)).await;
174    }
175}
176
177async fn start_ap_mode(wifi_peripheral: esp_hal::peripherals::WIFI<'static>, spawner: &Spawner) {
178    globals::console_println("No wifi configured, starting in AP mode").await;
179    let _ = globals::with_storage(|storage| storage.config_set("wifi_mode", "ap")).await;
180    let wifi = Wifi::start_station(wifi_peripheral, spawner, "".into(), "".into(), true);
181
182    // -- Server setup --
183    http_server::start(wifi.stack(), wifi.tls_seed(), spawner);
184    globals::console_println("Connect to 'WG-Display-AP'").await;
185    globals::console_println("and open 192.168.2.1").await;
186}
187
188async fn start_station_mode(
189    wifi_peripheral: esp_hal::peripherals::WIFI<'static>,
190    spawner: &Spawner,
191    ssid: alloc::string::String,
192    password: alloc::string::String,
193) {
194    globals::console_println("Starting in station mode").await;
195    let _ = globals::with_storage(|storage| storage.config_set("wifi_mode", "station")).await;
196    let wifi = Wifi::start_station(wifi_peripheral, spawner, ssid, password, false);
197    let ip = wifi.wait_for_connection().await;
198    let _ = globals::with_storage(|storage| storage.config_set("device_ip", &ip.to_string())).await;
199
200    // -- Spawn HTTP handler task for widget runtime --
201    spawner
202        .spawn(runtime::http_sync::http_handler_task(
203            wifi.stack(),
204            wifi.tls_seed(),
205        ))
206        .expect("Failed to spawn HTTP handler task");
207    info!("HTTP handler task spawned on core0 executor");
208
209    // init widget store
210    let mut widget_store = widget::store::WidgetStore::new();
211    widget_store
212        .fetch_from_store()
213        .await
214        .expect("Failed to fetch widget store");
215    globals::init_store(widget_store).await;
216
217    // -- Server setup --
218    http_server::start(wifi.stack(), wifi.tls_seed(), spawner);
219
220    let mut esp_time = EspTime::new();
221    esp_time.fetch_time().await;
222    globals::init_time(esp_time);
223    info!("Global time synced from time API");
224}
225
226/// Renderer task look that is spawned on the second core
227#[embassy_executor::task]
228async fn widget_runner() {
229    info!("Widget runner task started");
230
231    let mut renderer = renderer::Renderer::new();
232    // will loop forever
233    renderer.run().await;
234}
235
236#[embassy_executor::task]
237async fn reset_button_poll(gpio0: esp_hal::peripherals::GPIO0<'static>) -> ! {
238    let boot_button = Input::new(gpio0, InputConfig::default().with_pull(Pull::Up));
239    let mut boot_button_timer = 0;
240
241    loop {
242        if boot_button.is_low() {
243            boot_button_timer += 1;
244            if boot_button_timer >= 100 {
245                globals::console_println("Boot button held, resetting WiFi settings").await;
246                let _ =
247                    globals::with_storage(|storage| storage.config_set("wifi_mode", "ap")).await;
248                software_reset();
249            }
250        }
251
252        if boot_button.is_high() && boot_button_timer != 0 {
253            boot_button_timer = 0;
254        }
255
256        Timer::after(Duration::from_millis(50)).await;
257    }
258}