1use std::{
2 fs::File,
3 io::*,
4 os::unix::fs::MetadataExt,
5 path::PathBuf,
6 time::{Duration, Instant},
7};
8
9mod crc;
10mod ymodem;
11
12pub struct UbootShell {
13 pub tx: Option<Box<dyn Write + Send>>,
14 pub rx: Option<Box<dyn Read + Send>>,
15}
16
17impl UbootShell {
18 pub fn new(tx: impl Write + Send + 'static, rx: impl Read + Send + 'static) -> Result<Self> {
20 let mut s = Self {
21 tx: Some(Box::new(tx)),
22 rx: Some(Box::new(rx)),
23 };
24 s.wait_for_shell()?;
25 Ok(s)
26 }
27
28 fn rx(&mut self) -> &mut Box<dyn Read + Send> {
29 self.rx.as_mut().unwrap()
30 }
31
32 fn tx(&mut self) -> &mut Box<dyn Write + Send> {
33 self.tx.as_mut().unwrap()
34 }
35
36 fn wait_for_shell(&mut self) -> Result<()> {
37 let mut buf = [0u8; 1];
38 let mut history: Vec<u8> = Vec::new();
39 const CTRL_C: u8 = 0x03;
40
41 let mut last = Instant::now();
42
43 loop {
44 match self.rx().read(&mut buf) {
45 Ok(n) => {
46 if n == 1 {
47 let ch = buf[0];
48 if ch == b'\n' && history.last() != Some(&b'\r') {
49 stdout().write_all(b"\r").unwrap();
50 history.push(b'\r');
51 }
52 history.push(ch);
53
54 stdout().write_all(&buf).unwrap();
55
56 if history.ends_with(c"<INTERRUPT>".to_bytes()) {
57 return Ok(());
58 }
59
60 if last.elapsed() > Duration::from_millis(20) {
61 let _ = self.tx().write_all(&[CTRL_C]);
62 last = Instant::now();
63 }
64 }
65 }
66
67 Err(ref e) if e.kind() == ErrorKind::TimedOut => {
68 continue;
69 }
70 Err(e) => {
71 return Err(e);
72 }
73 }
74 }
75 }
76
77 fn read_line(&mut self) -> Result<String> {
78 let mut line_raw = Vec::new();
79 let mut byte = [0; 1];
80
81 loop {
82 let n = self.rx().read(&mut byte)?;
83 if n == 0 {
84 break;
85 }
86
87 stdout().write_all(&byte).unwrap();
88
89 if byte[0] == b'\r' {
90 continue;
91 }
92
93 if byte[0] == b'\n' {
94 break;
95 }
96
97 line_raw.push(byte[0]);
98 }
99
100 if line_raw.is_empty() {
101 return Ok(String::new());
102 }
103
104 let line = String::from_utf8_lossy(&line_raw);
105 Ok(line.trim().to_string())
106 }
107
108 pub fn wait_for_reply(&mut self, val: &str) -> Result<String> {
109 let mut reply = Vec::new();
110 let mut buff = [0u8; 1];
111 loop {
112 self.rx().read_exact(&mut buff)?;
113 reply.push(buff[0]);
114 let _ = stdout().write_all(&buff);
115
116 if reply.ends_with(val.as_bytes()) {
117 break;
118 }
119 }
120 Ok(String::from_utf8_lossy(&reply).trim().to_string())
121 }
122
123 pub fn cmd_without_reply(&mut self, cmd: &str, want_echo: bool) -> Result<String> {
124 self.tx().write_all(cmd.as_bytes())?;
125 self.tx().write_all("\r\n".as_bytes())?;
126 self.tx().flush()?;
127
128 if !want_echo {
129 return Ok(String::new());
130 }
131
132 let shell_start;
133 loop {
134 let line = self.read_line()?;
135 if line.contains(cmd) {
136 shell_start = line.trim().trim_end_matches(cmd).trim().to_string();
137 break;
138 }
139 }
140 Ok(shell_start)
141 }
142
143 pub fn cmd(&mut self, cmd: &str) -> Result<String> {
144 let shell_start = self.cmd_without_reply(cmd, true)?;
145
146 Ok(self
147 .wait_for_reply(&shell_start)?
148 .trim_end_matches(&shell_start)
149 .to_string())
150 }
151
152 pub fn set_env(&mut self, name: impl Into<String>, value: impl Into<String>) -> Result<()> {
153 self.cmd(&format!("setenv {} {}", name.into(), value.into()))?;
154 Ok(())
155 }
156
157 pub fn env(&mut self, name: impl Into<String>) -> Result<String> {
158 let name = name.into();
159 let s = self.cmd(&format!("echo ${}", name))?;
160 if s.is_empty() {
161 return Err(Error::new(
162 ErrorKind::NotFound,
163 format!("env {} not found", name),
164 ));
165 }
166 Ok(s)
167 }
168
169 pub fn env_int(&mut self, name: impl Into<String>) -> Result<usize> {
170 let name = name.into();
171 let line = self.env(&name)?;
172 parse_int(&line).ok_or(Error::new(
173 ErrorKind::InvalidData,
174 format!("env {name} is not a number"),
175 ))
176 }
177
178 pub fn loady(
179 &mut self,
180 addr: usize,
181 file: impl Into<PathBuf>,
182 on_progress: impl Fn(usize, usize),
183 ) -> Result<String> {
184 let shell_start = self.cmd_without_reply(&format!("loady {:#x}", addr,), true)?;
185
186 let mut p = ymodem::Ymodem::new();
187
188 let file = file.into();
189 let name = file.file_name().unwrap().to_str().unwrap();
190
191 let mut file = File::open(&file).unwrap();
192
193 let size = file.metadata().unwrap().size() as usize;
194
195 p.send(self, &mut file, name, size, |p| {
196 on_progress(p, size);
197 })?;
198
199 Ok(self
200 .wait_for_reply(&shell_start)?
201 .trim_end_matches(&shell_start)
202 .to_string())
203 }
204}
205
206impl Read for UbootShell {
207 fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
208 self.rx().read(buf)
209 }
210}
211
212impl Write for UbootShell {
213 fn write(&mut self, buf: &[u8]) -> Result<usize> {
214 self.tx().write(buf)
215 }
216
217 fn flush(&mut self) -> Result<()> {
218 self.tx().flush()
219 }
220}
221
222fn parse_int(line: &str) -> Option<usize> {
223 let mut line = line.trim();
224 let mut radix = 10;
225 if line.starts_with("0x") {
226 line = &line[2..];
227 radix = 16;
228 }
229 u64::from_str_radix(line, radix).ok().map(|o| o as _)
230}