From e28749190677129050a89a82fb16664ca23b8574 Mon Sep 17 00:00:00 2001 From: lukas Date: Sun, 2 Aug 2026 14:52:37 +0200 Subject: [PATCH] Moved nfc and wifi into dedicated file --- src/bin/main.rs | 323 +-------------------------------- src/card/decoder.rs | 73 +++++++- src/card/model.rs | 12 +- src/display/sprite.rs | 63 ++++++- src/display/views/card_view.rs | 99 +++++++++- src/lib.rs | 2 + src/network/wifi.rs | 68 ++++++- 7 files changed, 316 insertions(+), 324 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index c118785..8467582 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -7,24 +7,15 @@ )] #![deny(clippy::large_stack_frames)] -use alloc::{ - format, - string::{String, ToString}, -}; -use base64::{Engine as _, engine::general_purpose}; use creaturedex::card::mock::{MOCK_PAYLOAD, MOCK_PAYLOAD2}; -use creaturedex::display::palette::{PALETTES, Rgb, TYPE_STYLES}; +use creaturedex::display::views::card_view::render_nfc_payload; +use creaturedex::network::wifi::setup_wifi; +use embedded_graphics::draw_target::DrawTarget; use display_interface_spi::SPIInterface; use embassy_executor::Spawner; use embassy_time::Timer; -use embedded_graphics::mono_font::MonoTextStyle; -use embedded_graphics::mono_font::ascii::FONT_6X10; use embedded_graphics::pixelcolor::Rgb565; -use embedded_graphics::prelude::Point; use embedded_graphics::prelude::RgbColor; -use embedded_graphics::text::Text; -use embedded_graphics::{Drawable, geometry::Size, primitives::Rectangle}; -use embedded_graphics_core::draw_target::DrawTarget; use embedded_hal_bus::spi::ExclusiveDevice; use esp_hal::clock::CpuClock; use esp_hal::delay::Delay; @@ -53,312 +44,6 @@ extern crate alloc; // For more information see: esp_bootloader_esp_idf::esp_app_desc!(); - - - - -#[derive(Debug, Clone, Copy)] -struct NfcPayload<'a> { - pub event_encoding: &'a [u8], - pub card_type: u8, - pub card_uuid: &'a [u8], - pub _reserved: &'a [u8], - pub sprite: &'a [u8], - pub packed_card_text: &'a [u8], - pub secret: &'a [u8], - pub _opaque_trailer: &'a [u8], -} - -fn split_nfc_hex(payload: &[u8]) -> Option> { - if payload.len() < 0x35A { - return None; - } - - Some(NfcPayload { - event_encoding: &payload[0..2], - card_type: payload[2], - card_uuid: &payload[3..5], - _reserved: &payload[5..9], - sprite: &payload[9..0x2DB], - packed_card_text: &payload[0x2DB..0x311], - secret: &payload[0x311..0x329], - _opaque_trailer: &payload[0x329..0x35A], - }) -} - -fn render_sprite_onto_ili9341(ili9341: &mut D, sprite: &[u8], palette: &[Rgb; 16]) -where - D: DrawTarget, -{ - let pixel_count = sprite.len() * 2; - if pixel_count == 0 { - return; - } - - // OPTIMIZATION 1: Pre-compute the 16-color palette to Rgb565 once. - let mut palette565 = [Rgb565::new(0, 0, 0); 16]; - for (i, Rgb(r, g, b)) in palette.iter().enumerate() { - palette565[i] = Rgb565::new(*r >> 3, *g >> 2, *b >> 3); - } - - let mut src_width = 1usize; - while src_width * src_width < pixel_count { - src_width += 1; - } - - let target_width = 240usize; - let scale = core::cmp::max(1, target_width / src_width); - - for y in 0..src_width { - for x in 0..src_width { - let pixel_index = y * src_width + x; - if pixel_index >= pixel_count { - break; - } - - // Bitwise operations are slightly faster than division/modulo - let byte_index = pixel_index >> 1; - let pixel_byte = sprite[byte_index]; - let color_index = if pixel_index.is_multiple_of(2) { - (pixel_byte >> 4) & 0x0F - } else { - pixel_byte & 0x0F - }; - - let color = palette565[color_index as usize]; - - let start_x = (x * scale) as i32; - let start_y = (y * scale) as i32; - let end_x = core::cmp::min(start_x + scale as i32, target_width as i32); - let end_y = core::cmp::min(start_y + scale as i32, target_width as i32); - - let width = u32::try_from(end_x - start_x).unwrap(); - let height = u32::try_from(end_y - start_y).unwrap(); - - let area = Rectangle::new( - Point::new(start_x.try_into().unwrap(), start_y.try_into().unwrap()), - Size::new(width, height), - ); - - let _ = ili9341.fill_solid(&area, color); - } - } -} - -fn decode_known_event(event_encoding: &[u8]) -> Option<&'static str> { - match event_encoding { - [0x39, 0xC3] => Some("39C3"), - [0x40, 0xC3] => Some("40C3"), // future proofing :) - [0xE9, 0x24] => Some("GPN 24"), - [0xE9, 0x25] => Some("GPN 25"), // future proofing :) - _ => None, - } -} - -fn decode_secret(hex: &[u8]) -> alloc::string::String { - let mut ascii_string = alloc::string::String::new(); - for &byte in hex { - if byte == 0 { - break; - } - - if byte.is_ascii_graphic() || byte == b' ' { - ascii_string.push(byte as char); - } - } - ascii_string -} - -fn decode_packed_card_text(bytes: &[u8]) -> (String, String, String, String) { - let alphabet = b" ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-"; - let mut full_text = String::with_capacity((bytes.len() * 8) / 6 + 1); - - let mut accumulator: u32 = 0; - let mut bits_in_acc: u32 = 0; - - for &byte in bytes { - accumulator |= (byte as u32) << bits_in_acc; - bits_in_acc += 8; - - while bits_in_acc >= 6 { - let char_index = (accumulator & 0x3F) as usize; - - full_text.push(alphabet[char_index] as char); - - accumulator >>= 6; - bits_in_acc -= 6; - } - } - - let name = full_text[0..24].trim().to_string(); - let trait1 = full_text[24..40].trim().to_string(); - let trait2 = full_text[40..56].trim().to_string(); - let trait3 = full_text[56..72].trim().to_string(); - - (name, trait1, trait2, trait3) -} - -fn render_nfc_payload(display: &mut D, payload: &[u8; 858]) -where - D: DrawTarget, - D::Error: core::fmt::Debug, -{ - let nfc_payload: Option> = split_nfc_hex(payload); - // let simulated_time: u64 = 1780757711176; // Simulated time in milliseconds since epoch (2023-06-05 12:15:11 UTC) - // let mut final_payload = [0u8; 867]; - - // final_payload[..858].copy_from_slice(payload); - // final_payload[858..866].copy_from_slice(&simulated_time.to_be_bytes()); - // final_payload[866] = 0x00; - - // let b64_string = general_purpose::STANDARD.encode(&final_payload); - // log::info!("Base64 String:"); - // log::info!("{}", b64_string); - - if let Some(nfc_payload) = nfc_payload { - let (name, trait1, trait2, trait3) = decode_packed_card_text(nfc_payload.packed_card_text); - let secret = decode_secret(nfc_payload.secret); - - let background = Rectangle::new( - Point::new(0, 0), - Size::new(240, 320), - ); - let bg_color: Rgb565 = TYPE_STYLES[(nfc_payload.card_type - 1) as usize] - .primary - .into_rgb565(); - - let _ = display.fill_solid(&background, bg_color); - - let style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE); - Text::new(&format!("Name: {}", name), Point::new(20, 240), style) - .draw(display) - .unwrap(); - Text::new( - &format!( - "Type: {}", - TYPE_STYLES[(nfc_payload.card_type - 1) as usize].name - ), - Point::new(20, 250), - style, - ) - .draw(display) - .unwrap(); - Text::new( - &format!( - "Event: {}", - decode_known_event(nfc_payload.event_encoding).unwrap_or("Unknown") - ), - Point::new(20, 260), - style, - ) - .draw(display) - .unwrap(); - Text::new( - &format!( - "UUID: {}", - nfc_payload - .card_uuid - .iter() - .map(|b| format!("{:02X}", b)) - .collect::() - ), - Point::new(20, 270), - style, - ) - .draw(display) - .unwrap(); - Text::new(&format!("Trait1: {}", trait1), Point::new(20, 280), style) - .draw(display) - .unwrap(); - Text::new(&format!("Trait2: {}", trait2), Point::new(20, 290), style) - .draw(display) - .unwrap(); - Text::new(&format!("Trait3: {}", trait3), Point::new(20, 300), style) - .draw(display) - .unwrap(); - Text::new(&format!("Secret: {}", secret), Point::new(20, 310), style) - .draw(display) - .unwrap(); - - render_sprite_onto_ili9341( - display, - nfc_payload.sprite, - &PALETTES[(nfc_payload.card_type - 1) as usize], - ); - } else { - log::error!( - "NFC payload too short to decode packed text: {} bytes", - payload.len() - ); - } -} - -#[cfg(not(feature = "wokwi"))] -async fn setup_wifi( - wifi: esp_hal::peripherals::WIFI<'static>, - flash: esp_hal::peripherals::FLASH<'static>, - spawner: &Spawner, -) { - let nvs = esp_hal_wifimanager::Nvs::new(0x9000, 0x6000, flash).unwrap(); - - let mut wm_settings = esp_hal_wifimanager::WmSettings::default(); - - wm_settings.ssid.clear(); - _ = core::fmt::write( - &mut wm_settings.ssid, - format_args!("CreatureDex-{:X}", esp_hal_wifimanager::get_efuse_mac()), - ); - - wm_settings.wifi_conn_timeout = 30000; - wm_settings.esp_reset_timeout = Some(300000); // 5min - - let wifi_res = esp_hal_wifimanager::init_wm(wm_settings, spawner, Some(&nvs), wifi, None).await; - - log::info!("wifi_res: {wifi_res:?}"); -} - -#[cfg(feature = "wokwi")] -async fn setup_wifi( - wifi: esp_hal::peripherals::WIFI<'static>, - flash: esp_hal::peripherals::FLASH<'static>, - spawner: &Spawner, -) { - log::info!("Wokwi mode active: pre-seeding WiFi Manager with Wokwi credentials"); - - // Preload connection data so init_wm can connect immediately without AP setup. - let nvs = match esp_hal_wifimanager::Nvs::new(0x9000, 0x6000, flash) { - Ok(nvs) => nvs, - Err(e) => { - log::error!("Failed to initialize NVS for Wokwi: {e:?}"); - return; - } - }; - - if let Err(e) = nvs - .set( - esp_hal_wifimanager::WIFI_NVS_KEY, - "{\"ssid\":\"Wokwi-GUEST\",\"psk\":\"\",\"data\":{}}", - ) - .await - { - log::error!("Failed to store Wokwi WiFi config in NVS: {e:?}"); - return; - } - - let mut wm_settings = esp_hal_wifimanager::WmSettings::default(); - - // Wokwi simulation uses a default open network called "Wokwi-GUEST" - wm_settings.ssid.clear(); - _ = core::fmt::write(&mut wm_settings.ssid, format_args!("Wokwi-GUEST")); - - wm_settings.wifi_conn_timeout = 30000; - - // Initialize Wi-Fi connection through WiFi Manager using the pre-seeded NVS data. - let wifi_res = esp_hal_wifimanager::init_wm(wm_settings, spawner, Some(&nvs), wifi, None).await; - - log::info!("Wokwi wifi_res: {wifi_res:?}"); -} - #[allow( clippy::large_stack_frames, reason = "it's not unusual to allocate larger buffers etc. in main" @@ -435,7 +120,7 @@ async fn main(spawner: Spawner) { - //setup_wifi(peripherals.WIFI, peripherals.FLASH, &spawner).await; + // setup_wifi(peripherals.WIFI, peripherals.FLASH, &spawner).await; log::info!("Setup complete, entering main loop"); diff --git a/src/card/decoder.rs b/src/card/decoder.rs index d5d29ca..9c865e8 100644 --- a/src/card/decoder.rs +++ b/src/card/decoder.rs @@ -1 +1,72 @@ -// Card decoder module +use alloc::string::{String, ToString}; +use crate::card::model::NfcPayload; + +pub fn split_nfc_hex(payload: &[u8]) -> Option> { + if payload.len() < 0x35A { + return None; + } + + Some(NfcPayload { + event_encoding: &payload[0..2], + card_type: payload[2], + card_uuid: &payload[3..5], + _reserved: &payload[5..9], + sprite: &payload[9..0x2DB], + packed_card_text: &payload[0x2DB..0x311], + secret: &payload[0x311..0x329], + _opaque_trailer: &payload[0x329..0x35A], + }) +} + +pub fn decode_known_event(event_encoding: &[u8]) -> Option<&'static str> { + match event_encoding { + [0x39, 0xC3] => Some("39C3"), + [0x40, 0xC3] => Some("40C3"), // future proofing :) + [0xE9, 0x24] => Some("GPN 24"), + [0xE9, 0x25] => Some("GPN 25"), // future proofing :) + _ => None, + } +} + +pub fn decode_secret(hex: &[u8]) -> String { + let mut ascii_string = String::new(); + for &byte in hex { + if byte == 0 { + break; + } + + if byte.is_ascii_graphic() || byte == b' ' { + ascii_string.push(byte as char); + } + } + ascii_string +} + +pub fn decode_packed_card_text(bytes: &[u8]) -> (String, String, String, String) { + let alphabet = b" ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-"; + let mut full_text = String::with_capacity((bytes.len() * 8) / 6 + 1); + + let mut accumulator: u32 = 0; + let mut bits_in_acc: u32 = 0; + + for &byte in bytes { + accumulator |= (byte as u32) << bits_in_acc; + bits_in_acc += 8; + + while bits_in_acc >= 6 { + let char_index = (accumulator & 0x3F) as usize; + + full_text.push(alphabet[char_index] as char); + + accumulator >>= 6; + bits_in_acc -= 6; + } + } + + let name = full_text[0..24].trim().to_string(); + let trait1 = full_text[24..40].trim().to_string(); + let trait2 = full_text[40..56].trim().to_string(); + let trait3 = full_text[56..72].trim().to_string(); + + (name, trait1, trait2, trait3) +} diff --git a/src/card/model.rs b/src/card/model.rs index d27c187..d18b837 100644 --- a/src/card/model.rs +++ b/src/card/model.rs @@ -1 +1,11 @@ -// Card model module +#[derive(Debug, Clone, Copy)] +pub struct NfcPayload<'a> { + pub event_encoding: &'a [u8], + pub card_type: u8, + pub card_uuid: &'a [u8], + pub _reserved: &'a [u8], + pub sprite: &'a [u8], + pub packed_card_text: &'a [u8], + pub secret: &'a [u8], + pub _opaque_trailer: &'a [u8], +} diff --git a/src/display/sprite.rs b/src/display/sprite.rs index c931da1..a5ec12d 100644 --- a/src/display/sprite.rs +++ b/src/display/sprite.rs @@ -1 +1,62 @@ -// Display sprite module +use embedded_graphics::{geometry::Size, pixelcolor::Rgb565, primitives::Rectangle, prelude::Point}; +use embedded_graphics_core::draw_target::DrawTarget; +use crate::display::palette::Rgb; + +pub fn render_sprite_onto_ili9341(ili9341: &mut D, sprite: &[u8], palette: &[Rgb; 16]) +where + D: DrawTarget, +{ + let pixel_count = sprite.len() * 2; + if pixel_count == 0 { + return; + } + + // OPTIMIZATION 1: Pre-compute the 16-color palette to Rgb565 once. + let mut palette565 = [Rgb565::new(0, 0, 0); 16]; + for (i, Rgb(r, g, b)) in palette.iter().enumerate() { + palette565[i] = Rgb565::new(*r >> 3, *g >> 2, *b >> 3); + } + + let mut src_width = 1usize; + while src_width * src_width < pixel_count { + src_width += 1; + } + + let target_width = 240usize; + let scale = core::cmp::max(1, target_width / src_width); + + for y in 0..src_width { + for x in 0..src_width { + let pixel_index = y * src_width + x; + if pixel_index >= pixel_count { + break; + } + + // Bitwise operations are slightly faster than division/modulo + let byte_index = pixel_index >> 1; + let pixel_byte = sprite[byte_index]; + let color_index = if pixel_index.is_multiple_of(2) { + (pixel_byte >> 4) & 0x0F + } else { + pixel_byte & 0x0F + }; + + let color = palette565[color_index as usize]; + + let start_x = (x * scale) as i32; + let start_y = (y * scale) as i32; + let end_x = core::cmp::min(start_x + scale as i32, target_width as i32); + let end_y = core::cmp::min(start_y + scale as i32, target_width as i32); + + let width = u32::try_from(end_x - start_x).unwrap(); + let height = u32::try_from(end_y - start_y).unwrap(); + + let area = Rectangle::new( + Point::new(start_x.try_into().unwrap(), start_y.try_into().unwrap()), + Size::new(width, height), + ); + + let _ = ili9341.fill_solid(&area, color); + } + } +} diff --git a/src/display/views/card_view.rs b/src/display/views/card_view.rs index 76dfdbd..d298883 100644 --- a/src/display/views/card_view.rs +++ b/src/display/views/card_view.rs @@ -1 +1,98 @@ -// Card view module +use alloc::{format, string::String}; +use embedded_graphics::mono_font::MonoTextStyle; +use embedded_graphics::mono_font::ascii::FONT_6X10; +use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::prelude::Point; +use embedded_graphics::prelude::RgbColor; +use embedded_graphics::text::Text; +use embedded_graphics::{Drawable, geometry::Size, primitives::Rectangle}; +use embedded_graphics_core::draw_target::DrawTarget; +use crate::card::decoder::{decode_known_event, decode_packed_card_text, decode_secret, split_nfc_hex}; +use crate::card::model::NfcPayload; +use crate::display::palette::{PALETTES, TYPE_STYLES}; +use crate::display::sprite::render_sprite_onto_ili9341; + +pub fn render_nfc_payload(display: &mut D, payload: &[u8; 858]) +where + D: DrawTarget, + D::Error: core::fmt::Debug, +{ + let nfc_payload: Option> = split_nfc_hex(payload); + + if let Some(nfc_payload) = nfc_payload { + let (name, trait1, trait2, trait3) = decode_packed_card_text(nfc_payload.packed_card_text); + let secret = decode_secret(nfc_payload.secret); + + let background = Rectangle::new( + Point::new(0, 0), + Size::new(240, 320), + ); + let bg_color: Rgb565 = TYPE_STYLES[(nfc_payload.card_type - 1) as usize] + .primary + .into_rgb565(); + + let _ = display.fill_solid(&background, bg_color); + + let style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE); + Text::new(&format!("Name: {}", name), Point::new(20, 240), style) + .draw(display) + .unwrap(); + Text::new( + &format!( + "Type: {}", + TYPE_STYLES[(nfc_payload.card_type - 1) as usize].name + ), + Point::new(20, 250), + style, + ) + .draw(display) + .unwrap(); + Text::new( + &format!( + "Event: {}", + decode_known_event(nfc_payload.event_encoding).unwrap_or("Unknown") + ), + Point::new(20, 260), + style, + ) + .draw(display) + .unwrap(); + Text::new( + &format!( + "UUID: {}", + nfc_payload + .card_uuid + .iter() + .map(|b| format!("{:02X}", b)) + .collect::() + ), + Point::new(20, 270), + style, + ) + .draw(display) + .unwrap(); + Text::new(&format!("Trait1: {}", trait1), Point::new(20, 280), style) + .draw(display) + .unwrap(); + Text::new(&format!("Trait2: {}", trait2), Point::new(20, 290), style) + .draw(display) + .unwrap(); + Text::new(&format!("Trait3: {}", trait3), Point::new(20, 300), style) + .draw(display) + .unwrap(); + Text::new(&format!("Secret: {}", secret), Point::new(20, 310), style) + .draw(display) + .unwrap(); + + render_sprite_onto_ili9341( + display, + nfc_payload.sprite, + &PALETTES[(nfc_payload.card_type - 1) as usize], + ); + } else { + log::error!( + "NFC payload too short to decode packed text: {} bytes", + payload.len() + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 701e7ab..7e00201 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ #![no_std] +extern crate alloc; + pub mod card; pub mod display; pub mod drivers; diff --git a/src/network/wifi.rs b/src/network/wifi.rs index 03cf1c8..bf00e13 100644 --- a/src/network/wifi.rs +++ b/src/network/wifi.rs @@ -1 +1,67 @@ -// Network wifi module +use embassy_executor::Spawner; + +#[cfg(not(feature = "wokwi"))] +pub async fn setup_wifi( + wifi: esp_hal::peripherals::WIFI<'static>, + flash: esp_hal::peripherals::FLASH<'static>, + spawner: &Spawner, +) { + let nvs = esp_hal_wifimanager::Nvs::new(0x9000, 0x6000, flash).unwrap(); + + let mut wm_settings = esp_hal_wifimanager::WmSettings::default(); + + wm_settings.ssid.clear(); + _ = core::fmt::write( + &mut wm_settings.ssid, + format_args!("CreatureDex-{:X}", esp_hal_wifimanager::get_efuse_mac()), + ); + + wm_settings.wifi_conn_timeout = 30000; + wm_settings.esp_reset_timeout = Some(300000); // 5min + + let wifi_res = esp_hal_wifimanager::init_wm(wm_settings, spawner, Some(&nvs), wifi, None).await; + + log::info!("wifi_res: {wifi_res:?}"); +} + +#[cfg(feature = "wokwi")] +pub async fn setup_wifi( + wifi: esp_hal::peripherals::WIFI<'static>, + flash: esp_hal::peripherals::FLASH<'static>, + spawner: &Spawner, +) { + log::info!("Wokwi mode active: pre-seeding WiFi Manager with Wokwi credentials"); + + // Preload connection data so init_wm can connect immediately without AP setup. + let nvs = match esp_hal_wifimanager::Nvs::new(0x9000, 0x6000, flash) { + Ok(nvs) => nvs, + Err(e) => { + log::error!("Failed to initialize NVS for Wokwi: {e:?}"); + return; + } + }; + + if let Err(e) = nvs + .set( + esp_hal_wifimanager::WIFI_NVS_KEY, + "{\"ssid\":\"Wokwi-GUEST\",\"psk\":\"\",\"data\":{}}", + ) + .await + { + log::error!("Failed to store Wokwi WiFi config in NVS: {e:?}"); + return; + } + + let mut wm_settings = esp_hal_wifimanager::WmSettings::default(); + + // Wokwi simulation uses a default open network called "Wokwi-GUEST" + wm_settings.ssid.clear(); + _ = core::fmt::write(&mut wm_settings.ssid, format_args!("Wokwi-GUEST")); + + wm_settings.wifi_conn_timeout = 30000; + + // Initialize Wi-Fi connection through WiFi Manager using the pre-seeded NVS data. + let wifi_res = esp_hal_wifimanager::init_wm(wm_settings, spawner, Some(&nvs), wifi, None).await; + + log::info!("Wokwi wifi_res: {wifi_res:?}"); +}