vexide_core/io/stdio.rs
1use no_std_io::io::{self, Write};
2use vex_sdk::{vexSerialReadChar, vexSerialWriteBuffer, vexSerialWriteFree};
3
4use crate::sync::{Mutex, MutexGuard};
5
6pub(crate) const STDIO_CHANNEL: u32 = 1;
7
8static STDOUT: Mutex<StdoutRaw> = Mutex::new(StdoutRaw);
9static STDIN: Mutex<StdinRaw> = Mutex::new(StdinRaw);
10
11/// A handle to a raw instance of the serial output stream of this program.
12///
13/// This handle is not synchronized or buffered in any fashion. Constructed via
14/// the `stdout_raw` function.
15struct StdoutRaw;
16
17impl io::Write for StdoutRaw {
18 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
19 let written =
20 unsafe { vexSerialWriteBuffer(STDIO_CHANNEL, buf.as_ptr(), buf.len() as u32) };
21
22 if written == -1 {
23 return Err(io::Error::new(
24 io::ErrorKind::Other,
25 "Internal write error occurred.",
26 ));
27 }
28
29 self.flush()?;
30
31 Ok(written as usize)
32 }
33
34 fn flush(&mut self) -> io::Result<()> {
35 unsafe {
36 while vexSerialWriteFree(STDIO_CHANNEL) < Stdout::INTERNAL_BUFFER_SIZE as _ {
37 // Allow VEXos to flush the buffer by yielding.
38 vex_sdk::vexTasksRun();
39 }
40 }
41
42 Ok(())
43 }
44}
45
46/// A locked serial output stream.
47/// Only one of these can exist at a time and writes occur without waiting.
48pub struct StdoutLock<'a> {
49 inner: MutexGuard<'a, StdoutRaw>,
50}
51
52impl Write for StdoutLock<'_> {
53 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
54 self.inner.write(buf)
55 }
56
57 fn flush(&mut self) -> io::Result<()> {
58 self.inner.flush()
59 }
60}
61
62/// A handle to the serial output stream of this program.
63///
64/// An instance of this can be obtained using the [`stdout`] function.
65pub struct Stdout(());
66
67/// Constructs a handle to the serial output stream
68#[must_use]
69pub const fn stdout() -> Stdout {
70 Stdout(())
71}
72
73impl Stdout {
74 /// The size of the internal VEXOs FIFO serial out buffer.
75 pub const INTERNAL_BUFFER_SIZE: usize = 2048;
76
77 /// Locks the stdout for writing.
78 /// This function will wait until the lock is acquired.
79 pub async fn lock(&self) -> StdoutLock<'static> {
80 StdoutLock {
81 inner: STDOUT.lock().await,
82 }
83 }
84 /// Attempts to lock the stdout for writing.
85 ///
86 /// This function will return `None` if the lock could not be acquired.
87 pub fn try_lock(&self) -> Option<StdoutLock<'static>> {
88 Some(StdoutLock {
89 inner: STDOUT.try_lock()?,
90 })
91 }
92}
93
94struct StdinRaw;
95
96impl io::Read for StdinRaw {
97 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
98 let mut iterator = buf.iter_mut();
99
100 let mut byte: i32;
101 let mut written: usize = 0;
102
103 // Little but cursed, but hey it gets the job done...
104 while {
105 byte = unsafe { vexSerialReadChar(STDIO_CHANNEL) };
106 byte != -1
107 } {
108 if let Some(next) = iterator.next() {
109 *next = byte as u8;
110 written += 1;
111 } else {
112 return Ok(written);
113 }
114 }
115
116 Ok(written)
117 }
118}
119
120/// A locked serial input stream.
121/// Only one of these can exist at a time and reads occur without waiting.
122pub struct StdinLock<'a> {
123 inner: MutexGuard<'a, StdinRaw>,
124}
125
126impl io::Read for StdinLock<'_> {
127 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
128 self.inner.read(buf)
129 }
130}
131
132/// A handle to the serial input stream of this program.
133///
134/// An instance of this can be obtained using the [`stdin`] function.
135pub struct Stdin(());
136
137impl Stdin {
138 /// The size of the internal VEXos serial in buffer.
139 pub const STDIN_BUFFER_SIZE: usize = 4096;
140
141 /// Locks the stdin for reading.
142 /// This function is blocking and will wait until the lock is acquired.
143 pub async fn lock(&self) -> StdinLock<'static> {
144 StdinLock {
145 inner: STDIN.lock().await,
146 }
147 }
148 /// Attempts to lock the stdin for writing.
149 ///
150 /// This function will return `None` if the lock could not be acquired.
151 pub fn try_lock(&self) -> Option<StdinLock<'static>> {
152 Some(StdinLock {
153 inner: STDIN.try_lock()?,
154 })
155 }
156}
157
158/// Constructs a handle to the serial input stream.
159#[must_use]
160pub const fn stdin() -> Stdin {
161 Stdin(())
162}
163
164//////////////////////////////
165// Printing implementations //
166//////////////////////////////
167
168#[doc(hidden)]
169#[inline]
170pub fn __print(args: core::fmt::Arguments<'_>) {
171 use alloc::format;
172
173 use crate::io::Write;
174
175 // Panic on print if stdout is not available.
176 // While this is less than ideal,
177 // the alternative is either ingoring the print, a complete deadlock, or writing unsafely without locking.
178 let mut stdout = stdout()
179 .try_lock()
180 .expect("Attempted to print while stdout was already locked.");
181
182 // Format the arguments into a byte buffer before printing them.
183 // This lets us calculate if the bytes will overflow the buffer before printing them.
184 let formatted_bytes = format!("{args}").into_bytes();
185 let remaining_bytes_in_buffer = unsafe { vexSerialWriteFree(STDIO_CHANNEL) as usize };
186
187 // Write all of our data in chunks the size of the outgoing serial buffer.
188 // This ensures that writes of length greater than [`Stdout::INTERNAL_BUFFER_SIZE`] can still be written
189 // by flushing several times.
190 for chunk in formatted_bytes.chunks(Stdout::INTERNAL_BUFFER_SIZE) {
191 // If this chunk would overflow the buffer and cause a panic during `write_all`, wait for the buffer to clear.
192 // Not only does this prevent a panic (if the panic handler prints it could cause a recursive panic and immediately exit. **Very bad**),
193 // but it also allows prints and device comms inside of tight loops that have a print.
194 //
195 //TODO: In the future we may want to actually handle prints in tight loops
196 //TODO: in a way that makes it more clear that that loop is hogging executor time.
197 //TODO: In the past a lack of serial output was a tell that the executor was being hogged.
198 //TODO: Flushing the serial buffer now removes this tell, though.
199 if remaining_bytes_in_buffer < chunk.len() {
200 // Flushing is infallible, so we can unwrap here.
201 stdout.flush().unwrap();
202 }
203
204 // Re-use the buffer to write the formatted bytes to the serial output.
205 // This technically should never error because we have already flushed the buffer if it would overflow.
206 if let Err(e) = stdout.write_all(chunk) {
207 panic!("failed printing to stdout: {e}");
208 }
209 }
210}
211
212#[macro_export]
213/// Prints a message to the standard output and appends a newline.
214macro_rules! println {
215 () => {
216 $crate::print!("\n")
217 };
218 ($($arg:tt)*) => {
219 $crate::print!("{}\n", format_args!($($arg)*))
220 };
221}
222pub use println;
223
224#[macro_export]
225/// Prints a message to the standard output.
226macro_rules! print {
227 ($($arg:tt)*) => {{
228 $crate::io::__print(format_args!($($arg)*))
229 }};
230}
231pub use print;
232
233#[macro_export]
234#[expect(
235 edition_2024_expr_fragment_specifier,
236 reason = "OK for this macro to accept `const {}` expressions"
237)]
238/// Prints and returns the value of a given expression for quick and dirty debugging.
239macro_rules! dbg {
240 () => {
241 $crate::println!("[{}:{}]", file!(), line!())
242 };
243 ($val:expr $(,)?) => {
244 match $val {
245 tmp => {
246 $crate::println!("[{}:{}] {} = {:#?}", file!(), line!(), stringify!($val), &tmp);
247 tmp
248 }
249 }
250 };
251 ($($val:expr),+ $(,)?) => {
252 ($($crate::dbg!($val)),+,)
253 };
254}
255pub use dbg;