use super::messages::{GuiMessage, WorkerMessage};
use crate::domain::ChipSpec;
use eframe::{egui, App, Frame};
use serde::{Deserialize, Serialize};
use std::sync::mpsc::{Receiver, Sender};
#[derive(PartialEq, Serialize, Deserialize)]
enum Tab {
Read,
Write,
Erase,
}
#[derive(Serialize, Deserialize)]
#[serde(default)]
pub struct NanderApp {
#[serde(skip)]
tx: Sender<GuiMessage>,
#[serde(skip)]
rx: Receiver<WorkerMessage>,
active_tab: Tab,
spi_speed: u8,
cs_index: u8,
#[serde(skip)]
status_text: String,
#[serde(skip)]
programmer_name: Option<String>,
#[serde(skip)]
chip_spec: Option<ChipSpec>,
#[serde(skip)]
is_busy: bool,
#[serde(skip)]
progress: Option<f32>,
#[serde(skip)]
logs: Vec<String>,
logs_open: bool,
selected_file: Option<std::path::PathBuf>,
start_address: String,
length: String,
#[serde(skip)]
preview_data: Vec<u8>,
}
impl Default for NanderApp {
fn default() -> Self {
let (tx, _) = std::sync::mpsc::channel();
let (_, rx) = std::sync::mpsc::channel();
Self {
tx,
rx,
active_tab: Tab::Read,
spi_speed: 5,
cs_index: 0,
status_text: "Ready".to_string(),
programmer_name: None,
chip_spec: None,
is_busy: false,
progress: None,
logs: Vec::new(),
logs_open: false,
selected_file: None,
start_address: "0x0".to_string(),
length: "".to_string(),
preview_data: Vec::new(),
}
}
}
impl NanderApp {
pub fn new(
cc: &eframe::CreationContext<'_>,
tx: Sender<GuiMessage>,
rx: Receiver<WorkerMessage>,
) -> Self {
let mut app = if let Some(storage) = cc.storage {
eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default()
} else {
Self::default()
};
app.tx = tx;
app.rx = rx;
app.status_text = "Ready".to_string();
app.is_busy = false;
app.progress = None;
app.logs = Vec::new();
app.preview_data = Vec::new();
app
}
fn handle_messages(&mut self) {
while let Ok(msg) = self.rx.try_recv() {
match msg {
WorkerMessage::Connected(name) => {
self.programmer_name = Some(name);
self.log("Programmer connected");
self.tx.send(GuiMessage::DetectChip).ok(); }
WorkerMessage::ConnectionFailed(err) => {
self.log(&format!("Connection failed: {}", err));
self.status_text = "Connection Failed (see logs)".to_string();
self.is_busy = false;
self.logs_open = true; }
WorkerMessage::ChipDetected(spec) => {
self.log(&format!(
"Chip detected: {} ({})",
spec.name, spec.manufacturer
));
self.chip_spec = Some(spec);
self.is_busy = false; self.status_text = "Chip detected".to_string();
}
WorkerMessage::ChipDetectionFailed(err) => {
self.log(&format!("Chip detection failed: {}", err));
self.status_text = "Chip Detection Failed (see logs)".to_string();
self.is_busy = false;
self.logs_open = true;
}
WorkerMessage::Progress(p) => {
if p.total > 0 {
self.progress = Some(p.current as f32 / p.total as f32);
self.status_text = format!(
"Working... {:.1}%",
(p.current as f64 / p.total as f64) * 100.0
);
}
}
WorkerMessage::OperationComplete => {
self.log("Operation completed successfully");
self.progress = None;
self.is_busy = false;
self.status_text = "Ready".to_string();
}
WorkerMessage::DataRead(data) => {
self.log(&format!("Read {} bytes successfully", data.len()));
self.preview_data = data;
self.progress = None;
self.is_busy = false;
self.status_text = "Ready".to_string();
}
WorkerMessage::OperationFailed(err) => {
self.log(&format!("Operation failed: {}", err));
self.progress = None;
self.is_busy = false;
self.status_text = "Operation Failed (see logs)".to_string();
self.logs_open = true; }
WorkerMessage::Log(msg) => {
self.log(&msg);
}
WorkerMessage::DeviceList(devices) => {
self.log("=== Detected WCH Devices ===");
for device in devices {
self.log(&format!(" {}", device));
}
self.log("===========================");
}
}
}
}
fn log(&mut self, msg: &str) {
self.logs.push(format!(
"[{}] {}",
chrono::Local::now().format("%H:%M:%S"),
msg
));
if self.logs.len() > 1000 {
self.logs.remove(0);
}
}
fn render_hex_view(&mut self, ui: &mut egui::Ui) {
if self.preview_data.is_empty() {
ui.label("No data to display. Read flash to see content.");
return;
}
ui.monospace("Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII");
ui.separator();
let bytes_per_row = 16;
let total_rows = self.preview_data.len().div_ceil(bytes_per_row);
egui::ScrollArea::vertical()
.max_height(400.0)
.auto_shrink([false, false])
.show_rows(ui, 14.0, total_rows, |ui, row_range| {
for r in row_range {
let offset = r * bytes_per_row;
if offset >= self.preview_data.len() {
break;
}
let end = std::cmp::min(offset + bytes_per_row, self.preview_data.len());
let row_data = &self.preview_data[offset..end];
let mut hex_str = String::with_capacity(50);
let mut ascii_str = String::with_capacity(bytes_per_row);
for (i, &b) in row_data.iter().enumerate() {
hex_str.push_str(&format!("{:02X} ", b));
if i == 7 {
hex_str.push(' ');
}
if (32..=126).contains(&b) {
ascii_str.push(b as char);
} else {
ascii_str.push('.');
}
}
while hex_str.len() < 49 {
hex_str.push(' ');
}
ui.monospace(format!("{:08X} {} {}", offset, hex_str, ascii_str));
}
});
}
fn load_file_preview(&mut self) {
if let Some(path) = &self.selected_file {
use std::io::Read;
if let Ok(mut f) = std::fs::File::open(path) {
let mut buffer = vec![0u8; 64 * 1024]; if let Ok(n) = f.read(&mut buffer) {
buffer.truncate(n);
self.preview_data = buffer;
self.log(&format!("Loaded file preview (first {} bytes)", n));
}
}
}
}
fn handle_dropped_files(&mut self, ctx: &egui::Context) {
ctx.input(|i| {
if !i.raw.dropped_files.is_empty() {
if let Some(file) = i.raw.dropped_files.first() {
if let Some(path) = &file.path {
self.selected_file = Some(path.clone());
self.load_file_preview();
}
}
}
});
}
fn parse_u32(s: &str) -> Option<u32> {
let s = s.trim().to_lowercase();
if let Some(stripped) = s.strip_prefix("0x") {
u32::from_str_radix(stripped, 16).ok()
} else {
s.parse::<u32>().ok()
}
}
}
impl App for NanderApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) {
self.handle_messages();
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.menu_button("File", |ui| {
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
});
});
egui::SidePanel::left("settings_panel")
.resizable(true)
.default_width(200.0)
.show(ctx, |ui| {
ui.heading("Settings");
ui.separator();
ui.vertical(|ui| {
ui.label("Programmer:");
if let Some(name) = &self.programmer_name {
ui.label(egui::RichText::new(name).color(egui::Color32::GREEN));
} else {
ui.label(egui::RichText::new("Disconnected").color(egui::Color32::RED));
if ui.button("Connect").clicked() && !self.is_busy {
self.is_busy = true;
self.status_text = "Connecting...".to_string();
self.tx.send(GuiMessage::Connect).ok();
}
}
ui.separator();
ui.label("SPI Speed:");
if ui
.add(egui::Slider::new(&mut self.spi_speed, 0..=7).text("Level"))
.changed()
{
self.tx.send(GuiMessage::SetSpeed(self.spi_speed)).ok();
}
ui.separator();
ui.label("Chip Select (CS):");
ui.horizontal(|ui| {
if ui.selectable_value(&mut self.cs_index, 0, "CS0").clicked() {
self.tx.send(GuiMessage::SetCsIndex(0)).ok();
}
if ui.selectable_value(&mut self.cs_index, 1, "CS1").clicked() {
self.tx.send(GuiMessage::SetCsIndex(1)).ok();
}
});
ui.separator();
if ui
.add_enabled(!self.is_busy, egui::Button::new("Detect Chip"))
.clicked()
{
self.is_busy = true;
self.tx.send(GuiMessage::DetectChip).ok();
}
if let Some(spec) = &self.chip_spec {
ui.group(|ui| {
ui.strong("Chip Info");
ui.label(format!("Name: {}", spec.name));
ui.label(format!("Size: {}", spec.capacity));
ui.label(format!("Type: {:?}", spec.flash_type));
});
}
});
});
egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.strong("Status:");
if let Some(prog) = self.progress {
ui.add(
egui::ProgressBar::new(prog)
.show_percentage()
.desired_width(200.0),
);
}
});
ui.label(&self.status_text);
ui.separator();
let collapsing = egui::CollapsingHeader::new("Logs").open(Some(self.logs_open));
let resp = collapsing.show(ui, |ui| {
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.max_height(100.0)
.show(ui, |ui| {
for log in &self.logs {
ui.monospace(log);
}
});
});
if resp.header_response.clicked() {
self.logs_open = !self.logs_open;
}
});
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.active_tab, Tab::Read, "Read");
ui.selectable_value(&mut self.active_tab, Tab::Write, "Write");
ui.selectable_value(&mut self.active_tab, Tab::Erase, "Erase");
});
ui.separator();
let can_operate = !self.is_busy && self.programmer_name.is_some();
let start = Self::parse_u32(&self.start_address).unwrap_or(0);
let len = Self::parse_u32(&self.length);
match self.active_tab {
Tab::Read => {
ui.heading("Read Flash");
ui.horizontal(|ui| {
if ui.button("Select Save Path...").clicked() {
if let Some(path) = rfd::FileDialog::new().save_file() {
self.selected_file = Some(path);
}
}
if let Some(path) = &self.selected_file {
ui.label(format!("Path: {}", path.display()));
}
});
ui.horizontal(|ui| {
ui.label("Start Address:");
ui.text_edit_singleline(&mut self.start_address);
ui.label("Length:");
ui.text_edit_singleline(&mut self.length);
});
if ui
.add_enabled(
can_operate && self.selected_file.is_some(),
egui::Button::new("Start Reading"),
)
.clicked()
{
if let Some(path) = &self.selected_file {
self.is_busy = true;
self.status_text = "Reading...".to_string();
self.tx
.send(GuiMessage::ReadFlash {
path: path.clone(),
start,
length: len,
})
.ok();
}
}
ui.separator();
ui.label("Hex Preview:");
self.render_hex_view(ui);
}
Tab::Write => {
ui.heading("Write Flash");
ui.horizontal(|ui| {
if ui.button("Select File to Write...").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_file() {
self.selected_file = Some(path);
self.load_file_preview();
}
}
if let Some(path) = &self.selected_file {
ui.label(format!("File: {}", path.display()));
}
});
ui.horizontal(|ui| {
ui.label("Start Address:");
ui.text_edit_singleline(&mut self.start_address);
});
if ui
.add_enabled(
can_operate && self.selected_file.is_some(),
egui::Button::new("Start Writing"),
)
.clicked()
{
if let Some(path) = &self.selected_file {
self.is_busy = true;
self.status_text = "Writing...".to_string();
self.tx
.send(GuiMessage::WriteFlash {
path: path.clone(),
start,
verify: true,
})
.ok();
}
}
ui.separator();
ui.label("File Preview:");
self.render_hex_view(ui);
}
Tab::Erase => {
ui.heading("Erase Flash");
ui.horizontal(|ui| {
ui.label("Start Address:");
ui.text_edit_singleline(&mut self.start_address);
ui.label("Length:");
ui.text_edit_singleline(&mut self.length);
});
if ui
.add_enabled(can_operate, egui::Button::new("Start Erasing"))
.clicked()
{
self.is_busy = true;
self.status_text = "Erasing...".to_string();
self.tx
.send(GuiMessage::EraseFlash { start, length: len })
.ok();
}
}
}
});
self.handle_dropped_files(ctx);
if self.is_busy {
ctx.request_repaint();
}
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
}