1use std::fmt::{Display, Formatter};
2use std::fs;
3use std::io::Cursor;
4use std::sync::Arc;
5use std::sync::Mutex;
6use std::sync::atomic::AtomicU64;
7
8use crate::async_util::{perform_async_work, perform_work};
9use crate::channels::checked_send;
10use crate::cxxrtl_container::CxxrtlContainer;
11use crate::file_dialog::OpenMode;
12use crate::remote::{get_hierarchy_from_server, get_server_status, server_reload};
13use crate::transactions::TRANSACTIONS_FILE_EXTENSION;
14use crate::util::get_multi_extension;
15use camino::{Utf8Path, Utf8PathBuf};
16use eyre::Report;
17use eyre::Result;
18use eyre::{WrapErr, anyhow};
19use ftr_parser::parse;
20use futures_util::FutureExt;
21use serde::{Deserialize, Serialize};
22use tracing::{error, info, warn};
23use web_time::Instant;
24
25use crate::transaction_container::TransactionContainer;
26use crate::wave_container::WaveContainer;
27use crate::wellen::{
28 BodyResult, HeaderResult, LoadSignalPayload, LoadSignalsCmd, LoadSignalsResult,
29};
30use crate::{SystemState, message::Message};
31use surver::{
32 HTTP_SERVER_KEY, HTTP_SERVER_VALUE_SURFER, SurverFileInfo, WELLEN_SURFER_DEFAULT_OPTIONS,
33};
34
35#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
36pub enum CxxrtlKind {
37 Tcp { url: String },
38 Mailbox,
39}
40impl std::fmt::Display for CxxrtlKind {
41 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
42 match self {
43 CxxrtlKind::Tcp { url } => write!(f, "cxxrtl+tcp://{url}"),
44 CxxrtlKind::Mailbox => write!(f, "cxxrtl mailbox"),
45 }
46 }
47}
48
49#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
50pub enum WaveSource {
51 File(Utf8PathBuf),
52 Data,
53 DragAndDrop(Option<Utf8PathBuf>),
54 Url(String),
55 Cxxrtl(CxxrtlKind),
56}
57
58pub const STATE_FILE_EXTENSION: &str = "surf.ron";
59
60impl WaveSource {
61 #[must_use]
62 pub fn as_file(&self) -> Option<&Utf8Path> {
63 match self {
64 WaveSource::File(path) => Some(path.as_path()),
65 _ => None,
66 }
67 }
68
69 #[must_use]
70 pub fn path(&self) -> Option<&Utf8PathBuf> {
71 match self {
72 WaveSource::File(path) => Some(path),
73 WaveSource::DragAndDrop(Some(path)) => Some(path),
74 _ => None,
75 }
76 }
77
78 #[must_use]
79 pub fn sibling_state_file(&self) -> Option<Utf8PathBuf> {
80 let path = self.path()?;
81 let directory = path.parent()?;
82 let paths = fs::read_dir(directory).ok()?;
83
84 for entry in paths {
85 let Ok(entry) = entry else { continue };
86 if let Ok(path) = Utf8PathBuf::from_path_buf(entry.path()) {
87 let Some(ext) = get_multi_extension(&path) else {
88 continue;
89 };
90 if ext.as_str() == STATE_FILE_EXTENSION {
91 return Some(path);
92 }
93 }
94 }
95
96 None
97 }
98
99 #[must_use]
100 pub fn into_translation_type(&self) -> surfer_translation_types::WaveSource {
101 use surfer_translation_types::WaveSource as Ws;
102 match self {
103 WaveSource::File(file) => Ws::File(file.to_string()),
104 WaveSource::Data => Ws::Data,
105 WaveSource::DragAndDrop(file) => {
106 Ws::DragAndDrop(file.as_ref().map(ToString::to_string))
107 }
108 WaveSource::Url(u) => Ws::Url(u.clone()),
109 WaveSource::Cxxrtl(_) => Ws::Cxxrtl,
110 }
111 }
112}
113
114pub fn url_to_wavesource(url: &str) -> Option<WaveSource> {
115 if url.starts_with("https://") || url.starts_with("http://") {
116 info!("Wave source is url");
117 Some(WaveSource::Url(url.to_string()))
118 } else if url.starts_with("cxxrtl+tcp://") {
119 #[cfg(not(target_arch = "wasm32"))]
120 {
121 info!("Wave source is cxxrtl tcp");
122 Some(WaveSource::Cxxrtl(CxxrtlKind::Tcp {
123 url: url.replace("cxxrtl+tcp://", ""),
124 }))
125 }
126 #[cfg(target_arch = "wasm32")]
127 {
128 warn!("Loading waves from cxxrtl via tcp is unsupported in WASM builds.");
129 None
130 }
131 } else {
132 None
133 }
134}
135
136pub fn string_to_wavesource(path: &str) -> WaveSource {
137 if let Some(source) = url_to_wavesource(path) {
138 source
139 } else {
140 info!("Wave source is file");
141 WaveSource::File(path.into())
142 }
143}
144
145impl Display for WaveSource {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 match self {
148 WaveSource::File(file) => write!(f, "{file}"),
149 WaveSource::Data => write!(f, "File data"),
150 WaveSource::DragAndDrop(None) => write!(f, "Dropped file"),
151 WaveSource::DragAndDrop(Some(filename)) => write!(f, "Dropped file ({filename})"),
152 WaveSource::Url(url) => write!(f, "{url}"),
153 WaveSource::Cxxrtl(CxxrtlKind::Tcp { url }) => write!(f, "cxxrtl+tcp://{url}"),
154 WaveSource::Cxxrtl(CxxrtlKind::Mailbox) => write!(f, "cxxrtl mailbox"),
155 }
156 }
157}
158
159#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
160pub enum WaveFormat {
161 Vcd,
162 Fst,
163 Ghw,
164 CxxRtl,
165 Ftr,
166}
167
168impl Display for WaveFormat {
169 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
170 match self {
171 WaveFormat::Vcd => write!(f, "VCD"),
172 WaveFormat::Fst => write!(f, "FST"),
173 WaveFormat::Ghw => write!(f, "GHW"),
174 WaveFormat::CxxRtl => write!(f, "Cxxrtl"),
175 WaveFormat::Ftr => write!(f, "FTR"),
176 }
177 }
178}
179
180#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
181pub enum LoadOptions {
182 Clear,
183 KeepAvailable,
184 KeepAll,
185}
186
187impl From<(OpenMode, bool)> for LoadOptions {
188 fn from(val: (OpenMode, bool)) -> Self {
189 match val {
190 (OpenMode::Open, _) => LoadOptions::Clear,
191 (OpenMode::Switch, false) => LoadOptions::KeepAvailable,
192 (OpenMode::Switch, true) => LoadOptions::KeepAll,
193 }
194 }
195}
196
197pub struct LoadProgress {
198 pub started: Instant,
199 pub progress: LoadProgressStatus,
200}
201
202impl LoadProgress {
203 #[must_use]
204 pub fn new(progress: LoadProgressStatus) -> Self {
205 LoadProgress {
206 started: Instant::now(),
207 progress,
208 }
209 }
210}
211
212pub enum LoadProgressStatus {
213 Downloading(String),
214 Connecting(String),
215 ReadingHeader(WaveSource),
216 ReadingBody(WaveSource, u64, Arc<AtomicU64>),
217 LoadingVariables(u64),
218}
219
220impl SystemState {
221 pub fn load_from_file(
222 &mut self,
223 filename: Utf8PathBuf,
224 load_options: LoadOptions,
225 ) -> Result<()> {
226 match get_multi_extension(&filename) {
227 Some(ext) => match ext.as_str() {
228 STATE_FILE_EXTENSION => {
229 self.load_state_file(Some(filename.into_std_path_buf()));
230 Ok(())
231 }
232 TRANSACTIONS_FILE_EXTENSION => {
233 self.load_transactions_from_file(filename, load_options)
234 }
235 _ => self.load_wave_from_file(filename, load_options),
236 },
237 _ => self.load_wave_from_file(filename, load_options),
238 }
239 }
240
241 pub fn load_from_bytes(
242 &mut self,
243 source: WaveSource,
244 bytes: Vec<u8>,
245 load_options: LoadOptions,
246 ) {
247 if parse::is_ftr(&mut Cursor::new(&bytes)).is_ok_and(|is_ftr| is_ftr) {
248 self.load_transactions_from_bytes(source, bytes, load_options);
249 } else {
250 self.load_wave_from_bytes(source, bytes, load_options);
251 }
252 }
253
254 pub fn load_wave_from_file(
255 &mut self,
256 filename: Utf8PathBuf,
257 load_options: LoadOptions,
258 ) -> Result<()> {
259 info!("Loading a waveform file: {filename}");
260 let start = web_time::Instant::now();
261 let source = WaveSource::File(filename.clone());
262 let source_copy = source.clone();
263 let sender = self.channels.msg_sender.clone();
264
265 perform_work(move || {
266 let header_result = wellen::viewers::read_header_from_file(
267 filename.as_str(),
268 &WELLEN_SURFER_DEFAULT_OPTIONS,
269 )
270 .map_err(|e| anyhow!("{e:?}"))
271 .with_context(|| format!("Failed to parse wave file: {source}"));
272
273 let msg = match header_result {
274 Ok(header) => Message::WaveHeaderLoaded(
275 start,
276 source,
277 load_options,
278 HeaderResult::LocalFile(Box::new(header)),
279 ),
280 Err(e) => Message::Error(e),
281 };
282 checked_send(&sender, msg);
283 });
284
285 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingHeader(
286 source_copy,
287 )));
288 Ok(())
289 }
290
291 pub fn load_from_data(&mut self, data: Vec<u8>, load_options: LoadOptions) -> Result<()> {
292 self.load_from_bytes(WaveSource::Data, data, load_options);
293 Ok(())
294 }
295
296 pub fn load_from_dropped(&mut self, file: egui::DroppedFile) -> Result<()> {
297 info!("Got a dropped file");
298
299 let path = file.path.and_then(|x| Utf8PathBuf::try_from(x).ok());
300
301 if let Some(bytes) = file.bytes {
302 if bytes.is_empty() {
303 Err(anyhow!("Dropped an empty file"))
304 } else {
305 if let Some(path) = path.clone() {
306 if get_multi_extension(&path) == Some(STATE_FILE_EXTENSION.to_string()) {
307 let sender = self.channels.msg_sender.clone();
308 perform_async_work(async move {
309 let new_state = match ron::de::from_bytes(&bytes)
310 .context(format!("Failed loading {path}"))
311 {
312 Ok(s) => s,
313 Err(e) => {
314 error!("Failed to load state: {e:#?}");
315 return;
316 }
317 };
318
319 checked_send(
320 &sender,
321 Message::LoadState(new_state, Some(path.into_std_path_buf())),
322 );
323 });
324 } else {
325 self.load_from_bytes(
326 WaveSource::DragAndDrop(Some(path)),
327 bytes.to_vec(),
328 LoadOptions::Clear,
329 );
330 }
331 } else {
332 self.load_from_bytes(
333 WaveSource::DragAndDrop(path),
334 bytes.to_vec(),
335 LoadOptions::Clear,
336 );
337 }
338 Ok(())
339 }
340 } else if let Some(path) = path {
341 self.load_from_file(path, LoadOptions::Clear)
342 } else {
343 Err(anyhow!(
344 "Unknown how to load dropped file w/o path or bytes"
345 ))
346 }
347 }
348
349 pub fn load_wave_from_url(
350 &mut self,
351 url: String,
352 load_options: LoadOptions,
353 force_switch: bool,
354 ) {
355 match url_to_wavesource(&url) {
356 #[cfg(not(target_arch = "wasm32"))]
359 Some(WaveSource::Cxxrtl(kind)) => {
360 self.connect_to_cxxrtl(kind, load_options != LoadOptions::Clear);
361 }
362 _ => {
365 let sender = self.channels.msg_sender.clone();
366 let url_ = url.clone();
367 let file_index = self.user.selected_server_file_index;
368 info!("Loading wave from url: {url}");
369 perform_async_work(async move {
370 let maybe_response = reqwest::get(&url)
371 .map(|e| e.with_context(|| format!("Failed fetch download {url}")))
372 .await;
373 let response: reqwest::Response = match maybe_response {
374 Ok(r) => r,
375 Err(e) => {
376 checked_send(&sender, Message::Error(e));
377 return;
378 }
379 };
380
381 if let Some(value) = response.headers().get(HTTP_SERVER_KEY)
383 && matches!(value.to_str(), Ok(HTTP_SERVER_VALUE_SURFER))
384 {
385 match load_options {
386 LoadOptions::Clear => {
387 info!("Connecting to a surfer server at: {url}");
388 get_server_status(sender.clone(), url.clone(), 0);
390 if let Some(file_index) = file_index {
392 get_hierarchy_from_server(
393 sender.clone(),
394 url,
395 load_options,
396 file_index,
397 );
398 }
399 }
400 LoadOptions::KeepAvailable | LoadOptions::KeepAll => {
401 if let Some(file_index) = file_index {
403 if force_switch {
404 get_hierarchy_from_server(
405 sender.clone(),
406 url,
407 load_options,
408 file_index,
409 );
410 } else {
411 info!("Reloading from surver instance at: {url}");
412 server_reload(
413 sender.clone(),
414 url,
415 load_options,
416 file_index,
417 );
418 }
419 } else if force_switch {
420 get_server_status(sender.clone(), url.clone(), 0);
422 } else {
423 warn!(
424 "Cannot reload from surver instance without a selected file index"
425 );
426 }
427 }
428 }
429 return;
430 }
431
432 let bytes = response
434 .bytes()
435 .map(|e| e.with_context(|| format!("Failed to download {url}")))
436 .await;
437
438 let msg = match bytes {
439 Ok(b) => Message::FileDownloaded(url, b, load_options),
440 Err(e) => Message::Error(e),
441 };
442 checked_send(&sender, msg);
443 });
444
445 self.progress_tracker =
446 Some(LoadProgress::new(LoadProgressStatus::Downloading(url_)));
447 }
448 }
449 }
450
451 pub fn load_transactions_from_file(
452 &mut self,
453 filename: camino::Utf8PathBuf,
454 load_options: LoadOptions,
455 ) -> Result<()> {
456 info!("Loading a transaction file: {filename}");
457 let sender = self.channels.msg_sender.clone();
458 let source = WaveSource::File(filename.clone());
459 let format = WaveFormat::Ftr;
460
461 let result = ftr_parser::parse::parse_ftr(filename.into_std_path_buf());
462
463 info!("Done with loading ftr file");
464
465 let msg = match result {
466 Ok(ftr) => Message::TransactionStreamsLoaded(
467 source,
468 format,
469 TransactionContainer { inner: ftr },
470 load_options,
471 ),
472 Err(e) => Message::Error(Report::msg(e)),
473 };
474 checked_send(&sender, msg);
475 Ok(())
476 }
477 pub fn load_transactions_from_bytes(
478 &mut self,
479 source: WaveSource,
480 bytes: Vec<u8>,
481 load_options: LoadOptions,
482 ) {
483 let sender = self.channels.msg_sender.clone();
484
485 let result = parse::parse_ftr_from_bytes(bytes);
486
487 info!("Done with loading ftr file");
488
489 let msg = match result {
490 Ok(ftr) => Message::TransactionStreamsLoaded(
491 source,
492 WaveFormat::Ftr,
493 TransactionContainer { inner: ftr },
494 load_options,
495 ),
496 Err(e) => Message::Error(Report::msg(e)),
497 };
498 checked_send(&sender, msg);
499 }
500
501 pub fn server_status_to_progress(&mut self, server: &str, file_info: &SurverFileInfo) {
503 let body_loaded = self
505 .user
506 .waves
507 .as_ref()
508 .is_some_and(|w| w.inner.body_loaded());
509 if !body_loaded {
510 let source = WaveSource::Url(server.to_string());
512 let sender = self.channels.msg_sender.clone();
513 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingBody(
514 source,
515 file_info.bytes,
516 Arc::new(AtomicU64::new(file_info.bytes_loaded)),
517 )));
518 get_server_status(sender, server.to_string(), 250);
520 }
521 }
522
523 pub fn connect_to_cxxrtl(&mut self, kind: CxxrtlKind, keep_variables: bool) {
524 let sender = self.channels.msg_sender.clone();
525
526 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::Connecting(format!(
527 "{kind}"
528 ))));
529
530 let task = async move {
531 let container = match &kind {
532 #[cfg(not(target_arch = "wasm32"))]
533 CxxrtlKind::Tcp { url } => {
534 CxxrtlContainer::new_tcp(url, self.channels.msg_sender.clone()).await
535 }
536 #[cfg(target_arch = "wasm32")]
537 CxxrtlKind::Tcp { .. } => {
538 error!("Cxxrtl tcp is not supported om wasm");
539 return;
540 }
541 #[cfg(not(target_arch = "wasm32"))]
542 CxxrtlKind::Mailbox => {
543 error!("CXXRTL mailboxes are only supported on wasm for now");
544 return;
545 }
546 #[cfg(target_arch = "wasm32")]
547 CxxrtlKind::Mailbox => CxxrtlContainer::new_wasm_mailbox(sender.clone()).await,
548 };
549
550 let load_options = if keep_variables {
551 LoadOptions::KeepAvailable
552 } else {
553 LoadOptions::Clear
554 };
555 let msg = match container {
556 Ok(c) => Message::WavesLoaded(
557 WaveSource::Cxxrtl(kind),
558 WaveFormat::CxxRtl,
559 Box::new(WaveContainer::Cxxrtl(Box::new(Mutex::new(c)))),
560 load_options,
561 ),
562 Err(e) => Message::Error(e),
563 };
564 checked_send(&sender, msg);
565 };
566 #[cfg(not(target_arch = "wasm32"))]
567 futures::executor::block_on(task);
568 #[cfg(target_arch = "wasm32")]
569 wasm_bindgen_futures::spawn_local(task);
570 }
571
572 pub fn load_wave_from_bytes(
573 &mut self,
574 source: WaveSource,
575 bytes: Vec<u8>,
576 load_options: LoadOptions,
577 ) {
578 let start = web_time::Instant::now();
579 let sender = self.channels.msg_sender.clone();
580 let source_copy = source.clone();
581 perform_work(move || {
582 let header_result =
583 wellen::viewers::read_header(Cursor::new(bytes), &WELLEN_SURFER_DEFAULT_OPTIONS)
584 .map_err(|e| anyhow!("{e:?}"))
585 .with_context(|| format!("Failed to parse wave file: {source}"));
586
587 let msg = match header_result {
588 Ok(header) => Message::WaveHeaderLoaded(
589 start,
590 source,
591 load_options,
592 HeaderResult::LocalBytes(Box::new(header)),
593 ),
594 Err(e) => Message::Error(e),
595 };
596 checked_send(&sender, msg);
597 });
598
599 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingHeader(
600 source_copy,
601 )));
602 }
603
604 fn get_thread_pool() -> Option<rayon::ThreadPool> {
605 match rayon::ThreadPoolBuilder::new().build() {
608 Ok(pool) => Some(pool),
609 Err(e) => {
610 warn!("failed to create thread pool: {e:?}");
612 None
613 }
614 }
615 }
616
617 pub fn load_wave_body<R: std::io::BufRead + std::io::Seek + Sync + Send + 'static>(
618 &mut self,
619 source: WaveSource,
620 cont: wellen::viewers::ReadBodyContinuation<R>,
621 body_len: u64,
622 hierarchy: Arc<wellen::Hierarchy>,
623 ) {
624 let start = web_time::Instant::now();
625 let sender = self.channels.msg_sender.clone();
626 let source_copy = source.clone();
627 let progress = Arc::new(AtomicU64::new(0));
628 let progress_copy = progress.clone();
629 let pool = Self::get_thread_pool();
630
631 perform_work(move || {
632 let action = || {
633 let p = Some(progress_copy);
634 let body_result = wellen::viewers::read_body(cont, &hierarchy, p)
635 .map_err(|e| anyhow!("{e:?}"))
636 .with_context(|| format!("Failed to parse body of wave file: {source}"));
637
638 let msg = match body_result {
639 Ok(body) => Message::WaveBodyLoaded(start, source, BodyResult::Local(body)),
640 Err(e) => Message::Error(e),
641 };
642 checked_send(&sender, msg);
643 };
644 if let Some(pool) = pool {
645 pool.install(action);
646 } else {
647 action();
648 }
649 });
650
651 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingBody(
652 source_copy,
653 body_len,
654 progress,
655 )));
656 }
657
658 pub fn load_variables(&mut self, cmd: LoadSignalsCmd) {
659 let (signals, from_unique_id, payload) = cmd.destruct();
660 if signals.is_empty() {
661 return;
662 }
663 let num_signals = signals.len() as u64;
664 let start = web_time::Instant::now();
665 let sender = self.channels.msg_sender.clone();
666 let max_url_length = self.user.config.max_url_length;
667 match payload {
668 LoadSignalPayload::Local(mut source, hierarchy) => {
669 let pool = Self::get_thread_pool();
670
671 perform_work(move || {
672 let action = || {
673 let loaded = source.load_signals(&signals, &hierarchy, true);
674 let res = LoadSignalsResult::local(source, loaded, from_unique_id);
675 checked_send(&sender, Message::SignalsLoaded(start, res));
676 };
677 if let Some(pool) = pool {
678 pool.install(action);
679 } else {
680 action();
681 }
682 });
683 }
684 LoadSignalPayload::Remote(server) => {
685 perform_async_work(async move {
686 let res =
687 crate::remote::get_signals(server.clone(), &signals, max_url_length, 0)
688 .await
689 .map_err(|e| anyhow!("{e:?}"))
690 .with_context(|| {
691 format!("Failed to retrieve signals from remote server {server}")
692 });
693
694 let msg = match res {
695 Ok(loaded) => {
696 let res = LoadSignalsResult::remote(server, loaded, from_unique_id);
697 Message::SignalsLoaded(start, res)
698 }
699 Err(e) => Message::Error(e),
700 };
701 checked_send(&sender, msg);
702 });
703 }
704 }
705
706 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::LoadingVariables(
707 num_signals,
708 )));
709 }
710}
711
712pub fn draw_progress_information(ui: &mut egui::Ui, progress_data: &LoadProgress) {
713 match &progress_data.progress {
714 LoadProgressStatus::Connecting(url) => {
715 ui.horizontal(|ui| {
716 ui.spinner();
717 ui.monospace(format!("Connecting {url}"));
718 });
719 }
720 LoadProgressStatus::Downloading(url) => {
721 ui.horizontal(|ui| {
722 ui.spinner();
723 ui.monospace(format!("Downloading {url}"));
724 });
725 }
726 LoadProgressStatus::ReadingHeader(source) => {
727 ui.spinner();
728 ui.monospace(format!("Loading variable names from {source}"));
729 }
730 LoadProgressStatus::ReadingBody(source, 0, _) => {
731 ui.spinner();
732 ui.monospace(format!("Loading variable change data from {source}"));
733 }
734 LoadProgressStatus::LoadingVariables(num) => {
735 ui.spinner();
736 ui.monospace(format!("Loading {num} variables"));
737 }
738 LoadProgressStatus::ReadingBody(source, total, bytes_done) => {
739 let num_bytes = bytes_done.load(std::sync::atomic::Ordering::SeqCst);
740 let progress = num_bytes as f32 / *total as f32;
741 ui.monospace(format!(
742 "Loading variable change data from {source}. {} / {}",
743 bytesize::ByteSize::b(num_bytes),
744 bytesize::ByteSize::b(*total),
745 ));
746 let progress_bar = egui::ProgressBar::new(progress)
747 .show_percentage()
748 .desired_width(300.);
749 ui.add(progress_bar);
750 }
751 }
752}