Skip to content

Commit

Permalink
No -f command (moved to subcommands), updated README.md.
Browse files Browse the repository at this point in the history
  • Loading branch information
liamhays committed Sep 25, 2022
1 parent aaf9ef8 commit baa48e6
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 61 deletions.
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ because:
- It is cross-platform, currently tested on Windows and Linux
- It is programmed in Rust (compiles to an executable) and requires no
external dependencies, except for one small package on Linux
- Alum uses Rust implementations of XModem and Kermit
- Alum uses Rust implementations of XModem and Kermit, without
deferring to external programs

I am retiring my previous software HPex, a GUI tool that accomplished
a similar task. HPex was written in Python 3 and wxPython, which meant
Expand Down Expand Up @@ -60,6 +61,7 @@ Alum uses a "subcommand" structure. The commands are:
- `xsend`: send file with XModem
- `xget`: get file with XModem
- `ksend`: send file with Kermit
- `kget`: get file with Kermit
- `info`: calculate file size and HP checksum on file

Each subcommand takes a file argument and optionally flags. Alum
Expand Down Expand Up @@ -88,8 +90,20 @@ In this example, Alum found the one physical serial port on the system
and used it automatically.

Sometimes, after a file has been transferred to the calculator, the
rightmost "data transfer" annunciator will stay on. I don't know why
this is and it appears to be harmless.
rightmost "data transfer" annunciator will stay on while the XModem
server is still running. I don't know why this happens, and it appears
to be harmless.

## Kermit transfers
The `ksend` command sends files to a Kermit server or `RECV` command
on the calculator. To use `RECV`, run the calculator command
**before** you run Alum.

The `kget` command receives files from either a `SEND` command or the
`ARCHIVE` command, if it given an `:IO:<name>` argument. `kget` does
**not** talk to a Kermit server, to maintain compatibility with
`ARCHIVE`. To use `kget`, run Alum and then start the transfer on the
calculator.

## Extra transfer features
To finish or close any server after a transfer, pass the `-f` flag to
Expand All @@ -108,10 +122,12 @@ Alum also does not currently support receiving files over Kermit. I'm
working on it.

## Future features
- [ ] Kermit receive
- [ ] XModem server and Kermit server file listing
- [ ] 1K CRC direct XModem
sorted by urgency:

- [ ] HP 49 object info
- [ ] 1K CRC direct XModem
- [ ] XModem server and Kermit server file listing (maybe)


## XModem caveat
XModem is an old standard, and is so simple as to be
Expand All @@ -121,7 +137,7 @@ received file. However, some files have necessary `0x00` bytes at
their end, and sending these files via XModem causes the object to
become corrupted. For example, one file that suffers from this is the
tool [`FIXIT`](https://www.hpcalc.org/details/2416), by Joe Horn and
Mika Heiskanen. **Conn4x suffers from this same issue, including with
Mika Heiskanen. **Conn4x suffers from this issue as well, including with
this particular file. It is a limitation of the XModem protocol. If
you have sensitive files, or cannot get checksums to match, send them
via Kermit.**
Expand Down
62 changes: 47 additions & 15 deletions src/kermit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use std::fs::File;
use std::io::Write;

use serialport;
use console::style;
use indicatif::ProgressBar;

const SOH: u8 = 0x01;
Expand Down Expand Up @@ -68,6 +69,7 @@ fn tochar(c: u8) -> u8 {
c + 32
}

// TODO: this panics if it is called on an invalid value
fn unchar(c: u8) -> u8 {
c - 32
}
Expand Down Expand Up @@ -158,14 +160,14 @@ fn make_generic_packet(seq: &mut u32, ptype: char) -> Vec<u8> {
// do with how we read the packet (3 bytes then rest of packet).
fn read_packet(port: &mut Box<dyn serialport::SerialPort>) -> Result<KermitPacket, String> {
// have to sleep, probably because the calculator is slow
std::thread::sleep(std::time::Duration::from_millis(400));
std::thread::sleep(std::time::Duration::from_millis(300));
// it seems we have to read 3 bytes, then the rest of the packet
let mut header: [u8; 3] = [0; 3];
match port.read(header.as_mut_slice()) {
Ok(_) => {},
Err(e) => return Err("failed to read header of packet: ".to_owned() + &e.to_string()),
}
println!("header is {:x?}", header);
//println!("header is {:x?}", header);
if header[0] != SOH {
return Err("malformed Kermit packet (SOH missing)".to_owned());
}
Expand All @@ -175,12 +177,14 @@ fn read_packet(port: &mut Box<dyn serialport::SerialPort>) -> Result<KermitPacke
// this would be len - 1, but we want to also read the CR at the end of the packet.
let mut rest_of_packet = vec![0 as u8; len as usize];

std::thread::sleep(std::time::Duration::from_millis(200));
// could probably reduce this delay slightly
// this also seems to be needed only for getting files from the calc
std::thread::sleep(std::time::Duration::from_millis(50));
match port.read(rest_of_packet.as_mut_slice()) {
Ok(_) => {},
Err(e) => return Err("failed to read packet data: ".to_owned() + &e.to_string()),
}
println!("rest of packet is {:x?}", rest_of_packet);
//println!("rest of packet is {:x?}", rest_of_packet);
// subtract 2 to drop 0x0d and check field, to isolate just data
// portion and assemble KermitPacket struct.
let data_field = rest_of_packet[1..(len as usize - 2)].to_vec();
Expand All @@ -201,7 +205,7 @@ fn read_packet(port: &mut Box<dyn serialport::SerialPort>) -> Result<KermitPacke
return Err("Error: checksum of received data does not match checksum in packet".to_owned());
}

println!("packet is {:x?}", packet);
//println!("packet is {:x?}", packet);

return Ok(packet);
}
Expand Down Expand Up @@ -316,10 +320,12 @@ fn finish_server(port: &mut Box<dyn serialport::SerialPort>) {
// TODO: this is pretty unreliable and doesn't work with x48 at full
// speed. It has to do with the read_packet() function.

// TODO: (more important) need to handle special characters in the filename

// See the top of this file for what this function actually
// does. There are a lot of match statements, but it's how I catch
// serial port and protocol errors.
pub fn send_file(path: &PathBuf, port: &mut Box<dyn serialport::SerialPort>, finish: bool) {
pub fn send_file(path: &PathBuf, port: &mut Box<dyn serialport::SerialPort>, finish: &bool) {
let mut seq = 0u32;

let file_contents = crate::helpers::get_file_contents(path);
Expand Down Expand Up @@ -386,18 +392,28 @@ pub fn send_file(path: &PathBuf, port: &mut Box<dyn serialport::SerialPort>, fin
}
bar.finish();

if finish {
if *finish {
finish_server(port);
}
}


// TODO: indeterminate progress bar or something similar.
pub fn get_file(path: &PathBuf, port: &mut Box<dyn serialport::SerialPort>, overwrite: &bool) -> PathBuf {
let final_path = match overwrite {
true => path.to_path_buf(),
false => crate::helpers::get_unique_path(path.to_path_buf()),
};
let final_fname = final_path.file_name().unwrap().to_str().unwrap();

let pb = crate::helpers::get_spinner(
format!("Receiving file as {} from {}...",
style(final_fname).yellow().bright(),
style(port.name().unwrap()).green().bright()));

pub fn get_file(path: &PathBuf, port: &mut Box<dyn serialport::SerialPort>) {
println!("kermit::get_file");

let mut seq = 0;
let mut out = File::create(path).unwrap();
let mut out = File::create(&final_path).unwrap();

// read S packet, which initializes connection from the calculator
match read_packet(port) {
Expand Down Expand Up @@ -438,6 +454,8 @@ pub fn get_file(path: &PathBuf, port: &mut Box<dyn serialport::SerialPort>) {
}

let mut file_bytes: Vec<u8> = Vec::new();
let mut packet_counter = 0;

loop {
let packet: KermitPacket = match read_packet(port) {
Ok(packet) => {
Expand All @@ -446,10 +464,10 @@ pub fn get_file(path: &PathBuf, port: &mut Box<dyn serialport::SerialPort>) {
packet
} else if packet.ptype == 'Z' as u8 {
// Z (end-of-file) is sent by the calc
println!("got Z packet");
break;
} else {
crate::helpers::error_handler(format!("Error: unexpected packet type when waiting for \"D\" packet."));
crate::helpers::error_handler(
format!("Error: unexpected packet type when waiting for \"D\" packet."));
KermitPacket {data: Vec::new(), len: 0, ptype: 0u8, seq: 0}
}
},
Expand Down Expand Up @@ -483,10 +501,10 @@ pub fn get_file(path: &PathBuf, port: &mut Box<dyn serialport::SerialPort>) {
Err(e) => crate::helpers::error_handler(
format!("Error: failed to write \"Y\" packet for \"D\" packet: {}", e)),
}
packet_counter += 1;
}

// probably need some sleep here or something

std::thread::sleep(std::time::Duration::from_millis(300));
// read Z (EOF) packet from calculator
match read_packet(port) {
Ok(packet) => {
Expand Down Expand Up @@ -528,5 +546,19 @@ pub fn get_file(path: &PathBuf, port: &mut Box<dyn serialport::SerialPort>) {
Ok(_) => {},
Err(e) => panic!("Error: failed to write to output file: {:?}", e),
};


pb.finish_with_message(
format!("Receiving file as {:?} from {}...{} Got {:?} {}.",
style(final_fname).yellow().bright(),
style(port.name().unwrap()).green().bright(),
style("done!").green().bright(),
packet_counter,
match packet_counter {
1 => "packet",
_ => "packets",
}
)
);

return final_path;
}
66 changes: 33 additions & 33 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ struct Cli {
#[clap(value_parser = clap::value_parser!(u32).range(1..))]
baud: Option<u32>,

/// Finish remote server after file transfer
#[clap(short, long, action, default_value_t = false)]
finish: bool,

}


Expand All @@ -43,27 +39,31 @@ struct Cli {
#[derive(Subcommand)]
#[derive(Debug)]
enum Commands {
/// Send file to Kermit server
/// Send file to Kermit server or RECV command
Ksend {
#[clap(parse(from_os_str))]
path: std::path::PathBuf,
},

// Ah! Because Subcommands are each extendable, we can add an
// option to communicate with a server to this.

// The amount of future-proofing here is insane.
/// Send file to XModem server
/// Finish Kermit server after file transfer
#[clap(short, long, action, default_value_t = false)]
finish: bool,
},

/// Send file with XModem
Xsend {
#[clap(parse(from_os_str))]
path: std::path::PathBuf,

/// Send to direct XRECV, not server (bypasses server operations and uses 128-byte XModem)
/// Send to direct XRECV, not XModem server
#[clap(short, long, action, default_value_t = false)]
direct: bool,

/// Finish XModem server after file transfer
#[clap(short, long, action, default_value_t = false)]
finish: bool,
},

/// Get file from Kermit server
/// Get file from SEND or ARCHIVE command (not server!)
Kget {
#[clap(parse(from_os_str))]
path: std::path::PathBuf,
Expand All @@ -73,7 +73,7 @@ enum Commands {
overwrite: bool,
},

/// Get file from XModem server or ARCHIVE command
/// Get file with XModem
Xget {
#[clap(parse(from_os_str))]
path: std::path::PathBuf,
Expand All @@ -82,9 +82,13 @@ enum Commands {
#[clap(short, long, action, default_value_t = false)]
overwrite: bool,

/// Get from direct XSEND, not server (bypasses server operations)
/// Get from direct XSEND, not XMODEM server
#[clap(short, long, action, default_value_t = false)]
direct: bool,

/// Finish XModem server after file transfer
#[clap(short, long, action, default_value_t = false)]
finish: bool,
},

/// Run HP object info check on `path` instead of transferring file
Expand Down Expand Up @@ -140,7 +144,7 @@ fn get_serial_port(cli_port: Option<PathBuf>, cli_baud: Option<u32>) -> Box<dyn
// This is not how I would normally write a match statement, but I
// didn't want to deal with the return type in the Err arm.
let port = serialport::new(final_port, final_baud)
.timeout(Duration::from_millis(1500))
.timeout(Duration::from_millis(3500))
.open();
match port {
// e.description is a string,
Expand All @@ -160,7 +164,7 @@ fn main() {

// Dispatch operation
match &cli.command {
Commands::Xsend { direct, path } => {
Commands::Xsend { direct, path, finish } => {
let mut port = get_serial_port(cli.port, cli.baud);
//println!("Xsend, direct = {:?}, path = {:?}", direct, path);
// we actually use {:?} on the filename so that it displays in quotes
Expand All @@ -173,58 +177,54 @@ fn main() {
style(port.name().unwrap()).green().bright());
if *direct {
// send file directly to XRECV
if cli.finish {
if *finish {
println!("{}: {}{}{}",
style("warning").yellow().bright(),
"ignoring flag ", style("-f").green(),
" (finish) used in XModem direct mode");
" (finish server) used in XModem direct mode.");
}
// TODO: why do we use different forms of path here versus later?
xmodem::send_file_normal(&path.to_path_buf(), &mut port);
xmodem::send_file_normal(path, &mut port);
} else {
// send file to server
xmodem::send_file_conn4x(&path.to_path_buf(), &mut port, &cli.finish);
xmodem::send_file_conn4x(path, &mut port, finish);
}
println!("{}", style("Done!").green().bright());
// I like the way this newline and indent looks.
print!("File info:\n ");
hp_object::crc_and_output(path);
},

Commands::Xget { direct, path, overwrite } => {
Commands::Xget { direct, path, overwrite, finish } => {
let mut port = get_serial_port(cli.port, cli.baud);
//println!("Xget, path = {:?}, overwrite = {:?}", path, overwrite);
// get the actual path that the transfer wrote to
let final_path = xmodem::get_file(path, &mut port, direct, overwrite, &cli.finish);
let final_path = xmodem::get_file(path, &mut port, direct, overwrite, finish);
// "of" is not the right preposition to use here, but it
// makes it clear that we're talking about the file after
// processing, stored on the computer's drive.
print!("Info of received file:\n ");
hp_object::crc_and_output(&final_path);
},

Commands::Ksend { path } => {
Commands::Ksend { path, finish } => {
let mut port = get_serial_port(cli.port, cli.baud);
println!("Sending {:?} via Kermit on {}...",
style(path.file_name().unwrap()).yellow().bright(),
style(port.name().unwrap()).green().bright());
//println!("Ksend, path = {:?}, finish = {:?}", path, cli.finish);
kermit::send_file(path, &mut port, cli.finish);

kermit::send_file(path, &mut port, finish);
print!("File info:\n ");
hp_object::crc_and_output(path);
},
// I am not implementing kermit receive at the moment, but I
// probably will later, because it's the only easy way to get
// ASCII data.

Commands::Kget { path, overwrite } => {
let mut port = get_serial_port(cli.port, cli.baud);
kermit::get_file(path, &mut port);
println!("Kget, path = {:?}, overwrite = {:?}", path, overwrite);
let final_path = kermit::get_file(path, &mut port, overwrite);
print!("Info of received file:\n ");
hp_object::crc_and_output(&final_path);
},

Commands::Info { path } => {
//println!("Info mode, path = {:?}", path);
hp_object::crc_and_output(path);
},
}
Expand Down
Loading

0 comments on commit baa48e6

Please sign in to comment.