Repository: probe-rs/probe-rs
Pull request #3256 (2025-04-25 19:03:14)

Communicate arbitrary semihosting streams over RPC


Patch:
diff --git a/changelog/changed-swd-probe-interface-sequence-type.md b/changelog/changed-swd-probe-interface-sequence-type.md
new file mode 100644
index 00000000..b22708e4
--- /dev/null
+++ b/changelog/changed-swd-probe-interface-sequence-type.md
@@ -0,0 +1,1 @@
+Changed SWD protocol probe interface with a type to allow passing single stream of enums instead of two streams of bits.
diff --git a/probe-rs-tools/src/bin/probe-rs/rpc/functions/monitor.rs b/probe-rs-tools/src/bin/probe-rs/rpc/functions/monitor.rs
index ec216956..2f5c375e 100644
--- a/probe-rs-tools/src/bin/probe-rs/rpc/functions/monitor.rs
+++ b/probe-rs-tools/src/bin/probe-rs/rpc/functions/monitor.rs
@@ -42,6 +42,7 @@ impl MonitorMode {
 }
 
 #[derive(Serialize, Deserialize, Schema)]
+// TODO: maybe this should be an enum of RttEvent | SemihostingEvent? Communicate semihosting stream without strings, by adding open/close events?
 pub enum MonitorEvent {
     RttDiscovered {
         up_channels: Vec<String>,
@@ -51,7 +52,10 @@ pub enum MonitorEvent {
         channel: u32,
         bytes: Vec<u8>,
     },
-    SemihostingOutput(SemihostingOutput),
+    SemihostingOutput {
+        stream: String,
+        data: String,
+    },
 }
 
 #[derive(Serialize, Deserialize, Schema)]
@@ -240,8 +244,8 @@ impl<F: FnMut(MonitorEvent)> MonitorEventHandler<F> {
             }
             SemihostingCommand::Errno(_) => Ok(None),
             other if SemihostingReader::is_io(other) => {
-                if let Some(output) = self.semihosting_reader.handle(other, core)? {
-                    (self.sender)(MonitorEvent::SemihostingOutput(output));
+                if let Some((stream, data)) = self.semihosting_reader.handle(other, core)? {
+                    (self.sender)(MonitorEvent::SemihostingOutput { stream, data });
                 }
                 Ok(None)
             }
@@ -250,12 +254,6 @@ impl<F: FnMut(MonitorEvent)> MonitorEventHandler<F> {
     }
 }
 
-#[derive(Serialize, Deserialize, Schema)]
-pub enum SemihostingOutput {
-    StdOut(String),
-    StdErr(String),
-}
-
 pub struct SemihostingReader {
     stdout_open: bool,
     stderr_open: bool,
@@ -286,7 +284,7 @@ impl SemihostingReader {
         &mut self,
         command: SemihostingCommand,
         core: &mut Core<'_>,
-    ) -> anyhow::Result<Option<SemihostingOutput>> {
+    ) -> anyhow::Result<Option<(String, String)>> {
         let out = match command {
             SemihostingCommand::Open(request) => {
                 self.handle_open(core, request)?;
@@ -299,7 +297,7 @@ impl SemihostingReader {
             SemihostingCommand::Write(request) => self.handle_write(core, request)?,
             SemihostingCommand::WriteConsole(request) => {
                 let str = request.read(core)?;
-                Some(SemihostingOutput::StdOut(str))
+                Some((String::from("stdout"), str))
             }
 
             _ => None,
@@ -355,18 +353,18 @@ impl SemihostingReader {
         &mut self,
         core: &mut Core<'_>,
         request: WriteRequest,
-    ) -> anyhow::Result<Option<SemihostingOutput>> {
+    ) -> anyhow::Result<Option<(String, String)>> {
         match request.file_handle() {
             handle if handle == Self::STDOUT.get() => {
                 if self.stdout_open {
                     let string = read_written_string(core, request)?;
-                    return Ok(Some(SemihostingOutput::StdOut(string)));
+                    return Ok(Some((String::from("stdout"), string)));
                 }
             }
             handle if handle == Self::STDERR.get() => {
                 if self.stderr_open {
                     let string = read_written_string(core, request)?;
-                    return Ok(Some(SemihostingOutput::StdErr(string)));
+                    return Ok(Some((String::from("stderr"), string)));
                 }
             }
             other => tracing::warn!(
diff --git a/probe-rs-tools/src/bin/probe-rs/util/cli.rs b/probe-rs-tools/src/bin/probe-rs/util/cli.rs
index 0533e23a..050df499 100644
--- a/probe-rs-tools/src/bin/probe-rs/util/cli.rs
+++ b/probe-rs-tools/src/bin/probe-rs/util/cli.rs
@@ -17,7 +17,7 @@ use crate::{
         functions::{
             CancelTopic,
             flash::{BootInfo, DownloadOptions, FlashLayout, ProgressEvent, VerifyResult},
-            monitor::{MonitorEvent, MonitorMode, MonitorOptions, SemihostingOutput},
+            monitor::{MonitorEvent, MonitorMode, MonitorOptions},
             probe::{
                 AttachRequest, AttachResult, DebugProbeEntry, DebugProbeSelector, SelectProbeResult,
             },
@@ -523,14 +523,13 @@ async fn print_monitor_event(
                 return;
             };
 
-            processor.process(&bytes);
-        }
-        MonitorEvent::SemihostingOutput(SemihostingOutput::StdOut(str)) => {
-            print!("{}", str)
-        }
-        MonitorEvent::SemihostingOutput(SemihostingOutput::StdErr(str)) => {
-            eprint!("{}", str)
+            processor.process(&bytes).await;
         }
+        MonitorEvent::SemihostingOutput { stream, data } => match stream.as_str() {
+            "stdout" => print!("{data}"),
+            "stderr" => eprint!("{data}"),
+            _ => {}
+        },
     }
 }
 
@@ -555,8 +554,8 @@ impl Channel {
         self.printer.prefix = format!("[{:width$}] ", self.channel, width = width);
     }
 
-    fn process(&mut self, bytes: &[u8]) {
-        _ = self.decoder.process(bytes, &mut self.printer);
+    async fn process(&mut self, bytes: &[u8]) {
+        _ = self.decoder.process(bytes, &mut self.printer).await;
     }
 }
 
diff --git a/probe-rs/src/probe/arm_debug_interface.rs b/probe-rs/src/probe/arm_debug_interface.rs
index 3d1d79c5..d05385e2 100644
--- a/probe-rs/src/probe/arm_debug_interface.rs
+++ b/probe-rs/src/probe/arm_debug_interface.rs
@@ -365,7 +365,7 @@ fn perform_swd_transfers<P: RawProtocolIo>(
         io_sequence.extend(&transfer.io_sequence());
     }
 
-    let result = probe.swd_io(io_sequence.direction_bits(), io_sequence.io_bits())?;
+    let result = probe.swd_io(io_sequence.io_items())?;
 
     let mut result_bits = &result[..];
 
@@ -826,32 +826,47 @@ enum TransferStatus {
     Failed(DapError),
 }
 
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum IoSequenceItem {
+    Output(bool),
+    Input,
+}
+
+impl From<IoSequenceItem> for bool {
+    fn from(item: IoSequenceItem) -> Self {
+        match item {
+            IoSequenceItem::Output(b) => b,
+            IoSequenceItem::Input => panic!("Input type is not supposed to hold a value!"),
+        }
+    }
+}
+
+impl From<IoSequenceItem> for u8 {
+    fn from(value: IoSequenceItem) -> Self {
+        match value {
+            IoSequenceItem::Output(b) => b as u8,
+            IoSequenceItem::Input => panic!("Input type is not supposed to hold a value!"),
+        }
+    }
+}
+
 struct IoSequence {
-    io: Vec<bool>,
-    direction: Vec<bool>,
+    io: Vec<IoSequenceItem>,
 }
 
 impl IoSequence {
-    const INPUT: bool = false;
-    const OUTPUT: bool = true;
-
     fn new() -> Self {
-        IoSequence {
-            io: vec![],
-            direction: vec![],
-        }
+        IoSequence { io: vec![] }
     }
 
     fn with_capacity(capacity: usize) -> Self {
         IoSequence {
             io: Vec::with_capacity(capacity),
-            direction: Vec::with_capacity(capacity),
         }
     }
 
     fn reserve(&mut self, idle_cycles_after: usize) {
         self.io.reserve(idle_cycles_after);
-        self.direction.reserve(idle_cycles_after);
     }
 
     fn from_bytes(data: &[u8], mut bits: usize) -> Self {
@@ -871,13 +886,11 @@ impl IoSequence {
     }
 
     fn add_output(&mut self, bit: bool) {
-        self.io.push(bit);
-        self.direction.push(Self::OUTPUT);
+        self.io.push(IoSequenceItem::Output(bit));
     }
 
     fn add_input(&mut self) {
-        self.io.push(false);
-        self.direction.push(Self::INPUT);
+        self.io.push(IoSequenceItem::Input);
     }
 
     fn add_input_sequence(&mut self, length: usize) {
@@ -886,17 +899,12 @@ impl IoSequence {
         }
     }
 
-    fn io_bits(&self) -> impl Iterator<Item = bool> + '_ {
+    fn io_items(&self) -> impl Iterator<Item = IoSequenceItem> + '_ {
         self.io.iter().copied()
     }
 
-    fn direction_bits(&self) -> impl Iterator<Item = bool> + '_ {
-        self.direction.iter().copied()
-    }
-
     fn extend(&mut self, other: &IoSequence) {
         self.io.extend_from_slice(&other.io);
-        self.direction.extend_from_slice(&other.direction);
     }
 }
 
@@ -1037,10 +1045,9 @@ pub trait RawProtocolIo {
     where
         I: IntoIterator<Item = bool>;
 
-    fn swd_io<D, S>(&mut self, dir: D, swdio: S) -> Result<Vec<bool>, DebugProbeError>
+    fn swd_io<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>;
+        S: IntoIterator<Item = IoSequenceItem>;
 
     fn swj_pins(
         &mut self,
@@ -1278,10 +1285,10 @@ fn send_sequence<P: RawProtocolIo + JTAGAccess>(
         WireProtocol::Jtag => {
             // Swj sequences should be shifted out to tms, since that is the pin
             // shared between swd and jtag modes.
-            probe.jtag_shift_tms(sequence.io_bits(), false)?;
+            probe.jtag_shift_tms(sequence.io_items().map(|i| i.into()), false)?;
         }
         WireProtocol::Swd => {
-            probe.swd_io(sequence.direction_bits(), sequence.io_bits())?;
+            probe.swd_io(sequence.io_items())?;
         }
     }
 
@@ -1300,7 +1307,7 @@ mod test {
     };
 
     use super::{
-        JTAG_ABORT_IR_VALUE, JTAG_ACCESS_PORT_IR_VALUE, JTAG_DEBUG_PORT_IR_VALUE,
+        IoSequenceItem, JTAG_ABORT_IR_VALUE, JTAG_ACCESS_PORT_IR_VALUE, JTAG_DEBUG_PORT_IR_VALUE,
         JTAG_DR_BIT_LENGTH, JTAG_STATUS_OK, JTAG_STATUS_WAIT, ProbeStatistics, RawProtocolIo,
         SwdSettings,
     };
@@ -1326,8 +1333,7 @@ mod test {
 
     #[derive(Debug)]
     struct MockJaylink {
-        direction_input: Option<Vec<bool>>,
-        io_input: Option<Vec<bool>>,
+        io_input: Option<Vec<IoSequenceItem>>,
         transfer_responses: Vec<Vec<bool>>,
         jtag_transactions: Vec<ExpectedJtagTransaction>,
 
@@ -1345,7 +1351,6 @@ mod test {
     impl MockJaylink {
         fn new() -> Self {
             Self {
-                direction_input: None,
                 io_input: None,
                 transfer_responses: vec![vec![]],
                 jtag_transactions: vec![],
@@ -1584,19 +1589,12 @@ mod test {
             Ok(())
         }
 
-        fn swd_io<D, S>(&mut self, dir: D, swdio: S) -> Result<Vec<bool>, DebugProbeError>
+        fn swd_io<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
         where
-            D: IntoIterator<Item = bool>,
-            S: IntoIterator<Item = bool>,
+            S: IntoIterator<Item = IoSequenceItem>,
         {
-            self.direction_input = Some(dir.into_iter().collect());
             self.io_input = Some(swdio.into_iter().collect());
 
-            assert_eq!(
-                self.direction_input.as_ref().unwrap().len(),
-                self.io_input.as_ref().unwrap().len()
-            );
-
             let transfer_response = self.transfer_responses.remove(0);
 
             let io_bits = self.io_input.as_ref().map(|v| v.len()).unwrap();
diff --git a/probe-rs/src/probe/blackmagic/mod.rs b/probe-rs/src/probe/blackmagic/mod.rs
index 9511155c..381b1f02 100644
--- a/probe-rs/src/probe/blackmagic/mod.rs
+++ b/probe-rs/src/probe/blackmagic/mod.rs
@@ -25,7 +25,7 @@ use serialport::{SerialPortType, available_ports};
 
 use super::{
     DebugProbeError, ProbeCreationError, ProbeError, WireProtocol,
-    arm_debug_interface::{ProbeStatistics, RawProtocolIo, SwdSettings},
+    arm_debug_interface::{IoSequenceItem, ProbeStatistics, RawProtocolIo, SwdSettings},
     common::{JtagDriverState, RawJtagIo},
 };
 
@@ -599,6 +599,15 @@ impl From<bool> for SwdDirection {
     }
 }
 
+impl From<IoSequenceItem> for SwdDirection {
+    fn from(value: IoSequenceItem) -> Self {
+        match value {
+            IoSequenceItem::Input => SwdDirection::Input,
+            IoSequenceItem::Output(_) => SwdDirection::Output,
+        }
+    }
+}
+
 /// A Black Magic Probe.
 pub struct BlackMagicProbe {
     reader: BufReader<Box<dyn Read + Send>>,
@@ -908,24 +917,19 @@ impl BlackMagicProbe {
     ///
     /// The caller needs to ensure that the given iterators are not longer than the maximum transfer size
     /// allowed. It seems that the maximum transfer size is determined by [`self.max_mem_block_size`].
-    fn perform_swdio_transfer<D, S>(
-        &mut self,
-        dir: D,
-        swdio: S,
-    ) -> Result<Vec<bool>, DebugProbeError>
+    #[allow(clippy::unnecessary_fallible_conversions)] //  IoSequenceItem conversion may panic
+    fn perform_swdio_transfer<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>,
+        S: IntoIterator<Item = IoSequenceItem>,
     {
-        let dir = dir.into_iter();
-        let swdio = swdio.into_iter();
+        let swdio_sequence = swdio.into_iter();
         let mut output = vec![];
 
         let mut accumulator = 0u32;
         let mut accumulator_length = 0;
 
-        for (dir, swdio) in dir.zip(swdio) {
-            let dir = SwdDirection::from(dir);
+        for swdio in swdio_sequence {
+            let dir: SwdDirection = swdio.into();
             if dir != self.swd_direction
                 || accumulator_length >= core::mem::size_of_val(&accumulator) * 8
             {
@@ -950,7 +954,11 @@ impl BlackMagicProbe {
                 accumulator_length = 0;
             }
             self.swd_direction = dir;
-            accumulator |= if swdio { 1 << accumulator_length } else { 0 };
+            accumulator |= if swdio.try_into().unwrap_or(false) {
+                1 << accumulator_length
+            } else {
+                0
+            };
             accumulator_length += 1;
         }
 
@@ -1273,13 +1281,12 @@ impl RawProtocolIo for BlackMagicProbe {
         Ok(())
     }
 
-    fn swd_io<D, S>(&mut self, dir: D, swdio: S) -> Result<Vec<bool>, DebugProbeError>
+    fn swd_io<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>,
+        S: IntoIterator<Item = IoSequenceItem>,
     {
         self.probe_statistics.report_io();
-        self.perform_swdio_transfer(dir, swdio)
+        self.perform_swdio_transfer(swdio)
     }
 
     fn swj_pins(
diff --git a/probe-rs/src/probe/ftdi/mod.rs b/probe-rs/src/probe/ftdi/mod.rs
index 9c86872e..112dce57 100644
--- a/probe-rs/src/probe/ftdi/mod.rs
+++ b/probe-rs/src/probe/ftdi/mod.rs
@@ -13,7 +13,7 @@ use crate::{
     probe::{
         DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector, JTAGAccess,
         ProbeCreationError, ProbeFactory, ScanChainElement, WireProtocol,
-        arm_debug_interface::{ProbeStatistics, RawProtocolIo, SwdSettings},
+        arm_debug_interface::{IoSequenceItem, ProbeStatistics, RawProtocolIo, SwdSettings},
         common::{JtagDriverState, RawJtagIo},
     },
 };
@@ -457,10 +457,9 @@ impl RawProtocolIo for FtdiProbe {
         Ok(())
     }
 
-    fn swd_io<D, S>(&mut self, _dir: D, _swdio: S) -> Result<Vec<bool>, DebugProbeError>
+    fn swd_io<S>(&mut self, _swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>,
+        S: IntoIterator<Item = IoSequenceItem>,
     {
         Err(DebugProbeError::NotImplemented {
             function_name: "swd_io",
diff --git a/probe-rs/src/probe/jlink/bits.rs b/probe-rs/src/probe/jlink/bits.rs
index a3f7a96a..2754435e 100644
--- a/probe-rs/src/probe/jlink/bits.rs
+++ b/probe-rs/src/probe/jlink/bits.rs
@@ -65,7 +65,11 @@ pub(crate) trait IteratorExt: Sized {
     fn collapse_bytes(self) -> ByteIter<Self>;
 }
 
-impl<I: Iterator<Item = bool>> IteratorExt for I {
+impl<I, T> IteratorExt for I
+where
+    I: Iterator<Item = T>,
+    T: Into<bool>,
+{
     fn collapse_bytes(self) -> ByteIter<Self> {
         ByteIter { inner: self }
     }
@@ -75,7 +79,11 @@ pub(crate) struct ByteIter<I> {
     inner: I,
 }
 
-impl<I: Iterator<Item = bool>> Iterator for ByteIter<I> {
+impl<I, T> Iterator for ByteIter<I>
+where
+    I: Iterator<Item = T>,
+    T: Into<bool>,
+{
     type Item = u8;
 
     fn next(&mut self) -> Option<u8> {
@@ -84,7 +92,7 @@ impl<I: Iterator<Item = bool>> Iterator for ByteIter<I> {
         let mut has_data = false;
         for (pos, bit) in self.inner.by_ref().take(8).enumerate() {
             has_data = true;
-            byte |= (bit as u8) << pos;
+            byte |= (bit.into() as u8) << pos;
         }
 
         has_data.then_some(byte)
diff --git a/probe-rs/src/probe/jlink/mod.rs b/probe-rs/src/probe/jlink/mod.rs
index e439a6a8..6a30d848 100644
--- a/probe-rs/src/probe/jlink/mod.rs
+++ b/probe-rs/src/probe/jlink/mod.rs
@@ -48,7 +48,7 @@ use crate::{
     probe::{
         DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector, ProbeFactory,
         WireProtocol,
-        arm_debug_interface::{ProbeStatistics, RawProtocolIo, SwdSettings},
+        arm_debug_interface::{IoSequenceItem, ProbeStatistics, RawProtocolIo, SwdSettings},
     },
 };
 
@@ -791,10 +791,9 @@ impl JLink {
     ///
     /// The caller needs to ensure that the given iterators are not longer than the maximum transfer size
     /// allowed. It seems that the maximum transfer size is determined by [`JLink::max_mem_block_size`].
-    fn perform_swdio_transfer<D, S>(&self, dir: D, swdio: S) -> Result<Vec<bool>, DebugProbeError>
+    fn perform_swdio_transfer<S>(&self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>,
+        S: IntoIterator<Item = IoSequenceItem>,
     {
         self.require_interface_selected(Interface::Swd)?;
 
@@ -803,30 +802,42 @@ impl JLink {
         let max_bits = (self.max_mem_block_size - COMMAND_OVERHEAD) / 2 * 8;
         let max_bits = std::cmp::min(max_bits, 65535);
 
-        let dir_chunks = dir.into_iter().chunks(max_bits);
         let swdio_chunks = swdio.into_iter().chunks(max_bits);
 
-        #[allow(clippy::useless_conversion)]
-        let chunks = dir_chunks.into_iter().zip(swdio_chunks.into_iter());
-
         let mut output = Vec::new();
         let mut buf = Vec::with_capacity(self.max_mem_block_size);
         buf.resize(COMMAND_OVERHEAD, 0);
 
-        for (dir, swdio) in chunks {
+        for swdio in swdio_chunks.into_iter() {
             buf.truncate(COMMAND_OVERHEAD);
 
-            let mut dir_bit_count = 0;
-            buf.extend(dir.inspect(|_| dir_bit_count += 1).collapse_bytes());
-            let mut swdio_bit_count = 0;
-            buf.extend(swdio.inspect(|_| swdio_bit_count += 1).collapse_bytes());
+            const INPUT: bool = false;
+            const OUTPUT: bool = true;
+
+            let swdio: Vec<_> = swdio.collect();
 
-            assert_eq!(
-                dir_bit_count, swdio_bit_count,
-                "`dir` and `swdio` must have the same number of bits"
+            //  Extend using the direction bits:
+            buf.extend(
+                swdio
+                    .iter()
+                    .map(|item| match item {
+                        IoSequenceItem::Input => INPUT,
+                        IoSequenceItem::Output { .. } => OUTPUT,
+                    })
+                    .collapse_bytes(),
+            );
+            //  Extend using the value bits:
+            buf.extend(
+                swdio
+                    .iter()
+                    .map(|item| match item {
+                        IoSequenceItem::Input => false,
+                        IoSequenceItem::Output(x) => *x,
+                    })
+                    .collapse_bytes(),
             );
 
-            let num_bits = dir_bit_count as u16;
+            let num_bits = swdio.len() as u16;
 
             buf[0] = Command::HwJtag3 as u8;
             // buf[1] is dummy data for alignment
@@ -1206,13 +1217,12 @@ impl RawProtocolIo for JLink {
         Ok(())
     }
 
-    fn swd_io<D, S>(&mut self, dir: D, swdio: S) -> Result<Vec<bool>, DebugProbeError>
+    fn swd_io<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>,
+        S: IntoIterator<Item = IoSequenceItem>,
     {
         self.probe_statistics.report_io();
-        self.perform_swdio_transfer(dir, swdio)
+        self.perform_swdio_transfer(swdio)
     }
 
     fn swj_pins(


Test patch:
diff --git a/probe-rs-tools/src/bin/probe-rs/rpc/functions/test.rs b/probe-rs-tools/src/bin/probe-rs/rpc/functions/test.rs
index adef7b55..4d0b6629 100644
--- a/probe-rs-tools/src/bin/probe-rs/rpc/functions/test.rs
+++ b/probe-rs-tools/src/bin/probe-rs/rpc/functions/test.rs
@@ -13,7 +13,7 @@ use crate::{
             ListTestsEndpoint, MonitorTopic, RpcResult, RpcSpawnContext, RunTestEndpoint,
             WireTxImpl,
             flash::BootInfo,
-            monitor::{MonitorEvent, RttPoller, SemihostingOutput, SemihostingReader},
+            monitor::{MonitorEvent, RttPoller, SemihostingReader},
         },
         utils::run_loop::{ReturnReason, RunLoop},
     },
@@ -97,12 +97,6 @@ pub enum TestResult {
     Cancelled,
 }
 
-#[derive(Serialize, Deserialize, Schema)]
-pub enum TestEvent {
-    RttOutput(String),
-    SemihostingOutput(SemihostingOutput),
-}
-
 #[derive(Serialize, Deserialize, Schema)]
 pub struct ListTestsRequest {
     pub sessid: Key<Session>,
@@ -314,8 +308,8 @@ impl<F: FnMut(MonitorEvent)> ListEventHandler<F> {
                 Ok(Some(list.into()))
             }
             other if SemihostingReader::is_io(other) => {
-                if let Some(output) = self.semihosting_reader.handle(other, core)? {
-                    (self.sender)(MonitorEvent::SemihostingOutput(output));
+                if let Some((stream, data)) = self.semihosting_reader.handle(other, core)? {
+                    (self.sender)(MonitorEvent::SemihostingOutput { stream, data });
                 }
                 Ok(None)
             }
@@ -389,8 +383,8 @@ impl<F: FnMut(MonitorEvent)> RunEventHandler<F> {
                 Ok(Some(TestOutcome::Panic))
             }
             other if SemihostingReader::is_io(other) => {
-                if let Some(output) = self.semihosting_reader.handle(other, core)? {
-                    (self.sender)(MonitorEvent::SemihostingOutput(output));
+                if let Some((stream, data)) = self.semihosting_reader.handle(other, core)? {
+                    (self.sender)(MonitorEvent::SemihostingOutput { stream, data });
                 }
                 Ok(None)
             }
diff --git a/smoke-tester/src/tests.rs b/smoke-tester/src/tests.rs
index 50591f6f..d476fc50 100644
--- a/smoke-tester/src/tests.rs
+++ b/smoke-tester/src/tests.rs
@@ -82,7 +82,7 @@ fn test_write_read(
     println_test_status!(
         tracker,
         blue,
-        "Testing:  write and read at address {:#010X}: {scenario}",
+        "Testing: write and read at address {:#010X}: {scenario}",
         address
     );
 
@@ -118,6 +118,14 @@ fn test_memory_access(tracker: &TestTracker, core: &mut Core) -> TestResult {
     for ram in memory_regions {
         let ram_start = ram.range.start;
         let ram_size = ram.range.end - ram.range.start;
+        println_test_status!(
+            tracker,
+            blue,
+            "Testing region: {} ({:#010X} - {:#010X})",
+            ram.name.as_deref().unwrap_or("<unnamed region>"),
+            ram.range.start,
+            ram.range.end
+        );
 
         println_test_status!(tracker, blue, "Test - RAM Start 32");
         // Write first word
@@ -225,6 +233,14 @@ fn test_hw_breakpoints(tracker: &TestTracker, core: &mut Core) -> TestResult {
     // For this test, we assume that code is executed from Flash / non-volatile memory, and try to set breakpoints
     // in these regions.
     for region in memory_regions {
+        println_test_status!(
+            tracker,
+            blue,
+            "Testing region: {} ({:#010X} - {:#010X})",
+            region.name.as_deref().unwrap_or("<unnamed region>"),
+            region.range.start,
+            region.range.end
+        );
         let initial_breakpoint_addr = region.range.start;
 
         let num_breakpoints = core.available_breakpoint_units().into_diagnostic()?;
diff --git a/smoke-tester/src/tests/stepping.rs b/smoke-tester/src/tests/stepping.rs
index 3097b20f..b2780139 100644
--- a/smoke-tester/src/tests/stepping.rs
+++ b/smoke-tester/src/tests/stepping.rs
@@ -1,5 +1,6 @@
 use std::time::Duration;
 
+use colored::Colorize;
 use linkme::distributed_slice;
 use miette::IntoDiagnostic;
 use probe_rs::{
@@ -7,13 +8,13 @@ use probe_rs::{
     config::MemoryRegion, probe::DebugProbeError,
 };
 
-use crate::{CORE_TESTS, TestFailure, TestResult, TestTracker};
+use crate::{CORE_TESTS, TestFailure, TestResult, TestTracker, println_test_status};
 
 const TEST_CODE: &[u8] = include_bytes!("test_arm.bin");
 
 #[distributed_slice(CORE_TESTS)]
 fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
-    println!("Testing stepping...");
+    println_test_status!(tracker, blue, "Testing stepping on core {}...", core.id());
 
     if core.architecture() != Architecture::Arm {
         // Not implemented for RISC-V yet
@@ -70,9 +71,11 @@ fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
 
     assert_eq!(r0_value, 0);
 
-    println!("R0 value ok!");
+    println_test_status!(tracker, green, "R0 value ok!");
 
-    println!(
+    println_test_status!(
+        tracker,
+        blue,
         "Core halted at {:#08x}, now trying to run...",
         core_information.pc
     );
@@ -84,25 +87,25 @@ fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
     match core.wait_for_core_halted(Duration::from_millis(100)) {
         Ok(()) => {}
         Err(Error::Probe(DebugProbeError::Timeout)) => {
-            println!("Core did not halt after timeout!");
+            println_test_status!(tracker, yellow, "Core did not halt after timeout!");
             core.halt(Duration::from_millis(100)).into_diagnostic()?;
 
             let pc: u64 = core
                 .read_core_reg(core.program_counter())
                 .into_diagnostic()?;
 
-            println!("Core stopped at: {pc:#08x}");
+            println_test_status!(tracker, blue, "Core stopped at: {pc:#08x}");
 
             let r2_val: u64 = core
                 .read_core_reg(registers.core_register(2))
                 .into_diagnostic()?;
 
-            println!("$r2 = {r2_val:#08x}");
+            println_test_status!(tracker, blue, "$r2 = {r2_val:#08x}");
         }
         Err(other) => return Err(other.into()),
     }
 
-    println!("Core halted again!");
+    println_test_status!(tracker, green, "Core halted again!");
 
     let core_status = core.status().into_diagnostic()?;
 
@@ -118,13 +121,17 @@ fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
 
     assert_eq!(pc, break_address);
 
-    println!("Core halted at {pc:#08x}, now trying to run...");
+    println_test_status!(
+        tracker,
+        blue,
+        "Core halted at {pc:#08x}, now trying to run..."
+    );
 
     // Increase PC by 2 to skip breakpoint.
     core.write_core_reg(core.program_counter(), pc + 2)
         .into_diagnostic()?;
 
-    println!("Run core again, with pc = {:#010x}", pc + 2);
+    println_test_status!(tracker, blue, "Run core again, with pc = {:#010x}", pc + 2);
 
     // Run to the finish
     core.run().into_diagnostic()?;
@@ -136,20 +143,20 @@ fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
     match core.wait_for_core_halted(Duration::from_millis(100)) {
         Ok(()) => {}
         Err(Error::Probe(DebugProbeError::Timeout)) => {
-            println!("Core did not halt after timeout!");
+            println_test_status!(tracker, yellow, "Core did not halt after timeout!");
             core.halt(Duration::from_millis(100)).into_diagnostic()?;
 
             let pc: u64 = core
                 .read_core_reg(core.program_counter())
                 .into_diagnostic()?;
 
-            println!("Core stopped at: {pc:#08x}");
+            println_test_status!(tracker, blue, "Core stopped at: {pc:#08x}");
 
             let r2_val: u64 = core
                 .read_core_reg(registers.core_register(2))
                 .into_diagnostic()?;
 
-            println!("$r2 = {r2_val:#08x}");
+            println_test_status!(tracker, blue, "$r2 = {r2_val:#08x}");
         }
         Err(other) => return Err(other.into()),
     }