Initial commit

This commit is contained in:
2025-09-18 13:17:47 +02:00
commit 8f6b7fbc1f
9 changed files with 1063 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

184
Cargo.lock generated Normal file
View File

@@ -0,0 +1,184 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "argp"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7409aa6f1dd8464eac2e56cf538e1e5f7f79678caa32f198d214a3db8d5075c1"
dependencies = [
"argp_derive",
]
[[package]]
name = "argp_derive"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d9b949411282939e3f7d8923127e3f18aa474b46da4e8bb0ddf2cb8c81f963a"
dependencies = [
"proc-macro2",
"pulldown-cmark",
"quote",
"syn",
]
[[package]]
name = "bitflags"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "cfg-if"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "getopts"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
"unicode-width",
]
[[package]]
name = "libc"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "memchr"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "pbr"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5827dfa0d69b6c92493d6c38e633bbaa5937c153d0d7c28bf12313f8c6d514"
dependencies = [
"crossbeam-channel",
"libc",
"winapi",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pulldown-cmark"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
dependencies = [
"bitflags",
"getopts",
"memchr",
"unicase",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serial2"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349bb79c63bd2690fe4ca8ef7072a6d9ca36b3a1687a08393e733ea56d573911"
dependencies = [
"cfg-if",
"libc",
"winapi",
]
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tulflash"
version = "0.1.0"
dependencies = [
"argp",
"pbr",
"serial2",
]
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "unicode-width"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

9
Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "tulflash"
version = "0.1.0"
edition = "2024"
[dependencies]
argp = "0.4.0"
pbr = "1.1.1"
serial2 = "0.2.32"

87
src/args.rs Normal file
View File

@@ -0,0 +1,87 @@
use std::ffi::OsString;
use argp::FromArgs;
#[derive(FromArgs)]
#[argp(description = "tulflash - Flashing utility for the TUL Výukový Přípravek v1.1™")]
pub struct Args {
#[argp(option)]
#[argp(description = "Path to the serial port. Probably will be `/dev/ttyUSBx` on Linux, `/dev/tty.usbserial-xxx` on macOS or `COMx` on Windows.")]
pub port: OsString,
#[argp(switch)]
#[argp(description = "Try this if it does not work the first time.")]
pub slow: bool,
#[argp(subcommand)]
pub command: ArgCommand
}
#[derive(FromArgs)]
#[argp(subcommand)]
pub enum ArgCommand {
Read(ArgRead),
Write(ArgWrite),
Erase(ArgErase)
}
#[derive(FromArgs)]
#[argp(description = "Reads the Flash memory contents into an Intel HEX file.")]
#[argp(subcommand, name = "read")]
pub struct ArgRead {
#[argp(positional)]
#[argp(description = "Path to the destination Intel HEX file.")]
pub path: OsString,
#[argp(option, default = "0")]
#[argp(description = "Start address where to read from. Defaults to 0.")]
pub start: usize,
#[argp(option, default = "65535")]
#[argp(description = "End address where to read to. Defaults to 65535.")]
pub end: usize,
#[argp(switch)]
#[argp(description = "Output into a raw binary rather than an Intel HEX.")]
pub bin: bool
}
#[derive(FromArgs)]
#[argp(description = "Writes the contents of an Intel HEX file into the Flash memory.")]
#[argp(subcommand, name = "write")]
pub struct ArgWrite {
#[argp(positional)]
#[argp(description = "Path to the source Intel HEX file.")]
pub path: OsString,
#[argp(option, default = "0")]
#[argp(description = "Start address where to write from. Only for raw binaries, defaults to 0.")]
pub start: usize,
#[argp(option, default = "65535")]
#[argp(description = "End address where to write to. Only for raw binaries, defaults to 65535.")]
pub end: usize,
#[argp(switch)]
#[argp(description = "The input file is a binary rather than an Intel HEX.")]
pub bin: bool,
#[argp(switch)]
#[argp(description = "Skip verification of the written file. Not really recommended.")]
pub skip_verify: bool,
#[argp(switch)]
#[argp(description = "Write individual sections to Flash rather than performing a single write. Only applies to Intel HEX.")]
pub write_individual: bool
}
#[derive(FromArgs)]
#[argp(description = "Erases the flash memory. You will probably never need this, as the chip will do absolutely nothing when reset after erasing.")]
#[argp(subcommand, name = "erase")]
pub struct ArgErase {}
pub fn parse() -> Args {
let args: Args = argp::parse_args_or_exit(argp::DEFAULT);
args
}

139
src/ihex.rs Normal file
View File

@@ -0,0 +1,139 @@
use std::io::{BufRead, BufReader, BufWriter, Error, Lines, Read, Write};
use crate::utils::Hex;
pub struct IntelHexWriter<T: Write> {
writer: BufWriter<T>,
data: Vec<u8>,
addr: u16
}
impl<T: Write> IntelHexWriter<T> {
pub fn init(writer: T, addr: u16) -> Self {
Self {
writer: BufWriter::new(writer),
data: Vec::with_capacity(16),
addr
}
}
fn write_data(&mut self, val: u8) -> Result<(), Error> {
if self.data.len() >= self.data.capacity() {
self.flush_data_record()?;
}
self.data.push(val);
Ok(())
}
fn flush_data_record(&mut self) -> Result<(), Error> {
let mut data = [
&[self.data.len() as u8],
self.addr.to_be_bytes().as_slice(),
&[0x00],
self.data.as_slice()
].concat();
data.push(data.iter().fold(0, |acc: u8, val| acc.wrapping_add(*val)).wrapping_neg());
write!(self.writer, ":")?;
for val in data {
write!(self.writer, "{val:02X}")?;
}
writeln!(self.writer)?;
self.addr = self.addr.wrapping_add(self.data.len() as u16);
self.data.clear();
Ok(())
}
pub fn finish(mut self) -> Result<(), Error> {
self.flush_data_record()?;
writeln!(self.writer, ":00000001FF")?; // hardcoded end of file record
self.writer.flush()?;
Ok(())
}
}
impl<T: Write> Write for IntelHexWriter<T> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
for val in buf {
self.write_data(*val)?;
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
}
pub struct DataRecord {
pub addr: usize,
pub data: Vec<u8>
}
pub struct IntelHexReader<T: Read> {
reader: Lines<BufReader<T>>
}
impl<T: Read> IntelHexReader<T> {
pub fn init(reader: T) -> Self {
Self { reader: BufReader::new(reader).lines() }
}
}
impl<T: Read> Iterator for IntelHexReader<T> {
type Item = Result<DataRecord, Error>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let line = match self.reader.next()? {
Ok(line) => line,
Err(err) => return Some(Err(err))
};
if !line.starts_with(":") {
return Some(Err(Error::other("Intel HEX record not starting with a colon")))
}
let line = &line.as_bytes()[1..]; // can't do this with matlab, can you?
if line.len() % 2 != 0 {
return Some(Err(Error::other("Nibble count in Intel HEX record not even")))
}
let count = u8::hex_decode(line[..2].try_into().unwrap());
let addr = u16::hex_decode(line[2..6].try_into().unwrap()) as usize;
let rectype = u8::hex_decode(line[6..8].try_into().unwrap());
let expected_bytes = count as usize;
let received_bytes = line.len() / 2 - 5;
if expected_bytes != received_bytes { // Count (1), address (2), type (1), checksum (1)
return Some(Err(Error::other(format!("Byte count in Intel HEX not matching actual length (expected {expected_bytes}, got {received_bytes})"))))
}
if rectype != 0 {
continue
}
let data = (0..count as usize)
.map(|idx| {
let base = 8 + idx * 2;
u8::hex_decode(line[base..(base + 2)].try_into().unwrap())
})
.collect();
// fuck the checksum
break Some(Ok(DataRecord {
addr,
data
}))
}
}
}

225
src/main.rs Normal file
View File

@@ -0,0 +1,225 @@
use std::{io::{Cursor, Error, Write}, time::{Duration, SystemTime}};
use pbr::ProgressBar;
mod args;
use args::*;
mod ihex;
use ihex::{IntelHexWriter, IntelHexReader, DataRecord};
mod target;
use target::{PhysicalTarget, Target};
mod utils;
#[cfg(test)]
mod tests;
pub fn read_chip_into(writer: &mut impl Write, target: &mut impl Target, start: usize, end: usize) -> Result<(), Error> {
let mut pb = ProgressBar::new((end - start) as u64 + 1);
pb.message("reading chip flash ");
pb.set_units(pbr::Units::Bytes);
let mut last = start;
for i in (start..=end).step_by(1024).chain(std::iter::once(end + 1)) {
if i == last { continue }
let size = i - last;
let mut buf = [0u8; 1024];
target.read_flash(last as u16, &mut buf[..size])?;
writer.write_all(&buf[..size])?;
pb.add(size as u64);
last = i;
}
pb.finish_println("");
Ok(())
}
pub fn read_chip(args: ArgRead, target: &mut impl Target) -> Result<(), Error> {
let mut file = std::fs::File::create(args.path)?;
match args.bin {
true => read_chip_into(&mut file, target, args.start, args.end)?,
false => {
let mut writer = IntelHexWriter::init(file, args.start as u16);
read_chip_into(&mut writer, target, args.start, args.end)?;
writer.finish()?;
}
}
Ok(())
}
pub fn write_chip_chunk(data: &[u8], target: &mut impl Target, mut start: usize, verify: bool) -> Result<(), Error> {
let mut head = Vec::new();
if start & 0x7F != 0 {
head.resize(start & 0x7F, 0);
let mut pb = ProgressBar::new(head.len() as u64);
pb.message("reading chip flash ");
pb.set_units(pbr::Units::Bytes);
target.read_flash(start as u16 & !0x7F, &mut head)?;
pb.add(head.len() as u64);
pb.finish_println("");
}
let mut tail = Vec::new();
let end = start + data.len();
if end & 0x7F != 0 {
tail.resize(128 - (end & 0x7F), 0);
let mut pb = ProgressBar::new(tail.len() as u64);
pb.message("reading chip flash ");
pb.set_units(pbr::Units::Bytes);
target.read_flash(end as u16, &mut tail)?;
pb.add(tail.len() as u64);
pb.finish_println("");
}
let data = [ &head, data, &tail ].concat();
assert_eq!(data.len() & 0x7F, 0);
start = start & !0x7F;
if start + data.len() > 0x10000 {
Err(Error::other("Data does not fit into memory"))?
}
let mut page = start / 128;
let mut pb = ProgressBar::new(data.len() as u64);
pb.message("writing chip flash ");
pb.set_units(pbr::Units::Bytes);
for chunk in data.as_chunks::<128>().0.iter() {
target.write_flash(page as u16, chunk)?;
pb.add(chunk.len() as u64);
page += 1;
}
pb.finish_println("");
if verify {
let mut verify: Cursor<Vec<u8>> = Cursor::new(Vec::new());
read_chip_into(&mut verify, target, start, start + data.len() - 1)?;
if verify.get_ref() != &data {
Err(Error::other("Write mismatch"))?
}
}
Ok(())
}
pub fn write_chip(args: ArgWrite, target: &mut impl Target) -> Result<(), Error> {
if args.bin {
let mut file = std::fs::read(args.path)?;
let size = args.end - args.start + 1;
if size < file.len() {
file.resize(size, 0);
}
return write_chip_chunk(&file, target, args.start, !args.skip_verify)
}
let hexreader = IntelHexReader::init(std::fs::File::open(args.path)?);
if args.write_individual {
let mut last_addr = 0;
let mut next_expected_addr = 0;
let mut last_data = Vec::new();
for record in hexreader {
let DataRecord { addr, mut data } = record?;
if addr != next_expected_addr {
if !last_data.is_empty() {
write_chip_chunk(&last_data, target, last_addr, !args.skip_verify)?;
last_data.clear();
}
last_addr = addr;
next_expected_addr = addr;
}
next_expected_addr += data.len();
last_data.append(&mut data);
}
if !last_data.is_empty() {
write_chip_chunk(&last_data, target, last_addr, !args.skip_verify)?;
}
return Ok(())
}
let mut first_addr = 0xFFFF;
let mut last_addr = 0;
let mut buf = vec![0u8; 0x10000];
for record in hexreader {
let DataRecord { addr, data } = record?;
if addr < first_addr {
first_addr = addr;
}
if data.len() + addr - 1 > last_addr {
last_addr = addr + data.len() - 1;
}
buf[addr..addr + data.len()].copy_from_slice(&data);
}
write_chip_chunk(&buf[first_addr..=last_addr], target, first_addr, !args.skip_verify)
}
pub fn erase_chip(_args: ArgErase, target: &mut impl Target) -> Result<(), Error> {
target.erase()?;
Ok(())
}
fn main() {
let args = args::parse();
let start = SystemTime::now();
let mut target = PhysicalTarget::init(args.port, if args.slow { 57600 } else { 312500 }).unwrap();
let res = match args.command {
ArgCommand::Read(args) => read_chip(args, &mut target),
ArgCommand::Write(args) => write_chip(args, &mut target),
ArgCommand::Erase(args) => erase_chip(args, &mut target)
};
if let Err(err) = res {
println!("An error occured during the operation: {}", err);
}
let end = SystemTime::now();
let dur = end.duration_since(start).unwrap_or(Duration::ZERO);
println!("Finished operation in {} ms.", dur.as_millis());
}

234
src/target.rs Normal file
View File

@@ -0,0 +1,234 @@
use std::io::Error;
use std::path::Path;
use std::time::Duration;
use serial2::SerialPort;
use crate::utils::Hex;
pub trait Target {
fn read_flash(&mut self, offset: u16, out: &mut [u8]) -> Result<(), Error>;
fn write_flash(&mut self, page: u16, data: &[u8; 128]) -> Result<(), Error>;
fn erase(&mut self) -> Result<(), Error>;
}
pub struct PhysicalTarget {
port: SerialPort
}
impl PhysicalTarget {
pub fn init(path: impl AsRef<Path>, baud: u32) -> Result<Self, Error> {
let mut port = SerialPort::open(path, baud)?;
port.set_read_timeout(Duration::from_millis(100))?;
port.set_dtr(false)?;
std::thread::sleep(Duration::from_millis(100));
for i in (0..5).rev() {
port.write(b"U")?;
let mut buf = [0u8; 1];
port.read(&mut buf)?;
if buf[0] == b'U' { break }
if i == 0 {
Err(Error::other(format!("Invalid sync response: 0x{:02X}", buf[0])))?
}
}
let mut new = Self { port };
let manufacturer = new.read_info(0x00, 0x00).unwrap();
let family = new.read_info(0x00, 0x01).unwrap();
let name = new.read_info(0x00, 0x02).unwrap();
let rev = new.read_info(0x00, 0x03).unwrap();
println!("Detected chip ID: {manufacturer:02X} {family:02X} {name:02X} {rev:02X} (whatever the fuck that means)");
Ok(new)
}
pub fn read_info(&mut self, category: u8, idx: u8) -> Result<u8, Error> {
self.send_command(0x05, 0, &[category, idx])?;
let mut resp_hex_digits = Vec::new();
loop {
let mut tmp = [0u8; 1];
self.port.read_exact(&mut tmp)?;
match tmp[0] {
x @ (b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F') => resp_hex_digits.push(x),
b'.' | b'\r' => {},
b'\n' => break,
b'P' => Err(Error::other("Security error"))?,
b'X' => Err(Error::other("Checksum error"))?,
x => Err(Error::other(format!("Invalid byte in response: {x:02X}")))?
}
}
let mut rx = [0u8; 1];
let resp = resp_hex_digits.as_chunks::<2>().0.iter()
.map(u8::hex_decode);
if resp.len() != rx.len() {
Err(Error::other(format!("Response size mismatch, got {} bytes, expected {} bytes", resp.len(), rx.len())))?
}
rx.iter_mut()
.zip(resp)
.for_each(|(dest, src)| *dest = src);
Ok(u8::from_le_bytes(rx))
}
fn send_command(&mut self, cmd_type: u8, offset: u16, tx: &[u8]) -> Result<(), Error> {
if tx.len() >= 256 {
Err(Error::other(format!("Payload too large: {} bytes", tx.len())))?
}
// Why, the ACTUAL FUCK, ARE THEY USING ASCII FOR TRANSFERING BINARIES?!
let mut ascii_buf = Vec::with_capacity(1 + 2 + 4 + 2 + tx.len() * 2 + 1); // Mark, Length, Offset, Type, Data, Checksum
ascii_buf.push(b':');
ascii_buf.extend_from_slice(&(tx.len() as u8).hex_encode());
ascii_buf.extend_from_slice(&offset.hex_encode());
ascii_buf.extend_from_slice(&cmd_type.hex_encode());
let mut checksum: u8 = 0;
checksum = checksum.wrapping_add(cmd_type);
checksum = checksum.wrapping_add(tx.len() as u8);
checksum = checksum.wrapping_add(offset as u8);
checksum = checksum.wrapping_add((offset >> 8) as u8);
for val in tx {
ascii_buf.extend_from_slice(&val.hex_encode());
checksum = checksum.wrapping_add(*val);
}
ascii_buf.extend_from_slice(&checksum.wrapping_neg().hex_encode());
self.port.write_all(&ascii_buf)?;
let mut echo = vec![0u8; ascii_buf.len()];
self.port.read_exact(&mut echo)?;
if echo != ascii_buf {
Err(Error::other("Echo response mismatch"))?
}
Ok(())
}
}
impl Target for PhysicalTarget {
fn read_flash(&mut self, offset: u16, out: &mut [u8]) -> Result<(), Error> {
self.send_command(0x04, 0, &[
offset.to_be_bytes().as_slice(),
offset.saturating_add(out.len() as u16 - 1).to_be_bytes().as_slice(),
&[0] // Flash
].concat())?;
let mut resp_hex_digits = Vec::with_capacity(out.len() * 2);
let mut ignore = false;
let mut finish_line = false;
loop {
let mut tmp = [0u8; 1];
self.port.read_exact(&mut tmp)?;
match tmp[0] {
x @ (b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F') => {
if !ignore {
resp_hex_digits.push(x);
if resp_hex_digits.len() >= resp_hex_digits.capacity() {
ignore = true;
finish_line = true;
}
}
},
b'=' => {
ignore = false;
}
b'.' | b'\r' => {},
b'\n' => {
ignore = true;
if finish_line { break }
},
b'P' => Err(Error::other("Security error"))?,
b'X' => Err(Error::other("Checksum error"))?,
x => Err(Error::other(format!("Invalid byte in response: {x:02X}")))?
}
}
let resp = resp_hex_digits.as_chunks::<2>().0.iter()
.map(u8::hex_decode);
out.iter_mut()
.zip(resp)
.for_each(|(dest, src)| *dest = src);
Ok(())
}
fn write_flash(&mut self, page: u16, data: &[u8; 128]) -> Result<(), Error> {
self.send_command(0x00, page * 128, data)?;
loop {
let mut tmp = [0u8; 1];
self.port.read_exact(&mut tmp)?;
match tmp[0] {
b'.' | b'\r' => {},
b'\n' => break,
b'P' => Err(Error::other("Security error"))?,
b'X' => Err(Error::other("Checksum error"))?,
x => Err(Error::other(format!("Invalid byte in response: {x:02X}")))?
}
}
Ok(())
}
fn erase(&mut self) -> Result<(), Error> {
self.send_command(0x03, 0, &[0x07])?;
let old_timeout = self.port.get_read_timeout()?;
self.port.set_read_timeout(Duration::from_secs(15))?;
loop {
let mut tmp = [0u8; 1];
self.port.read_exact(&mut tmp)?;
match tmp[0] {
b'.' | b'\r' => {},
b'\n' => break,
b'P' => Err(Error::other("Security error"))?,
b'X' => Err(Error::other("Checksum error"))?,
x => Err(Error::other(format!("Invalid byte in response: {x:02X}")))?
}
}
self.port.set_read_timeout(old_timeout)?;
Ok(())
}
}
impl Drop for PhysicalTarget {
fn drop(&mut self) {
let _ = self.port.set_dtr(true);
}
}

128
src/tests.rs Normal file
View File

@@ -0,0 +1,128 @@
use crate::target::Target;
use crate::write_chip_chunk;
#[derive(Debug, PartialEq, Eq)]
enum TestTargetOperation {
Read { offset: u16, len: usize },
Write { page: u16 },
Erase
}
struct TestTarget {
ops: Vec<TestTargetOperation>
}
impl TestTarget {
pub fn init() -> Self {
Self { ops: Vec::new() }
}
pub fn get_ops(&mut self) -> Vec<TestTargetOperation> {
std::mem::take(&mut self.ops)
}
}
impl Target for TestTarget {
fn read_flash(&mut self, offset: u16, out: &mut [u8]) -> Result<(), std::io::Error> {
self.ops.push(TestTargetOperation::Read { offset, len: out.len() });
Ok(())
}
fn write_flash(&mut self, page: u16, _data: &[u8; 128]) -> Result<(), std::io::Error> {
self.ops.push(TestTargetOperation::Write { page });
Ok(())
}
fn erase(&mut self) -> Result<(), std::io::Error> {
self.ops.push(TestTargetOperation::Erase);
Ok(())
}
}
#[test]
fn aligned_chunk_write() {
let mut target = TestTarget::init();
for page in 0..=511 {
write_chip_chunk(&[0u8; 128], &mut target, page * 128, false).unwrap();
assert_eq!(target.get_ops().as_slice(), [
TestTargetOperation::Write { page: page as u16 }
]);
}
assert!(write_chip_chunk(&[0u8; 128], &mut target, 65536, false).is_err());
}
#[test]
fn aligned_multi_chunk_write() {
let mut target = TestTarget::init();
for page in 0..=508 {
write_chip_chunk(&[0u8; 512], &mut target, page * 128, false).unwrap();
assert_eq!(target.get_ops().as_slice(), [
TestTargetOperation::Write { page: page as u16 },
TestTargetOperation::Write { page: page as u16 + 1 },
TestTargetOperation::Write { page: page as u16 + 2 },
TestTargetOperation::Write { page: page as u16 + 3 }
]);
}
}
#[test]
fn unaligned_chunk_write() {
let mut target = TestTarget::init();
for page in (0..=510).step_by(10) {
for start in 1..=127 {
write_chip_chunk(&[0u8; 128], &mut target, page * 128 + start, false).unwrap();
assert_eq!(target.get_ops().as_slice(), [
TestTargetOperation::Read { offset: page as u16 * 128, len: start },
TestTargetOperation::Read { offset: page as u16 * 128 + start as u16 + 128, len: 128 - start },
TestTargetOperation::Write { page: page as u16 },
TestTargetOperation::Write { page: page as u16 + 1 }
]);
}
}
}
#[test]
fn aligned_missized_chunk_write() {
let mut target = TestTarget::init();
for page in (0..=510).step_by(10) {
for len in 1..=127 {
write_chip_chunk(&vec![0u8; len], &mut target, page * 128, false).unwrap();
assert_eq!(target.get_ops().as_slice(), [
TestTargetOperation::Read { offset: page as u16 * 128 + len as u16, len: 128 - len },
TestTargetOperation::Write { page: page as u16 }
]);
}
}
}
#[test]
fn unaligned_multi_chunk_write() {
let mut target = TestTarget::init();
for page in (0..=507).step_by(10) {
for start in 1..=127 {
write_chip_chunk(&[0u8; 512], &mut target, page * 128 + start, false).unwrap();
assert_eq!(target.get_ops().as_slice(), [
TestTargetOperation::Read { offset: page as u16 * 128, len: start },
TestTargetOperation::Read { offset: (page + 3) as u16 * 128 + start as u16 + 128, len: 128 - start },
TestTargetOperation::Write { page: page as u16 },
TestTargetOperation::Write { page: page as u16 + 1 },
TestTargetOperation::Write { page: page as u16 + 2 },
TestTargetOperation::Write { page: page as u16 + 3 },
TestTargetOperation::Write { page: page as u16 + 4 }
]);
}
}
}

56
src/utils.rs Normal file
View File

@@ -0,0 +1,56 @@
fn hex_digit(val: usize) -> u8 {
// Disgusting? Yes.
// Could I be fucked to fix it? No.
b"0123456789ABCDEF"[val & 0xF]
}
fn from_hex_digit(val: u8) -> usize {
(match val {
val @ b'0'..=b'9' => val - b'0',
val @ b'a'..=b'f' => val - b'a' + 10,
val @ b'A'..=b'F' => val - b'A' + 10,
_ => unreachable!()
}) as usize
}
pub trait Hex<const N: usize> {
fn hex_encode(&self) -> [u8; N];
fn hex_decode(buf: &[u8; N]) -> Self where Self: Sized;
}
impl Hex<2> for u8 {
fn hex_encode(&self) -> [u8; 2] {
[
hex_digit(*self as usize >> 4),
hex_digit(*self as usize)
]
}
fn hex_decode(buf: &[u8; 2]) -> Self {
(
from_hex_digit(buf[0]) << 4 |
from_hex_digit(buf[1])
) as u8
}
}
impl Hex<4> for u16 {
fn hex_encode(&self) -> [u8; 4] {
[
hex_digit(*self as usize >> 12),
hex_digit(*self as usize >> 8),
hex_digit(*self as usize >> 4),
hex_digit(*self as usize)
]
}
fn hex_decode(buf: &[u8; 4]) -> Self {
(
from_hex_digit(buf[0]) << 12 |
from_hex_digit(buf[1]) << 8 |
from_hex_digit(buf[2]) << 4 |
from_hex_digit(buf[3])
) as u16
}
}