693 lines
29 KiB
Rust
693 lines
29 KiB
Rust
#![windows_subsystem = "windows"]
|
|
|
|
mod acr122u;
|
|
mod mifare;
|
|
mod ndef;
|
|
mod ntag;
|
|
|
|
use acr122u::{Acr122u, CardInfo, LedBuzzerConfig};
|
|
use eframe::egui;
|
|
use mifare::{MifareDump, MifareHandler};
|
|
use ndef::NdefCodec;
|
|
use ntag::NtagHandler;
|
|
use pcsc::Context;
|
|
use std::fs;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
pub enum CardEvent {
|
|
ReaderStatus { connected: bool, name: String },
|
|
CardDetected(CardInfo),
|
|
CardRemoved,
|
|
}
|
|
|
|
pub struct NfcApp {
|
|
rx: Receiver<CardEvent>,
|
|
reader_name: String,
|
|
is_reader_connected: bool,
|
|
current_card: Option<CardInfo>,
|
|
event_logs: Vec<String>,
|
|
active_tab: Tab,
|
|
|
|
// LED / Beep state
|
|
led_red: bool,
|
|
led_green: bool,
|
|
led_blink: bool,
|
|
led_beep: bool,
|
|
|
|
// MIFARE state
|
|
mifare_dump: Option<MifareDump>,
|
|
mifare_status: String,
|
|
|
|
// NDEF & Text State
|
|
ndef_subtab: NdefSubTab,
|
|
ndef_url_input: String,
|
|
ndef_text_input: String,
|
|
ndef_text_lang: String,
|
|
wifi_ssid: String,
|
|
wifi_pass: String,
|
|
vcard_name: String,
|
|
vcard_phone: String,
|
|
vcard_email: String,
|
|
vcard_company: String,
|
|
geo_url: String,
|
|
aar_package: String,
|
|
ndef_status: String,
|
|
|
|
// NTAG Password State
|
|
pwd_input: String,
|
|
pack_input: String,
|
|
pwd_status: String,
|
|
|
|
// APDU State
|
|
apdu_input: String,
|
|
apdu_response_log: Vec<String>,
|
|
|
|
pcsc_ctx: Arc<Mutex<Context>>,
|
|
is_busy: Arc<AtomicBool>,
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
enum Tab {
|
|
Scanner,
|
|
Ndef,
|
|
Mifare,
|
|
Password,
|
|
Hardware,
|
|
ExpertApdu,
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
enum NdefSubTab {
|
|
Text,
|
|
Url,
|
|
Wifi,
|
|
VCard,
|
|
Geo,
|
|
AppLauncher,
|
|
}
|
|
|
|
impl NfcApp {
|
|
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
|
setup_edel_theme(&cc.egui_ctx);
|
|
|
|
let (tx, rx) = channel();
|
|
let ctx = Acr122u::create_context().expect("PC/SC Context konnte nicht erstellt werden!");
|
|
let pcsc_ctx = Arc::new(Mutex::new(ctx));
|
|
let is_busy = Arc::new(AtomicBool::new(false));
|
|
|
|
let poll_ctx = pcsc_ctx.clone();
|
|
let poll_busy = is_busy.clone();
|
|
thread::spawn(move || {
|
|
poll_loop(tx, poll_ctx, poll_busy);
|
|
});
|
|
|
|
Self {
|
|
rx,
|
|
reader_name: "Kein Lesegerät angeschlossen".to_string(),
|
|
is_reader_connected: false,
|
|
current_card: None,
|
|
event_logs: vec!["System bereit. Bitte lege einen NFC-Tag auf das Gerät...".to_string()],
|
|
active_tab: Tab::Scanner,
|
|
|
|
led_red: false,
|
|
led_green: true,
|
|
led_blink: false,
|
|
led_beep: true,
|
|
|
|
mifare_dump: None,
|
|
mifare_status: "Klicke auf 'Karte Auslesen', um alle Speichersektoren zu scannen.".to_string(),
|
|
|
|
ndef_subtab: NdefSubTab::Text,
|
|
ndef_url_input: "https://example.com".to_string(),
|
|
ndef_text_input: "Hallo! Das ist meine persönliche Notiz auf dem NFC-Tag.".to_string(),
|
|
ndef_text_lang: "de".to_string(),
|
|
wifi_ssid: "MeinWLAN".to_string(),
|
|
wifi_pass: "MeinPasswort123".to_string(),
|
|
vcard_name: "Max Mustermann".to_string(),
|
|
vcard_phone: "+49 170 1234567".to_string(),
|
|
vcard_email: "max@beispiel.de".to_string(),
|
|
vcard_company: "Meine Firma GmbH".to_string(),
|
|
geo_url: "https://maps.google.com/?q=Berlin".to_string(),
|
|
aar_package: "com.spotify.music".to_string(),
|
|
ndef_status: "Bereit zum Beschreiben".to_string(),
|
|
|
|
pwd_input: "1234".to_string(),
|
|
pack_input: "0000".to_string(),
|
|
pwd_status: "Gib ein 4-stelliges Passwort ein, um den Tag zu schützen.".to_string(),
|
|
|
|
apdu_input: "FF 00 48 00 00".to_string(),
|
|
apdu_response_log: Vec::new(),
|
|
|
|
pcsc_ctx,
|
|
is_busy,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn setup_edel_theme(ctx: &egui::Context) {
|
|
let mut style = (*ctx.style()).clone();
|
|
let mut visuals = egui::Visuals::dark();
|
|
|
|
visuals.panel_fill = egui::Color32::from_rgb(10, 13, 20);
|
|
visuals.window_fill = egui::Color32::from_rgb(19, 25, 39);
|
|
visuals.extreme_bg_color = egui::Color32::from_rgb(6, 8, 14);
|
|
|
|
visuals.widgets.noninteractive.rounding = egui::Rounding::same(10.0);
|
|
visuals.widgets.noninteractive.bg_fill = egui::Color32::from_rgb(26, 34, 50);
|
|
visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0_f32, egui::Color32::from_rgb(45, 58, 82));
|
|
|
|
visuals.widgets.inactive.rounding = egui::Rounding::same(10.0);
|
|
visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(26, 34, 50);
|
|
|
|
visuals.widgets.hovered.rounding = egui::Rounding::same(10.0);
|
|
visuals.widgets.hovered.bg_fill = egui::Color32::from_rgb(45, 58, 82);
|
|
visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.5_f32, egui::Color32::from_rgb(0, 242, 254));
|
|
|
|
visuals.widgets.active.rounding = egui::Rounding::same(10.0);
|
|
visuals.widgets.active.bg_fill = egui::Color32::from_rgb(0, 242, 254);
|
|
visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0_f32, egui::Color32::BLACK);
|
|
|
|
visuals.selection.bg_fill = egui::Color32::from_rgba_premultiplied(0, 242, 254, 70);
|
|
|
|
style.visuals = visuals;
|
|
style.spacing.item_spacing = egui::vec2(12.0, 12.0);
|
|
style.spacing.button_padding = egui::vec2(16.0, 10.0);
|
|
ctx.set_style(style);
|
|
}
|
|
|
|
fn poll_loop(tx: Sender<CardEvent>, ctx_store: Arc<Mutex<Context>>, is_busy: Arc<AtomicBool>) {
|
|
let mut last_uid = String::new();
|
|
|
|
loop {
|
|
thread::sleep(Duration::from_millis(600));
|
|
|
|
if is_busy.load(Ordering::SeqCst) {
|
|
continue;
|
|
}
|
|
|
|
let ctx = ctx_store.lock().unwrap();
|
|
|
|
let reader = match Acr122u::find_acr122u(&ctx) {
|
|
Ok(r) => {
|
|
let _ = tx.send(CardEvent::ReaderStatus {
|
|
connected: true,
|
|
name: r.clone(),
|
|
});
|
|
r
|
|
}
|
|
Err(_) => {
|
|
let _ = tx.send(CardEvent::ReaderStatus {
|
|
connected: false,
|
|
name: "Kein Lesegerät".to_string(),
|
|
});
|
|
last_uid.clear();
|
|
let _ = tx.send(CardEvent::CardRemoved);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
match Acr122u::read_card_info(&ctx, &reader) {
|
|
Ok(info) => {
|
|
if info.uid != last_uid {
|
|
last_uid = info.uid.clone();
|
|
let _ = Acr122u::beep(&ctx, &reader, 50, 1);
|
|
let _ = tx.send(CardEvent::CardDetected(info));
|
|
}
|
|
}
|
|
Err(_) => {
|
|
if !last_uid.is_empty() {
|
|
last_uid.clear();
|
|
let _ = tx.send(CardEvent::CardRemoved);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl eframe::App for NfcApp {
|
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
while let Ok(evt) = self.rx.try_recv() {
|
|
let timestamp = chrono::Local::now().format("%H:%M:%S").to_string();
|
|
match evt {
|
|
CardEvent::ReaderStatus { connected, name } => {
|
|
self.is_reader_connected = connected;
|
|
self.reader_name = name;
|
|
}
|
|
CardEvent::CardDetected(info) => {
|
|
let friendly_type = match info.uid_bytes.len() {
|
|
7 => "Standard NFC-Sticker / Tag (NTAG)",
|
|
4 => "MIFARE Chipkarte / Schlüsselanhänger",
|
|
_ => "Kontaktloser NFC-Chip",
|
|
};
|
|
self.event_logs.insert(0, format!("[{}] 💳 NFC-Tag erkannt: ID {} ({})", timestamp, info.uid, friendly_type));
|
|
self.current_card = Some(info);
|
|
}
|
|
CardEvent::CardRemoved => {
|
|
if self.current_card.is_some() {
|
|
self.event_logs.insert(0, format!("[{}] 🚫 NFC-Tag entfernt", timestamp));
|
|
self.current_card = None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.request_repaint_after(Duration::from_millis(200));
|
|
|
|
egui::TopBottomPanel::top("top_header").show(ctx, |ui| {
|
|
ui.add_space(8.0);
|
|
ui.horizontal(|ui| {
|
|
ui.label(egui::RichText::new("💎 ACR122U NFC Studio Pro").font(egui::FontId::proportional(22.0)).strong().color(egui::Color32::from_rgb(0, 242, 254)));
|
|
ui.label(egui::RichText::new("Premium Edition").font(egui::FontId::proportional(12.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
|
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
|
if self.is_reader_connected {
|
|
ui.colored_label(egui::Color32::from_rgb(0, 230, 153), format!("🟢 {}", self.reader_name));
|
|
} else {
|
|
ui.colored_label(egui::Color32::from_rgb(255, 75, 87), "🔴 Gerät nicht verbunden");
|
|
}
|
|
});
|
|
});
|
|
ui.add_space(8.0);
|
|
});
|
|
|
|
egui::TopBottomPanel::top("tab_bar").show(ctx, |ui| {
|
|
ui.horizontal(|ui| {
|
|
ui.selectable_value(&mut self.active_tab, Tab::Scanner, "📡 Live-Erkennung");
|
|
ui.selectable_value(&mut self.active_tab, Tab::Ndef, "✏️ NFC-Tag Beschreiben");
|
|
ui.selectable_value(&mut self.active_tab, Tab::Mifare, "🔍 Speicher & Klonen");
|
|
ui.selectable_value(&mut self.active_tab, Tab::Password, "🔒 Passwort-Schutz");
|
|
ui.selectable_value(&mut self.active_tab, Tab::Hardware, "🔔 LEDs & Töne");
|
|
ui.selectable_value(&mut self.active_tab, Tab::ExpertApdu, "🛠️ Experten-Befehle");
|
|
});
|
|
});
|
|
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
|
match self.active_tab {
|
|
Tab::Scanner => self.render_scanner_tab(ui),
|
|
Tab::Ndef => self.render_ndef_tab(ui),
|
|
Tab::Mifare => self.render_mifare_tab(ui),
|
|
Tab::Password => self.render_password_tab(ui),
|
|
Tab::Hardware => self.render_hardware_tab(ui),
|
|
Tab::ExpertApdu => self.render_apdu_tab(ui),
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
impl NfcApp {
|
|
fn render_scanner_tab(&mut self, ui: &mut egui::Ui) {
|
|
ui.columns(2, |cols| {
|
|
cols[0].group(|ui| {
|
|
ui.heading("💳 Aufgelegter NFC-Tag");
|
|
ui.label(egui::RichText::new("Hier siehst du sofort alle Infos zur erkannten Karte.").font(egui::FontId::proportional(12.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
ui.add_space(8.0);
|
|
|
|
egui::Frame::none()
|
|
.fill(egui::Color32::from_rgb(19, 25, 39))
|
|
.stroke(egui::Stroke::new(1.5_f32, egui::Color32::from_rgb(0, 242, 254)))
|
|
.rounding(16.0)
|
|
.inner_margin(20.0)
|
|
.show(ui, |ui| {
|
|
ui.horizontal(|ui| {
|
|
ui.label(egui::RichText::new("📇 NFC-CHIP").font(egui::FontId::proportional(14.0)).strong().color(egui::Color32::from_rgb(246, 211, 101)));
|
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
|
ui.label(egui::RichText::new("Kontaktlos").font(egui::FontId::proportional(12.0)).color(egui::Color32::from_rgb(0, 242, 254)));
|
|
});
|
|
});
|
|
|
|
ui.add_space(16.0);
|
|
ui.label(egui::RichText::new("EINDEUTIGE CHIP-ID (UID):").font(egui::FontId::proportional(11.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
|
|
if let Some(card) = &self.current_card {
|
|
ui.label(egui::RichText::new(&card.uid).font(egui::FontId::monospace(26.0)).strong().color(egui::Color32::from_rgb(246, 211, 101)));
|
|
ui.add_space(12.0);
|
|
let friendly_type = match card.uid_bytes.len() {
|
|
7 => "Standard NFC-Sticker / Tag (NTAG213/215/216)",
|
|
4 => "MIFARE Classic Chipkarte / Schlüsselanhänger",
|
|
_ => "Kontaktloser NFC-Chip",
|
|
};
|
|
ui.label(format!("Chip-Typ: {}", friendly_type));
|
|
} else {
|
|
ui.label(egui::RichText::new("-- -- -- --").font(egui::FontId::monospace(26.0)).color(egui::Color32::GRAY));
|
|
ui.add_space(12.0);
|
|
ui.colored_label(egui::Color32::GRAY, "Bitte halte einen NFC-Tag auf das Gerät...");
|
|
}
|
|
});
|
|
});
|
|
|
|
cols[1].group(|ui| {
|
|
ui.heading("📜 Echtzeit Aktivitäten-Log");
|
|
ui.label(egui::RichText::new("Verlauf der Erfassungen und Aktionen.").font(egui::FontId::proportional(12.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
ui.add_space(6.0);
|
|
egui::ScrollArea::vertical().max_height(350.0).show(ui, |ui| {
|
|
for log in &self.event_logs {
|
|
ui.label(egui::RichText::new(log).font(egui::FontId::monospace(13.0)));
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
fn render_ndef_tab(&mut self, ui: &mut egui::Ui) {
|
|
ui.heading("✏️ NFC-Tag Beschreiben");
|
|
ui.label(egui::RichText::new("Wähle aus, was beim Dranhalten eines Smartphones passieren soll:").font(egui::FontId::proportional(12.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
ui.add_space(8.0);
|
|
|
|
ui.horizontal(|ui| {
|
|
ui.selectable_value(&mut self.ndef_subtab, NdefSubTab::Text, "📝 Text & Notiz");
|
|
ui.selectable_value(&mut self.ndef_subtab, NdefSubTab::Url, "🌐 Internet-Link (URL)");
|
|
ui.selectable_value(&mut self.ndef_subtab, NdefSubTab::Wifi, "📶 WLAN Quick-Connect");
|
|
ui.selectable_value(&mut self.ndef_subtab, NdefSubTab::VCard, "🎴 Visitenkarte");
|
|
ui.selectable_value(&mut self.ndef_subtab, NdefSubTab::Geo, "🗺️ Standort / Maps");
|
|
ui.selectable_value(&mut self.ndef_subtab, NdefSubTab::AppLauncher, "📱 App Öffnen");
|
|
});
|
|
|
|
ui.add_space(12.0);
|
|
|
|
let mut tlv_payload: Option<Vec<u8>> = None;
|
|
|
|
match self.ndef_subtab {
|
|
NdefSubTab::Text => {
|
|
ui.group(|ui| {
|
|
ui.label("Deine Nachricht / Notiz:");
|
|
ui.add(egui::TextEdit::multiline(&mut self.ndef_text_input).desired_rows(4).desired_width(f32::INFINITY));
|
|
|
|
ui.horizontal(|ui| {
|
|
if ui.button("💾 Text als Datei speichern (.txt)").clicked() {
|
|
if let Some(path) = rfd::FileDialog::new().add_filter("Textdatei", &["txt"]).save_file() {
|
|
let _ = fs::write(path, &self.ndef_text_input);
|
|
self.ndef_status = "Text erfolgreich auf dem PC gespeichert!".to_string();
|
|
}
|
|
}
|
|
if ui.button("📂 Text aus Datei laden (.txt)").clicked() {
|
|
if let Some(path) = rfd::FileDialog::new().add_filter("Textdatei", &["txt"]).pick_file() {
|
|
if let Ok(content) = fs::read_to_string(path) {
|
|
self.ndef_text_input = content;
|
|
self.ndef_status = "Text aus Datei geladen!".to_string();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
tlv_payload = Some(NdefCodec::encode_text(&self.ndef_text_input, &self.ndef_text_lang));
|
|
});
|
|
}
|
|
NdefSubTab::Url => {
|
|
ui.group(|ui| {
|
|
ui.label("Internet-Adresse (öffnet sich beim Scannen automatisch):");
|
|
ui.text_edit_singleline(&mut self.ndef_url_input);
|
|
tlv_payload = Some(NdefCodec::encode_url(&self.ndef_url_input));
|
|
});
|
|
}
|
|
NdefSubTab::Wifi => {
|
|
ui.group(|ui| {
|
|
ui.label("WLAN Netz-Name (SSID):");
|
|
ui.text_edit_singleline(&mut self.wifi_ssid);
|
|
ui.label("WLAN Passwort:");
|
|
ui.text_edit_singleline(&mut self.wifi_pass);
|
|
ui.label(egui::RichText::new("💡 Smartphones verbinden sich beim Dranhalten direkt mit dem WLAN.").font(egui::FontId::proportional(11.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
tlv_payload = Some(NdefCodec::encode_wifi(&self.wifi_ssid, &self.wifi_pass));
|
|
});
|
|
}
|
|
NdefSubTab::VCard => {
|
|
ui.group(|ui| {
|
|
ui.label("Name:");
|
|
ui.text_edit_singleline(&mut self.vcard_name);
|
|
ui.label("Telefonnummer:");
|
|
ui.text_edit_singleline(&mut self.vcard_phone);
|
|
ui.label("E-Mail:");
|
|
ui.text_edit_singleline(&mut self.vcard_email);
|
|
ui.label("Firma / Organisation:");
|
|
ui.text_edit_singleline(&mut self.vcard_company);
|
|
ui.label(egui::RichText::new("💡 Beim Scannen wird der Kontakt direkt in das Handy-Telefonbuch gespeichert.").font(egui::FontId::proportional(11.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
tlv_payload = Some(NdefCodec::encode_vcard(&self.vcard_name, &self.vcard_phone, &self.vcard_email, &self.vcard_company));
|
|
});
|
|
}
|
|
NdefSubTab::Geo => {
|
|
ui.group(|ui| {
|
|
ui.label("Google Maps Standorts-Link:");
|
|
ui.text_edit_singleline(&mut self.geo_url);
|
|
tlv_payload = Some(NdefCodec::encode_url(&self.geo_url));
|
|
});
|
|
}
|
|
NdefSubTab::AppLauncher => {
|
|
ui.group(|ui| {
|
|
ui.label("Android App-Name (z. B. com.spotify.music):");
|
|
ui.text_edit_singleline(&mut self.aar_package);
|
|
tlv_payload = Some(NdefCodec::encode_aar(&self.aar_package));
|
|
});
|
|
}
|
|
}
|
|
|
|
ui.add_space(15.0);
|
|
|
|
if ui.button("⚡ Auf NFC-Tag Schreiben").clicked() {
|
|
if let Some(tlv) = tlv_payload {
|
|
self.is_busy.store(true, Ordering::SeqCst);
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
let pcsc = self.pcsc_ctx.lock().unwrap();
|
|
if let Ok(reader) = Acr122u::find_acr122u(&pcsc) {
|
|
if let Ok(mut card) = Acr122u::connect_card_with_ctx(&pcsc, &reader) {
|
|
let _ = NtagHandler::ensure_cc_header(&pcsc, &reader, &mut card);
|
|
let mut page = 4;
|
|
let mut err = false;
|
|
|
|
for chunk in tlv.chunks(4) {
|
|
let mut p_data = [0u8; 4];
|
|
p_data[..chunk.len()].copy_from_slice(chunk);
|
|
if NtagHandler::write_page(&pcsc, &reader, &mut card, page, &p_data).is_err() {
|
|
err = true;
|
|
self.ndef_status = format!("Schreibfehler auf Seite {}", page);
|
|
break;
|
|
}
|
|
page += 1;
|
|
}
|
|
|
|
if !err {
|
|
let _ = Acr122u::beep(&pcsc, &reader, 100, 2);
|
|
self.ndef_status = "✅ Inhalt erfolgreich auf den NFC-Tag geschrieben!".to_string();
|
|
}
|
|
} else {
|
|
self.ndef_status = "❌ Karte konnte nicht verbunden werden!".to_string();
|
|
}
|
|
}
|
|
|
|
self.is_busy.store(false, Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
ui.add_space(10.0);
|
|
ui.colored_label(egui::Color32::from_rgb(0, 242, 254), &self.ndef_status);
|
|
}
|
|
|
|
fn render_mifare_tab(&mut self, ui: &mut egui::Ui) {
|
|
ui.heading("🔍 Speicher Auslesen & Klonen");
|
|
ui.label(egui::RichText::new("Lies den kompletten Speicherinhalt von MIFARE-Karten aus und speichere ihn ab.").font(egui::FontId::proportional(12.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
ui.add_space(10.0);
|
|
|
|
ui.horizontal(|ui| {
|
|
if ui.button("🔄 Karte Auslesen").clicked() {
|
|
self.is_busy.store(true, Ordering::SeqCst);
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
let pcsc = self.pcsc_ctx.lock().unwrap();
|
|
if let Ok(reader) = Acr122u::find_acr122u(&pcsc) {
|
|
match MifareHandler::dump_card(&pcsc, &reader) {
|
|
Ok(dump) => {
|
|
self.mifare_dump = Some(dump);
|
|
self.mifare_status = "Auslesen erfolgreich!".to_string();
|
|
}
|
|
Err(e) => {
|
|
self.mifare_status = format!("Fehler beim Auslesen: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.is_busy.store(false, Ordering::SeqCst);
|
|
}
|
|
|
|
if let Some(dump) = &self.mifare_dump {
|
|
if ui.button("💾 Als Backup speichern (.json)").clicked() {
|
|
if let Some(path) = rfd::FileDialog::new().add_filter("JSON Backup", &["json"]).save_file() {
|
|
if let Ok(json) = serde_json::to_string_pretty(dump) {
|
|
let _ = fs::write(path, json);
|
|
self.mifare_status = "Backup-Datei gespeichert!".to_string();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ui.button("📂 Backup aus Datei laden").clicked() {
|
|
if let Some(path) = rfd::FileDialog::new().add_filter("JSON Backup", &["json"]).pick_file() {
|
|
if let Ok(content) = fs::read_to_string(path) {
|
|
if let Ok(loaded_dump) = serde_json::from_str::<MifareDump>(&content) {
|
|
self.mifare_dump = Some(loaded_dump);
|
|
self.mifare_status = "Backup geladen!".to_string();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.colored_label(egui::Color32::from_rgb(0, 242, 254), &self.mifare_status);
|
|
ui.add_space(10.0);
|
|
|
|
if let Some(dump) = &self.mifare_dump {
|
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
|
for sec in &dump.sectors {
|
|
ui.group(|ui| {
|
|
ui.label(egui::RichText::new(format!("Speicherblock {} (Key: {})", sec.sector_number, sec.key_a.as_deref().unwrap_or("Schlüsselgeschützt"))).strong());
|
|
for b in &sec.blocks {
|
|
let text_color = if b.is_trailer { egui::Color32::from_rgb(214, 188, 250) } else { egui::Color32::WHITE };
|
|
ui.label(egui::RichText::new(format!("Block {:02}: {} | {}", b.block_number, b.hex_data, b.ascii_data)).font(egui::FontId::monospace(12.0)).color(text_color));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fn render_password_tab(&mut self, ui: &mut egui::Ui) {
|
|
ui.heading("🔒 Passwort-Schutz & Tag Versiegeln");
|
|
ui.label(egui::RichText::new("Schütze deinen NFC-Tag vor unbefugtem Überschreiben.").font(egui::FontId::proportional(12.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
ui.add_space(10.0);
|
|
|
|
ui.group(|ui| {
|
|
ui.label("4-stelliges Passwort (z. B. 1234):");
|
|
ui.text_edit_singleline(&mut self.pwd_input);
|
|
|
|
ui.add_space(10.0);
|
|
if ui.button("🔐 Passwort auf Tag aktivieren").clicked() {
|
|
self.is_busy.store(true, Ordering::SeqCst);
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
let pcsc = self.pcsc_ctx.lock().unwrap();
|
|
if let Ok(reader) = Acr122u::find_acr122u(&pcsc) {
|
|
if let Ok(mut card) = Acr122u::connect_card_with_ctx(&pcsc, &reader) {
|
|
let pwd_bytes = self.pwd_input.as_bytes();
|
|
if pwd_bytes.len() == 4 {
|
|
let mut p_arr = [0u8; 4];
|
|
p_arr.copy_from_slice(pwd_bytes);
|
|
if NtagHandler::write_page(&pcsc, &reader, &mut card, 134, &p_arr).is_ok() {
|
|
self.pwd_status = "✅ Passwort erfolgreich auf dem Tag aktiviert!".to_string();
|
|
let _ = Acr122u::beep(&pcsc, &reader, 100, 2);
|
|
} else {
|
|
self.pwd_status = "❌ Fehler beim Aktivieren des Passworts!".to_string();
|
|
}
|
|
} else {
|
|
self.pwd_status = "Das Passwort muss genau 4 Zeichen lang sein!".to_string();
|
|
}
|
|
}
|
|
}
|
|
|
|
self.is_busy.store(false, Ordering::SeqCst);
|
|
}
|
|
});
|
|
|
|
ui.add_space(15.0);
|
|
ui.colored_label(egui::Color32::from_rgb(0, 242, 254), &self.pwd_status);
|
|
}
|
|
|
|
fn render_hardware_tab(&mut self, ui: &mut egui::Ui) {
|
|
ui.heading("🔔 LEDs & Signal-Töne des Lesegeräts");
|
|
ui.label(egui::RichText::new("Steuere die Hardware-Signale deines ACR122U Geräts.").font(egui::FontId::proportional(12.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
ui.add_space(10.0);
|
|
|
|
ui.checkbox(&mut self.led_red, "Rote LED aktivieren");
|
|
ui.checkbox(&mut self.led_green, "Grüne LED aktivieren");
|
|
ui.checkbox(&mut self.led_blink, "Blinken aktivieren");
|
|
ui.checkbox(&mut self.led_beep, "Ton beim Schalten abspielen");
|
|
|
|
ui.add_space(12.0);
|
|
if ui.button("⚡ LED & Ton-Einstellungen Anwenden").clicked() {
|
|
let pcsc = self.pcsc_ctx.lock().unwrap();
|
|
let config = LedBuzzerConfig {
|
|
red: self.led_red,
|
|
green: self.led_green,
|
|
blink_red: self.led_blink,
|
|
blink_green: self.led_blink,
|
|
blink_count: if self.led_blink { 4 } else { 0 },
|
|
t1_duration_ms: 100,
|
|
t2_duration_ms: 100,
|
|
buzzer_mode: if self.led_beep { 1 } else { 0 },
|
|
};
|
|
if let Ok(reader) = Acr122u::find_acr122u(&pcsc) {
|
|
let _ = Acr122u::set_led_buzzer(&pcsc, &reader, &config);
|
|
}
|
|
}
|
|
|
|
ui.add_space(20.0);
|
|
ui.label("Signalton testen:");
|
|
ui.horizontal(|ui| {
|
|
if ui.button("🔊 1x Signalton").clicked() {
|
|
let pcsc = self.pcsc_ctx.lock().unwrap();
|
|
if let Ok(reader) = Acr122u::find_acr122u(&pcsc) {
|
|
let _ = Acr122u::beep(&pcsc, &reader, 100, 1);
|
|
}
|
|
}
|
|
if ui.button("🔊 2x Bestätigungston").clicked() {
|
|
let pcsc = self.pcsc_ctx.lock().unwrap();
|
|
if let Ok(reader) = Acr122u::find_acr122u(&pcsc) {
|
|
let _ = Acr122u::beep(&pcsc, &reader, 100, 2);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn render_apdu_tab(&mut self, ui: &mut egui::Ui) {
|
|
ui.heading("🛠️ Experten-Befehle (Rohdaten / APDU)");
|
|
ui.label(egui::RichText::new("Manuelle Befehlseingabe für fortgeschrittene Nutzer.").font(egui::FontId::proportional(12.0)).color(egui::Color32::from_rgb(148, 163, 184)));
|
|
ui.add_space(10.0);
|
|
|
|
ui.label("Befehl in Hexadezimal (z. B. FF 00 48 00 00):");
|
|
ui.text_edit_singleline(&mut self.apdu_input);
|
|
|
|
if ui.button("Befehl Senden").clicked() {
|
|
let pcsc = self.pcsc_ctx.lock().unwrap();
|
|
if let Ok(reader) = Acr122u::find_acr122u(&pcsc) {
|
|
if let Ok(card) = Acr122u::connect_card_with_ctx(&pcsc, &reader).or_else(|_| Acr122u::connect_direct_with_ctx(&pcsc, &reader)) {
|
|
if let Ok(bytes) = hex::decode(self.apdu_input.replace(" ", "")) {
|
|
match Acr122u::transmit(&card, &bytes) {
|
|
Ok((data, sw1, sw2)) => {
|
|
let entry = format!("> {}\n< DATEN: {}\n< STATUS: {:02X}{:02X}", self.apdu_input, hex::encode_upper(&data), sw1, sw2);
|
|
self.apdu_response_log.insert(0, entry);
|
|
}
|
|
Err(e) => {
|
|
self.apdu_response_log.insert(0, format!("❌ FEHLER: {}", e));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ui.add_space(15.0);
|
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
|
for log in &self.apdu_response_log {
|
|
ui.label(egui::RichText::new(log).font(egui::FontId::monospace(13.0)));
|
|
ui.separator();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fn main() -> eframe::Result<()> {
|
|
let options = eframe::NativeOptions {
|
|
viewport: egui::ViewportBuilder::default()
|
|
.with_title("ACR122U NFC Studio Pro (Transparent Reconnect)")
|
|
.with_inner_size([1150.0, 780.0]),
|
|
..Default::default()
|
|
};
|
|
|
|
eframe::run_native(
|
|
"nfc_tool",
|
|
options,
|
|
Box::new(|cc| Ok(Box::new(NfcApp::new(cc)))),
|
|
)
|
|
}
|