From 993d304aa1d26f7ae6e25a19ff289bb4627e29d9 Mon Sep 17 00:00:00 2001 From: Cosma George Date: Sat, 29 Apr 2023 19:02:22 +0300 Subject: [PATCH 01/11] Implemented a proof of concept of reading from serial --- Cargo.toml | 5 ++++ src/cli.rs | 5 ++-- src/main.rs | 18 +++++++++----- src/serial_interface.rs | 52 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 src/serial_interface.rs diff --git a/Cargo.toml b/Cargo.toml index 59905e5..70a7d74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bytes = "1.4.0" clap = { version = "4.1.1", features = ["cargo"] } +futures = "0.3.28" +tokio = { version ="1.28.0", features= ["full"] } +tokio-serial = "5.4.4" +tokio-util = {version = "0.7.8", features = ["full"] } diff --git a/src/cli.rs b/src/cli.rs index 1a903d5..cd70f14 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use clap::{arg, crate_version, Command}; +use clap::{arg, crate_version, Command, value_parser}; /// Create the [command](clap::Command) object which will handle all of the command line arguments. pub fn make_cli() -> Command { @@ -39,7 +39,7 @@ fn get_app_args() -> Vec { /// with channels and computer-board communication. fn get_channel_args() -> Vec { vec![ - arg!(-p --port "The serial port or device name to use"), + arg!(-p --port "The serial port or device name to use"), arg!(--serial "Use the serial bootloader to flash") .action(clap::ArgAction::SetTrue), arg!(--jlink "Use JLinkExe to flash") @@ -65,6 +65,7 @@ fn get_channel_args() -> Vec { arg!(--"page-size" "Explicitly specify how many bytes in a flash page") .default_value("0"), arg!(--"baud-rate" "If using serial, set the target baud rate") + .value_parser(value_parser!(u32)) .default_value("115200"), arg!(--"no-bootloader-entry" "Tell Tockloader to assume the bootloader is already active") .action(clap::ArgAction::SetTrue), diff --git a/src/main.rs b/src/main.rs index 7c80977..a8c1061 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,11 @@ mod cli; +mod serial_interface; use cli::make_cli; -fn main() { +use crate::serial_interface::{open_port, run_terminal}; + +#[tokio::main] +async fn main() -> tokio_serial::Result<()> { let matches = make_cli().get_matches(); if matches.get_flag("debug") { @@ -10,12 +14,12 @@ fn main() { match matches.subcommand() { Some(("listen", sub_matches)) => { + let port = sub_matches.get_one::("port").unwrap(); + let baud_rate = *sub_matches.get_one::("baud-rate").unwrap(); + println!("Got the listen subcommand"); - let default_adr = "NONE".to_string(); - let adr = sub_matches - .get_one::("app-address") - .unwrap_or(&default_adr); - println!("With App Address {adr}"); + let stream = open_port(port.to_string(), baud_rate)?; + run_terminal(stream).await; } // If only the "--debug" flag is set, then this branch is executed // Or, more likely at this stage, a subcommand hasn't been implemented yet. @@ -24,4 +28,6 @@ fn main() { _ = make_cli().print_help(); } } + + Ok(()) } diff --git a/src/serial_interface.rs b/src/serial_interface.rs new file mode 100644 index 0000000..ab3a0b2 --- /dev/null +++ b/src/serial_interface.rs @@ -0,0 +1,52 @@ +use futures::stream::StreamExt; +use std::io::Write; +use std::{env, io, str}; +use tokio_util::codec::{Decoder, Encoder}; + +use bytes::BytesMut; +use tokio_serial::SerialPortBuilderExt; + + +use tokio_serial::{SerialStream}; + +struct LineCodec; + +impl Decoder for LineCodec { + type Item = String; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + // let newline = src.as_ref().iter().position(|b| *b == b'\n'); + // if let Some(n) = newline { + // let line = src.split_to(n + 1); + // return match str::from_utf8(line.as_ref()) { + // Ok(s) => Ok(Some(s.to_string())), + // Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Invalid String")), + // }; + // } + // Ok(None) + if src.is_empty() { + return Ok(None); + } + let result = match str::from_utf8(&src) { + Ok(s) => Ok(Some(s.to_string())), + Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Invalid String")), + }; + src.clear(); + return result; + } +} + +pub fn open_port(path: String, baud_rate:u32) -> tokio_serial::Result { + // Is it async? It can't be awaited... + tokio_serial::new(path, baud_rate).open_native_async() +} + +pub async fn run_terminal(stream: SerialStream) { + let mut reader = LineCodec.framed(stream); + while let Some(line_result) = reader.next().await { + let line = line_result.expect("Failed to read line"); + print!("{}", line); + io::stdout().flush().unwrap(); + } +} \ No newline at end of file From cc43e00de3cb5d572ba63c8b2431450a7f365ad9 Mon Sep 17 00:00:00 2001 From: Cosma George Date: Wed, 3 May 2023 12:35:49 +0300 Subject: [PATCH 02/11] Cleaned up the code, added some comments and a whole lot of TODOs --- src/cli.rs | 2 +- src/main.rs | 2 +- src/serial_interface.rs | 34 +++++++++++++++++----------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index cd70f14..9d28b10 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use clap::{arg, crate_version, Command, value_parser}; +use clap::{arg, crate_version, value_parser, Command}; /// Create the [command](clap::Command) object which will handle all of the command line arguments. pub fn make_cli() -> Command { diff --git a/src/main.rs b/src/main.rs index a8c1061..a31dd9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ async fn main() -> tokio_serial::Result<()> { Some(("listen", sub_matches)) => { let port = sub_matches.get_one::("port").unwrap(); let baud_rate = *sub_matches.get_one::("baud-rate").unwrap(); - + println!("Got the listen subcommand"); let stream = open_port(port.to_string(), baud_rate)?; run_terminal(stream).await; diff --git a/src/serial_interface.rs b/src/serial_interface.rs index ab3a0b2..7be3177 100644 --- a/src/serial_interface.rs +++ b/src/serial_interface.rs @@ -1,13 +1,12 @@ use futures::stream::StreamExt; use std::io::Write; -use std::{env, io, str}; -use tokio_util::codec::{Decoder, Encoder}; +use std::{io, str}; +use tokio_util::codec::Decoder; use bytes::BytesMut; use tokio_serial::SerialPortBuilderExt; - -use tokio_serial::{SerialStream}; +use tokio_serial::SerialStream; struct LineCodec; @@ -16,37 +15,38 @@ impl Decoder for LineCodec { type Error = io::Error; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - // let newline = src.as_ref().iter().position(|b| *b == b'\n'); - // if let Some(n) = newline { - // let line = src.split_to(n + 1); - // return match str::from_utf8(line.as_ref()) { - // Ok(s) => Ok(Some(s.to_string())), - // Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Invalid String")), - // }; - // } - // Ok(None) if src.is_empty() { return Ok(None); } - let result = match str::from_utf8(&src) { + + // Read everything you can, and interpret it as a string. + // TODO: Note that this can fail if we try to decode in the middle of a multi-byte UTF-8 Character. + // We could wait for more output, or use this + let result = match str::from_utf8(src) { Ok(s) => Ok(Some(s.to_string())), Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Invalid String")), }; src.clear(); - return result; + result } } -pub fn open_port(path: String, baud_rate:u32) -> tokio_serial::Result { +pub fn open_port(path: String, baud_rate: u32) -> tokio_serial::Result { // Is it async? It can't be awaited... + // TODO: What if we don't know the port? We need to copy over the implemenntation from the python version tokio_serial::new(path, baud_rate).open_native_async() } pub async fn run_terminal(stream: SerialStream) { + // TODO: What if there is another instance of tockloader open? Check the python implementation + let mut reader = LineCodec.framed(stream); + // TODO: Spawn this into its own task, so that we may read and write at the same time. + // TODO: Can we hijack CTRL+C so that we can exit cleanly? while let Some(line_result) = reader.next().await { let line = line_result.expect("Failed to read line"); print!("{}", line); + // We need to flush the buffer because the "tock>" prompt does not have a newline. io::stdout().flush().unwrap(); } -} \ No newline at end of file +} From d1d6d56db896e4f47f47f867931ffbfa7d8b6ada Mon Sep 17 00:00:00 2001 From: Cosma George Date: Sun, 7 May 2023 21:58:42 +0300 Subject: [PATCH 03/11] Somewhat working read/write functions from serial --- Cargo.toml | 1 + src/serial_interface.rs | 63 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 70a7d74..b83642a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] bytes = "1.4.0" clap = { version = "4.1.1", features = ["cargo"] } +console = "0.15.5" futures = "0.3.28" tokio = { version ="1.28.0", features= ["full"] } tokio-serial = "5.4.4" diff --git a/src/serial_interface.rs b/src/serial_interface.rs index 7be3177..8b43012 100644 --- a/src/serial_interface.rs +++ b/src/serial_interface.rs @@ -1,14 +1,15 @@ -use futures::stream::StreamExt; +use futures::stream::{SplitSink, SplitStream, StreamExt}; +use futures::SinkExt; use std::io::Write; use std::{io, str}; -use tokio_util::codec::Decoder; +use tokio_util::codec::{Decoder, Encoder, Framed}; -use bytes::BytesMut; +use bytes::{BufMut, BytesMut}; +use console::Term; use tokio_serial::SerialPortBuilderExt; - use tokio_serial::SerialStream; -struct LineCodec; +pub struct LineCodec; impl Decoder for LineCodec { type Item = String; @@ -31,6 +32,15 @@ impl Decoder for LineCodec { } } +impl Encoder for LineCodec { + type Error = io::Error; + + fn encode(&mut self, _item: String, _dst: &mut BytesMut) -> Result<(), Self::Error> { + _dst.put(_item.as_bytes()); + Ok(()) + } +} + pub fn open_port(path: String, baud_rate: u32) -> tokio_serial::Result { // Is it async? It can't be awaited... // TODO: What if we don't know the port? We need to copy over the implemenntation from the python version @@ -38,9 +48,20 @@ pub fn open_port(path: String, baud_rate: u32) -> tokio_serial::Result>) { // TODO: What if there is another instance of tockloader open? Check the python implementation - let mut reader = LineCodec.framed(stream); // TODO: Spawn this into its own task, so that we may read and write at the same time. // TODO: Can we hijack CTRL+C so that we can exit cleanly? while let Some(line_result) = reader.next().await { @@ -50,3 +71,33 @@ pub async fn run_terminal(stream: SerialStream) { io::stdout().flush().unwrap(); } } + +pub async fn write_to_serial( + mut writer: SplitSink, std::string::String>, +) { + let term = Term::stdout(); + + loop { + let buf = match term.read_char() { + Ok(c) => c, + Err(e) => { + eprintln!("Read error: {e}"); + break; + } + }; + + if buf as u8 == 0x03 { + println!("Session aborted"); + break; + } + + // println!("Sending {} | {}", buf, buf as u8); + writer.send(buf.into()).await.expect("BBBBBB"); + } + // loop { + // let mut buffer = String::new(); + // let _ = io::stdin().read_line(&mut buffer); + // writer.send(buffer).await.expect("AAAaaaa"); + // writer.flush().await.expect("BBBBBBbb"); + // } +} From 62fe85dae3b1a1efa9764a392735be8ba98abf73 Mon Sep 17 00:00:00 2001 From: Cosma George Date: Fri, 12 May 2023 22:12:23 +0300 Subject: [PATCH 04/11] Every recieved '\n' is now paired up with an '\r' --- src/serial_interface.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/serial_interface.rs b/src/serial_interface.rs index 8b43012..f955997 100644 --- a/src/serial_interface.rs +++ b/src/serial_interface.rs @@ -24,7 +24,10 @@ impl Decoder for LineCodec { // TODO: Note that this can fail if we try to decode in the middle of a multi-byte UTF-8 Character. // We could wait for more output, or use this let result = match str::from_utf8(src) { - Ok(s) => Ok(Some(s.to_string())), + Ok(s) => { + let output = s.replace('\n', "\r\n"); + Ok(Some(output)) + } Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Invalid String")), }; src.clear(); @@ -90,7 +93,6 @@ pub async fn write_to_serial( println!("Session aborted"); break; } - // println!("Sending {} | {}", buf, buf as u8); writer.send(buf.into()).await.expect("BBBBBB"); } From ba6bc587d7ff7f7cca55c711d27f5b2eea6df7bb Mon Sep 17 00:00:00 2001 From: Cosma George Date: Wed, 17 May 2023 22:35:33 +0300 Subject: [PATCH 05/11] Added port auto-detection and switched to reading keys for transmitting characters. --- src/cli.rs | 2 +- src/main.rs | 8 +++-- src/serial_interface.rs | 66 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 9d28b10..a7668ec 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -20,7 +20,7 @@ fn get_subcommands() -> Vec { .about("Open a terminal to receive UART data") .args(get_app_args()) .args(get_channel_args()) - .arg_required_else_help(true)] + .arg_required_else_help(false)] } /// Generate all of the [arguments](clap::Arg) that are required by subcommands which work with apps. diff --git a/src/main.rs b/src/main.rs index a31dd9c..e5f18f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod cli; mod serial_interface; use cli::make_cli; +use serial_interface::open_first_available_port; use crate::serial_interface::{open_port, run_terminal}; @@ -14,11 +15,12 @@ async fn main() -> tokio_serial::Result<()> { match matches.subcommand() { Some(("listen", sub_matches)) => { - let port = sub_matches.get_one::("port").unwrap(); let baud_rate = *sub_matches.get_one::("baud-rate").unwrap(); + let stream = match sub_matches.get_one::("port") { + Some(port) => open_port(port.to_string(), baud_rate)?, + None => open_first_available_port(baud_rate)?, + }; - println!("Got the listen subcommand"); - let stream = open_port(port.to_string(), baud_rate)?; run_terminal(stream).await; } // If only the "--debug" flag is set, then this branch is executed diff --git a/src/serial_interface.rs b/src/serial_interface.rs index f955997..894c465 100644 --- a/src/serial_interface.rs +++ b/src/serial_interface.rs @@ -44,6 +44,35 @@ impl Encoder for LineCodec { } } +pub fn open_first_available_port(baud_rate: u32) -> tokio_serial::Result { + let ports = tokio_serial::available_ports()?; + + for p in ports { + // For whatever reason, the returend ports are listed under the "/sys/class/tty/" directory. + // While it does identify the right port, the result doesn't actually point to the device. + // As such, we'll try to use the name of the port with the "/dev/" path + + let port_path: String = if p.port_name.contains("tty") { + match p.port_name.split('/').last() { + Some(port_name) => format!("/dev/{}", port_name), + None => p.port_name, + } + } else { + p.port_name + }; + + match open_port(port_path.clone(), baud_rate) { + Ok(stream) => return Ok(stream), + Err(_) => println!("Failed to open port {}.", port_path), + } + } + + Err(tokio_serial::Error::new( + tokio_serial::ErrorKind::Io(io::ErrorKind::NotConnected), + "Couldn't open any of the available ports.", + )) +} + pub fn open_port(path: String, baud_rate: u32) -> tokio_serial::Result { // Is it async? It can't be awaited... // TODO: What if we don't know the port? We need to copy over the implemenntation from the python version @@ -81,7 +110,7 @@ pub async fn write_to_serial( let term = Term::stdout(); loop { - let buf = match term.read_char() { + let key = match term.read_key() { Ok(c) => c, Err(e) => { eprintln!("Read error: {e}"); @@ -89,12 +118,37 @@ pub async fn write_to_serial( } }; - if buf as u8 == 0x03 { - println!("Session aborted"); - break; + let buf: Option = match key { + console::Key::Unknown => todo!(), + console::Key::UnknownEscSeq(_) => todo!(), + console::Key::ArrowLeft => Some("\u{1B}[D".into()), + console::Key::ArrowRight => Some("\u{1B}[C".into()), + console::Key::ArrowUp => Some("\u{1B}[A".into()), + console::Key::ArrowDown => Some("\u{1B}[B".into()), + console::Key::Enter => Some("\n".into()), + console::Key::Escape => None, + console::Key::Backspace => Some("\x08".into()), + console::Key::Home => Some("\u{1B}[H".into()), + console::Key::End => Some("\u{1B}[F".into()), + console::Key::Tab => Some("\t".into()), + console::Key::BackTab => Some("\t".into()), + console::Key::Alt => None, + console::Key::Del => Some("\x7f".into()), + console::Key::Shift => None, + console::Key::Insert => None, + console::Key::PageUp => None, + console::Key::PageDown => None, + console::Key::Char(c) => Some(c.into()), + _ => todo!(), + }; + + if let Some(c) = buf { + println!("Sending {}", c); + writer + .send(c.into()) + .await + .expect("Could not send message."); } - // println!("Sending {} | {}", buf, buf as u8); - writer.send(buf.into()).await.expect("BBBBBB"); } // loop { // let mut buffer = String::new(); From b156c63afd9ab870fe514adee0ec463531433b31 Mon Sep 17 00:00:00 2001 From: Cosma George Date: Wed, 24 May 2023 23:15:35 +0300 Subject: [PATCH 06/11] Put console read_key() under a blocking task --- src/serial_interface.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/serial_interface.rs b/src/serial_interface.rs index 894c465..3149f4e 100644 --- a/src/serial_interface.rs +++ b/src/serial_interface.rs @@ -107,15 +107,19 @@ pub async fn read_from_serial(mut reader: SplitStream, std::string::String>, ) { - let term = Term::stdout(); - loop { - let key = match term.read_key() { - Ok(c) => c, - Err(e) => { - eprintln!("Read error: {e}"); - break; + let console_input = tokio::task::spawn_blocking( move || { + match Term::stdout().read_key() { + Ok(c) => Some(c), + Err(e) => { + eprintln!("Read error: {e}"); + None } + }}).await.unwrap(); + + let key = match console_input { + Some(c) => c, + None => break, }; let buf: Option = match key { @@ -143,12 +147,14 @@ pub async fn write_to_serial( }; if let Some(c) = buf { - println!("Sending {}", c); + // println!("Sending {}", c); writer .send(c.into()) .await .expect("Could not send message."); } + + // tokio::time::sleep(std::time::Duration::from_millis(1)).await; } // loop { // let mut buffer = String::new(); From 100dfb99c6385142cc61be41633b9be2f690e184 Mon Sep 17 00:00:00 2001 From: Cosma George Date: Thu, 25 May 2023 00:51:29 +0300 Subject: [PATCH 07/11] Cleaned up code and made listen wait for any task to finish --- src/serial_interface.rs | 95 ++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/src/serial_interface.rs b/src/serial_interface.rs index 3149f4e..a7aabab 100644 --- a/src/serial_interface.rs +++ b/src/serial_interface.rs @@ -1,7 +1,9 @@ use futures::stream::{SplitSink, SplitStream, StreamExt}; use futures::SinkExt; +use std::error::Error; use std::io::Write; use std::{io, str}; + use tokio_util::codec::{Decoder, Encoder, Framed}; use bytes::{BufMut, BytesMut}; @@ -81,50 +83,66 @@ pub fn open_port(path: String, baud_rate: u32) -> tokio_serial::Result { + // The write handle cannot be aborrted because of the blocking task spawned in it. + // As such, I think we are pretty much safe to forcefully exit at this point. + match result { + Ok(_) => std::process::exit(0), + Err(_) => std::process::exit(1), + } + } + _ = write_handle => {} + } } -pub async fn read_from_serial(mut reader: SplitStream>) { +pub async fn read_from_serial( + mut reader: SplitStream>, +) -> Result<(), Box> { // TODO: What if there is another instance of tockloader open? Check the python implementation - // TODO: Spawn this into its own task, so that we may read and write at the same time. - // TODO: Can we hijack CTRL+C so that we can exit cleanly? while let Some(line_result) = reader.next().await { - let line = line_result.expect("Failed to read line"); + let line = match line_result { + Ok(it) => it, + Err(err) => { + eprint!("Failed to read string. Error: {:?}", err); + return Err(Box::new(err)); + } + }; print!("{}", line); + // We need to flush the buffer because the "tock>" prompt does not have a newline. io::stdout().flush().unwrap(); } + + Ok(()) } pub async fn write_to_serial( mut writer: SplitSink, std::string::String>, -) { +) -> Result<(), Box> { loop { - let console_input = tokio::task::spawn_blocking( move || { - match Term::stdout().read_key() { - Ok(c) => Some(c), - Err(e) => { - eprintln!("Read error: {e}"); - None - } - }}).await.unwrap(); - - let key = match console_input { - Some(c) => c, - None => break, - }; + let console_input = tokio::task::spawn_blocking(move || Term::stdout().read_key()).await?; - let buf: Option = match key { - console::Key::Unknown => todo!(), - console::Key::UnknownEscSeq(_) => todo!(), + let key = console_input?; + + let send_buffer: Option = match key { + console::Key::Unknown => None, + console::Key::UnknownEscSeq(_) => None, console::Key::ArrowLeft => Some("\u{1B}[D".into()), console::Key::ArrowRight => Some("\u{1B}[C".into()), console::Key::ArrowUp => Some("\u{1B}[A".into()), @@ -146,20 +164,17 @@ pub async fn write_to_serial( _ => todo!(), }; - if let Some(c) = buf { - // println!("Sending {}", c); - writer - .send(c.into()) - .await - .expect("Could not send message."); + if let Some(buffer) = send_buffer { + if let Err(err) = writer.send(buffer.clone()).await { + eprintln!( + "Error writing to serial. Buffer {}. Error: {:?} ", + buffer, err + ); + return Err(Box::new(err)); + } } - - // tokio::time::sleep(std::time::Duration::from_millis(1)).await; } - // loop { - // let mut buffer = String::new(); - // let _ = io::stdin().read_line(&mut buffer); - // writer.send(buffer).await.expect("AAAaaaa"); - // writer.flush().await.expect("BBBBBBbb"); - // } + + // TODO: handle CTRL+C + // Ok(()) } From 53c7786874012c8f9d3830ef938f1950da54d40f Mon Sep 17 00:00:00 2001 From: Cosma George Date: Thu, 24 Aug 2023 11:31:54 +0300 Subject: [PATCH 08/11] Fixed string interpretation with broken UTF-8 characters Before, if an invalid UTF-8 sequence was decoded, the program would error out. Now, it will decode as much as it and can keep the remaining uninterpreted bytes in its buffer. --- src/serial_interface.rs | 44 ++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/serial_interface.rs b/src/serial_interface.rs index a7aabab..cb02aae 100644 --- a/src/serial_interface.rs +++ b/src/serial_interface.rs @@ -6,34 +6,54 @@ use std::{io, str}; use tokio_util::codec::{Decoder, Encoder, Framed}; -use bytes::{BufMut, BytesMut}; +use bytes::{BufMut, BytesMut, Buf}; use console::Term; use tokio_serial::SerialPortBuilderExt; use tokio_serial::SerialStream; pub struct LineCodec; +impl LineCodec { + fn clean_input(input: &str ) -> String { + input.replace('\n', "\r\n") + } +} + impl Decoder for LineCodec { type Item = String; type Error = io::Error; - fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - if src.is_empty() { + fn decode(&mut self, source: &mut BytesMut) -> Result, Self::Error> { + if source.is_empty() { return Ok(None); } // Read everything you can, and interpret it as a string. - // TODO: Note that this can fail if we try to decode in the middle of a multi-byte UTF-8 Character. - // We could wait for more output, or use this - let result = match str::from_utf8(src) { - Ok(s) => { - let output = s.replace('\n', "\r\n"); + match str::from_utf8(&source) { + Ok(utf8_string) => { + let output = LineCodec::clean_input(utf8_string); + source.clear(); Ok(Some(output)) } - Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Invalid String")), - }; - src.clear(); - result + Err(error) => { + let index = error.valid_up_to(); + + if index == 0 { + // Returning Some("") makes it so no other bytes are read in. I have no idea why. + // If you find a reason why, please edit this comment. + return Ok(None); + } + + match str::from_utf8(&source[..index]) { + Ok(utf8_string) => { + let output = LineCodec::clean_input(utf8_string); + source.advance(index); + Ok(Some(output)) + }, + Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, format!("Couldn't parse input as UTF8. Last valid index: {}. Buffer: {:?}",index,source))), + } + } + } } } From 89305277b571cfbedd8c60125e9615fc2e256c3b Mon Sep 17 00:00:00 2001 From: Cosma George Date: Thu, 24 Aug 2023 11:38:31 +0300 Subject: [PATCH 09/11] Ran cargo fmt Don't be like me, add the checks to a git hook --- src/serial_interface.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/serial_interface.rs b/src/serial_interface.rs index cb02aae..a975526 100644 --- a/src/serial_interface.rs +++ b/src/serial_interface.rs @@ -6,7 +6,7 @@ use std::{io, str}; use tokio_util::codec::{Decoder, Encoder, Framed}; -use bytes::{BufMut, BytesMut, Buf}; +use bytes::{Buf, BufMut, BytesMut}; use console::Term; use tokio_serial::SerialPortBuilderExt; use tokio_serial::SerialStream; @@ -14,7 +14,7 @@ use tokio_serial::SerialStream; pub struct LineCodec; impl LineCodec { - fn clean_input(input: &str ) -> String { + fn clean_input(input: &str) -> String { input.replace('\n', "\r\n") } } @@ -29,7 +29,7 @@ impl Decoder for LineCodec { } // Read everything you can, and interpret it as a string. - match str::from_utf8(&source) { + match str::from_utf8(source) { Ok(utf8_string) => { let output = LineCodec::clean_input(utf8_string); source.clear(); @@ -49,8 +49,14 @@ impl Decoder for LineCodec { let output = LineCodec::clean_input(utf8_string); source.advance(index); Ok(Some(output)) - }, - Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, format!("Couldn't parse input as UTF8. Last valid index: {}. Buffer: {:?}",index,source))), + } + Err(_) => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Couldn't parse input as UTF8. Last valid index: {}. Buffer: {:?}", + index, source + ), + )), } } } From 2c62ebf01e1a8ac88a4b5b1597e2e841194d41b9 Mon Sep 17 00:00:00 2001 From: Cosma George Date: Fri, 25 Aug 2023 15:00:54 +0300 Subject: [PATCH 10/11] Fixed backspace issue on new kernel versions --- src/serial_interface.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/serial_interface.rs b/src/serial_interface.rs index a975526..5dd2b3f 100644 --- a/src/serial_interface.rs +++ b/src/serial_interface.rs @@ -15,7 +15,25 @@ pub struct LineCodec; impl LineCodec { fn clean_input(input: &str) -> String { - input.replace('\n', "\r\n") + // Use consistent line endings for all OS versions. + // More so, in the newest version of the kernel a "backspace" is echoed out as + // + // + // + // The character only moves the cursor back, and does not delete. What the + // space does is overwrite the previous character with a seemingly empty one (space) and + // then moves the cursor back. + // + // In previous versiouns only these three characters were printed, but now also + // an null (or "End of file" byte) is also transmitted and promptly deleted. + // The issues appear when we can't actually delete null bytes, the actual result + // being two (normal) characters being deleted at once, sometimes overflowing and + // starting to (visually) delete the tock prompt ("tock$ ") that preceds all lines. + // + // Python's minterm dealt with this issue by converting the null byte into + // Unicode code point 0x2400. This is a specific "end of file" 3-byte long character + // which can be deleted. + input.replace('\n', "\r\n").replace('\x00', "\u{2400}") } } From 4aa47755b138461146182e7a1000eb367933cd4a Mon Sep 17 00:00:00 2001 From: Cosma George Date: Fri, 25 Aug 2023 20:04:37 +0300 Subject: [PATCH 11/11] Fixed delete key --- src/serial_interface.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/serial_interface.rs b/src/serial_interface.rs index 5dd2b3f..c61bf37 100644 --- a/src/serial_interface.rs +++ b/src/serial_interface.rs @@ -199,7 +199,9 @@ pub async fn write_to_serial( console::Key::Tab => Some("\t".into()), console::Key::BackTab => Some("\t".into()), console::Key::Alt => None, - console::Key::Del => Some("\x7f".into()), + // In latest version of kernel (2023.08.25), the "del" ascii code (\x7F) + // is handled exactly as backspace (\x08). Proper del is this: + console::Key::Del => Some("\u{1B}[3~".into()), console::Key::Shift => None, console::Key::Insert => None, console::Key::PageUp => None,