diff --git a/CHANGELOG.md b/CHANGELOG.md index e2ff523..4658d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,19 @@ # Change Log -## Unreleased changes +## Unreleased changes ([Source](https://github.com/neotron-compute/neotron-os/tree/develop) | [Changes](https://github.com/neotron-compute/neotron-os/compare/v0.7.0...develop)) * None -## v0.6.0 (2023-10-08, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.6.0)) +## v0.7.0 - 2023-10-21 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.7.0) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.7.0)) + +* Add `i2c` command. +* Support printing `\t`, with 8 character tab-stops +* Add `type` command to print files +* Add `exec` command to execute scripts containing commands +* Update `embedded-sdmmc` crate +* Split `lshw` into `lsblk`, `lsbus`, `lsi2c`, `lsmem` and `lsuart` + +## v0.6.0 - 2023-10-08 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.6.0) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.6.0)) * Can set/set video mode * Stores video mode as part of config @@ -13,7 +22,7 @@ * Added mixer command * Switch to [`neotron-common-bios`] 0.11.1 -## v0.5.0 (2023-07-21, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.5.0)) +## v0.5.0 - 2023-07-21 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.5.0) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.5.0)) * Switch to [`neotron-common-bios`] 0.11 * Added "Shutdown" command @@ -22,12 +31,12 @@ * Use new compare-and-swap BIOS API to implement mutexes, instead of `static mut` * OS now requires 256K Flash space -## v0.4.0 (2023-06-25, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.4.0)) +## v0.4.0 - 2023-06-25 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.4.0) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.4.0)) * The `load` command now takes ELF binaries, not raw binaries. * Neotron OS can now be used as a dependency within an application, if desired. -## v0.3.3 (2023-05-22, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.3.3)) +## v0.3.3 - 2023-05-22 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.3.3) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.3.3)) * Add `dir` command * Change `load` command to load from disk @@ -35,18 +44,18 @@ * Update to `postcard` 1.0 * Fix `readblk` help text, and print 32 bytes per line -## v0.3.2 (2023-05-05, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.3.2)) +## v0.3.2 - 2023-05-05 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.3.2) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.3.2)) * Add `date` command. * Add `lsblk` and `blkread` commands. * Renamed `bioshw` to `lshw` -## v0.3.1 (2023-03-09, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.3.1)) +## v0.3.1 - 2023-03-09 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.3.1) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.3.1)) * Add `hexdump`, `load` and `run` commands. * Set colour attributes correctly (White on Black only currently) -## v0.3.0 (2023-02-12, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.3.0)) +## v0.3.0 - 2023-02-12 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.3.0) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.3.0)) * Updated to [`neotron-common-bios`] v0.8.0 * Use [`pc-keyboard`] for decoding HID events @@ -59,10 +68,10 @@ [`neotron-common-bios`]: https://crates.io/crates/neotron-common-bios [`pc-keyboard`]: https://crates.io/crates/pc-keyboard -## v0.2.0 (2023-01-07, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.2.0)) +## v0.2.0 - 2023-01-07 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.2.0) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.2.0)) Adds HID support and basic shell, with 'mem' and 'fill' commands. -## v0.1.0 (2022-03-18, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.1.0)) +## v0.1.0 - 2022-03-18 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.1.0) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.1.0)) First version. diff --git a/Cargo.lock b/Cargo.lock index 53592db..1952fbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,12 +75,13 @@ dependencies = [ [[package]] name = "embedded-sdmmc" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4d14180a76a8af24a45a0e1a4f9c97491b05a3b962d59d5e4ce0e6ab103736" +checksum = "3778099e48fedd6b81048e1d84f60db663124f5c6e22fcef495040b9d51fe06a" dependencies = [ "byteorder", "embedded-hal", + "heapless", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8d82f88..9709e4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "neotron-os" -version = "0.6.0" +version = "0.7.0" authors = [ "Jonathan 'theJPster' Pallant ", "The Neotron Developers" @@ -49,7 +49,7 @@ postcard = "1.0" serde = { version = "1.0", default-features = false } menu = "0.3" chrono = { version = "0.4", default-features = false } -embedded-sdmmc = { version = "0.5", default-features = false } +embedded-sdmmc = { version = "0.6", default-features = false } neotron-api = "0.1" neotron-loader = "0.1" vte = "0.12" diff --git a/build.rs b/build.rs index 06bdfa4..de46505 100644 --- a/build.rs +++ b/build.rs @@ -31,3 +31,5 @@ fn main() { println!("cargo:rustc-link-lib=dylib=msvcrt"); } } + +// End of file diff --git a/src/bin/flash0002.rs b/src/bin/flash0002.rs index de2fc0f..8de5d7b 100644 --- a/src/bin/flash0002.rs +++ b/src/bin/flash0002.rs @@ -14,3 +14,5 @@ #[link_section = ".entry_point"] #[used] pub static ENTRY_POINT_ADDR: extern "C" fn(&neotron_common_bios::Api) -> ! = neotron_os::os_main; + +// End of file diff --git a/src/bin/flash0802.rs b/src/bin/flash0802.rs index 340707b..3e3a09e 100644 --- a/src/bin/flash0802.rs +++ b/src/bin/flash0802.rs @@ -14,3 +14,5 @@ #[link_section = ".entry_point"] #[used] pub static ENTRY_POINT_ADDR: extern "C" fn(&neotron_common_bios::Api) -> ! = neotron_os::os_main; + +// End of file diff --git a/src/bin/flash1002.rs b/src/bin/flash1002.rs index 33fa480..8e4d1c7 100644 --- a/src/bin/flash1002.rs +++ b/src/bin/flash1002.rs @@ -14,3 +14,5 @@ #[link_section = ".entry_point"] #[used] pub static ENTRY_POINT_ADDR: extern "C" fn(&neotron_common_bios::Api) -> ! = neotron_os::os_main; + +// End of file diff --git a/src/commands/block.rs b/src/commands/block.rs index afe9d13..c83cb06 100644 --- a/src/commands/block.rs +++ b/src/commands/block.rs @@ -1,16 +1,8 @@ //! Block Device related commands for Neotron OS +use super::{parse_u64, parse_u8}; use crate::{bios, osprint, osprintln, Ctx, API}; -pub static LSBLK_ITEM: menu::Item = menu::Item { - item_type: menu::ItemType::Callback { - function: lsblk, - parameters: &[], - }, - command: "lsblk", - help: Some("List all the Block Devices"), -}; - pub static READ_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { function: read_block, @@ -29,66 +21,21 @@ pub static READ_ITEM: menu::Item = menu::Item { help: Some("Display one disk block, as hex"), }; -/// Called when the "lsblk" command is executed. -fn lsblk(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { - let api = API.get(); - let mut found = false; - - osprintln!("Block Devices:"); - for dev_idx in 0..=255u8 { - if let bios::FfiOption::Some(device_info) = (api.block_dev_get_info)(dev_idx) { - let (bsize, bunits, dsize, dunits) = - match device_info.num_blocks * u64::from(device_info.block_size) { - x if x < (1024 * 1024 * 1024) => { - // Under 1 GiB, give it in 10s of MiB - (10 * x / (1024 * 1024), "MiB", x / 100_000, "MB") - } - x => { - // Anything else in GiB - (10 * x / (1024 * 1024 * 1024), "GiB", x / 100_000_000, "GB") - } - }; - osprintln!("Device {}:", dev_idx); - osprintln!(" Name: {}", device_info.name); - osprintln!(" Type: {:?}", device_info.device_type); - osprintln!(" Block size: {}", device_info.block_size); - osprintln!(" Num Blocks: {}", device_info.num_blocks); - osprintln!( - " Card Size: {}.{} {} ({}.{} {})", - bsize / 10, - bsize % 10, - bunits, - dsize / 10, - dsize % 10, - dunits - ); - osprintln!(" Ejectable: {}", device_info.ejectable); - osprintln!(" Removable: {}", device_info.removable); - osprintln!(" Media Present: {}", device_info.media_present); - osprintln!(" Read Only: {}", device_info.read_only); - found = true; - } - } - if !found { - osprintln!(" None"); - } -} - /// Called when the "read_block" command is executed. fn read_block(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { let api = API.get(); - let Ok(dev_idx) = args[0].parse::() else { + let Ok(device_idx) = parse_u8(args[0]) else { osprintln!("Couldn't parse {:?}", args[0]); return; }; - let Ok(block_idx) = args[1].parse::() else { + let Ok(block_idx) = parse_u64(args[1]) else { osprintln!("Couldn't parse {:?}", args[1]); return; }; osprintln!("Reading block {}:", block_idx); let mut buffer = [0u8; 512]; match (api.block_read)( - dev_idx, + device_idx, bios::block_dev::BlockIdx(block_idx), 1, bios::FfiBuffer::new(&mut buffer), @@ -110,3 +57,5 @@ fn read_block(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], _ } } } + +// End of file diff --git a/src/commands/config.rs b/src/commands/config.rs index f5aeea9..741d729 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -106,3 +106,5 @@ fn command(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: } } } + +// End of file diff --git a/src/commands/fs.rs b/src/commands/fs.rs index 4d8fcf9..7a1a9d8 100644 --- a/src/commands/fs.rs +++ b/src/commands/fs.rs @@ -1,7 +1,5 @@ //! File Systems related commands for Neotron OS -use embedded_sdmmc::VolumeIdx; - use crate::{bios, osprint, osprintln, Ctx}; pub static DIR_ITEM: menu::Item = menu::Item { @@ -25,6 +23,30 @@ pub static LOAD_ITEM: menu::Item = menu::Item { help: Some("Load a file into the application area"), }; +pub static EXEC_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: exec, + parameters: &[menu::Parameter::Mandatory { + parameter_name: "file", + help: Some("The shell script to run"), + }], + }, + command: "exec", + help: Some("Execute a shell script"), +}; + +pub static TYPE_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: typefn, + parameters: &[menu::Parameter::Mandatory { + parameter_name: "file", + help: Some("The file to type"), + }], + }, + command: "type", + help: Some("Type a file to the console"), +}; + /// Called when the "dir" command is executed. fn dir(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { fn work() -> Result<(), embedded_sdmmc::Error> { @@ -33,11 +55,11 @@ fn dir(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: & let time = crate::fs::BiosTime(); let mut mgr = embedded_sdmmc::VolumeManager::new(bios_block, time); // Open the first partition - let volume = mgr.get_volume(VolumeIdx(0))?; - let root_dir = mgr.open_root_dir(&volume)?; + let volume = mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; + let root_dir = mgr.open_root_dir(volume)?; let mut total_bytes = 0u64; let mut num_files = 0; - mgr.iterate_dir(&volume, &root_dir, |dir_entry| { + mgr.iterate_dir(root_dir, |dir_entry| { let padding = 8 - dir_entry.name.base_name().len(); for b in dir_entry.name.base_name() { let ch = *b as char; @@ -99,3 +121,76 @@ fn load(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &m } } } + +/// Called when the "exec" command is executed. +fn exec(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { + fn work(ctx: &mut Ctx, filename: &str) -> Result<(), embedded_sdmmc::Error> { + let bios_block = crate::fs::BiosBlock(); + let time = crate::fs::BiosTime(); + let mut mgr = embedded_sdmmc::VolumeManager::new(bios_block, time); + // Open the first partition + let volume = mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; + let root_dir = mgr.open_root_dir(volume)?; + let file = mgr.open_file_in_dir(root_dir, filename, embedded_sdmmc::Mode::ReadOnly)?; + let buffer = ctx.tpa.as_slice_u8(); + let count = mgr.read(file, buffer)?; + if count != mgr.file_length(file)? as usize { + osprintln!("File too large! Max {} bytes allowed.", buffer.len()); + return Ok(()); + } + let Ok(s) = core::str::from_utf8(&buffer[0..count]) else { + osprintln!("File is not valid UTF-8"); + return Ok(()); + }; + // tell the main loop to run from these bytes next + ctx.exec_tpa = Some(s.len()); + Ok(()) + } + + // index can't panic - we always have enough args + let r = work(ctx, args[0]); + match r { + Ok(_) => {} + Err(e) => { + osprintln!("Error: {:?}", e); + } + } +} + +/// Called when the "type" command is executed. +fn typefn(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { + fn work(ctx: &mut Ctx, filename: &str) -> Result<(), embedded_sdmmc::Error> { + let bios_block = crate::fs::BiosBlock(); + let time = crate::fs::BiosTime(); + let mut mgr = embedded_sdmmc::VolumeManager::new(bios_block, time); + // Open the first partition + let volume = mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; + let root_dir = mgr.open_root_dir(volume)?; + let file = mgr.open_file_in_dir(root_dir, filename, embedded_sdmmc::Mode::ReadOnly)?; + let buffer = ctx.tpa.as_slice_u8(); + let count = mgr.read(file, buffer)?; + if count != mgr.file_length(file)? as usize { + osprintln!("File too large! Max {} bytes allowed.", buffer.len()); + return Ok(()); + } + let Ok(s) = core::str::from_utf8(&buffer[0..count]) else { + osprintln!("File is not valid UTF-8"); + return Ok(()); + }; + osprintln!("{}", s); + Ok(()) + } + + // index can't panic - we always have enough args + let r = work(ctx, args[0]); + // reset SGR + osprint!("\u{001b}[0m"); + match r { + Ok(_) => {} + Err(e) => { + osprintln!("Error: {:?}", e); + } + } +} + +// End of file diff --git a/src/commands/hardware.rs b/src/commands/hardware.rs index 3b52d93..f72b14a 100644 --- a/src/commands/hardware.rs +++ b/src/commands/hardware.rs @@ -2,13 +2,51 @@ use crate::{bios, osprintln, Ctx, API}; -pub static LSHW_ITEM: menu::Item = menu::Item { +use super::{parse_u8, parse_usize}; + +pub static LSBLK_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { - function: lshw, + function: lsblk, parameters: &[], }, - command: "lshw", - help: Some("List all the BIOS hardware"), + command: "lsblk", + help: Some("List all the Block Devices"), +}; + +pub static LSBUS_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: lsbus, + parameters: &[], + }, + command: "lsbus", + help: Some("List all the Neotron Bus devices"), +}; + +pub static LSI2C_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: lsi2c, + parameters: &[], + }, + command: "lsi2c", + help: Some("List all the BIOS I2C devices"), +}; + +pub static LSMEM_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: lsmem, + parameters: &[], + }, + command: "lsmem", + help: Some("List all the BIOS Memory regions"), +}; + +pub static LSUART_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: lsuart, + parameters: &[], + }, + command: "lsuart", + help: Some("List all the BIOS UARTs"), }; pub static SHUTDOWN_ITEM: menu::Item = menu::Item { @@ -29,116 +67,162 @@ pub static SHUTDOWN_ITEM: menu::Item = menu::Item { help: Some("Shutdown the system"), }; -/// Called when the "lshw" command is executed. -fn lshw(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { +pub static I2C_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: i2c, + parameters: &[ + menu::Parameter::Mandatory { + parameter_name: "bus_idx", + help: Some("I2C bus index"), + }, + menu::Parameter::Mandatory { + parameter_name: "dev_addr", + help: Some("7-bit I2C device address"), + }, + menu::Parameter::Mandatory { + parameter_name: "tx_bytes", + help: Some("Hex string to transmit"), + }, + menu::Parameter::Mandatory { + parameter_name: "rx_count", + help: Some("How many bytes to receive"), + }, + ], + }, + command: "i2c", + help: Some("Do an I2C transaction on a bus"), +}; + +/// Called when the "lsblk" command is executed. +fn lsblk(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { let api = API.get(); let mut found = false; - osprintln!("Memory regions:"); - for region_idx in 0..=255u8 { - if let bios::FfiOption::Some(region) = (api.memory_get_region)(region_idx) { - osprintln!(" {}: {}", region_idx, region); - found = true; - } - } - if !found { - osprintln!(" None"); - } - - found = false; - - osprintln!("Serial Devices:"); + osprintln!("Block Devices:"); for dev_idx in 0..=255u8 { - if let bios::FfiOption::Some(device_info) = (api.serial_get_info)(dev_idx) { + if let bios::FfiOption::Some(device_info) = (api.block_dev_get_info)(dev_idx) { + let (bsize, bunits, dsize, dunits) = + match device_info.num_blocks * u64::from(device_info.block_size) { + x if x < (1024 * 1024 * 1024) => { + // Under 1 GiB, give it in 10s of MiB + (10 * x / (1024 * 1024), "MiB", x / 100_000, "MB") + } + x => { + // Anything else in GiB + (10 * x / (1024 * 1024 * 1024), "GiB", x / 100_000_000, "GB") + } + }; + osprintln!("Device {}:", dev_idx); + osprintln!("\t Name: {}", device_info.name); + osprintln!("\t Type: {:?}", device_info.device_type); + osprintln!("\tBlock size: {}", device_info.block_size); + osprintln!("\tNum Blocks: {}", device_info.num_blocks); osprintln!( - " {}: {} {:?}", - dev_idx, - device_info.name, - device_info.device_type + "\t Card Size: {}.{} {} ({}.{} {})", + bsize / 10, + bsize % 10, + bunits, + dsize / 10, + dsize % 10, + dunits + ); + osprintln!("\t Ejectable: {}", device_info.ejectable); + osprintln!("\t Removable: {}", device_info.removable); + osprintln!("\t Read Only: {}", device_info.read_only); + osprintln!( + "\t Media: {}", + if device_info.media_present { + "Present" + } else { + "Missing" + } ); found = true; } } if !found { - osprintln!(" None"); + osprintln!("\tNone"); } +} - found = false; - - osprintln!("Block Devices:"); +/// Called when the "lsbus" command is executed. +fn lsbus(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { + let api = API.get(); + let mut found = false; + osprintln!("Neotron Bus Devices:"); for dev_idx in 0..=255u8 { - if let bios::FfiOption::Some(device_info) = (api.block_dev_get_info)(dev_idx) { - osprintln!( - " {}: {} {:?} bs={} size={} MiB", - dev_idx, - device_info.name, - device_info.device_type, - device_info.block_size, - (device_info.num_blocks * u64::from(device_info.block_size)) / (1024 * 1024) - ); + if let bios::FfiOption::Some(device_info) = (api.bus_get_info)(dev_idx) { + let kind = match device_info.kind { + bios::bus::PeripheralKind::Slot => "Slot", + bios::bus::PeripheralKind::SdCard => "SdCard", + bios::bus::PeripheralKind::Reserved => "Reserved", + }; + osprintln!("\t{}: {} ({})", dev_idx, device_info.name, kind); found = true; } } if !found { - osprintln!(" None"); + osprintln!("\tNone"); } +} - found = false; - +/// Called when the "lsi2c" command is executed. +fn lsi2c(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { + let api = API.get(); + let mut found = false; osprintln!("I2C Buses:"); for dev_idx in 0..=255u8 { if let bios::FfiOption::Some(device_info) = (api.i2c_bus_get_info)(dev_idx) { - osprintln!(" {}: {:?}", dev_idx, device_info); + osprintln!("\t{}: {}", dev_idx, device_info.name); found = true; } } if !found { - osprintln!(" None"); + osprintln!("\tNone"); } +} - found = false; - - osprintln!("Neotron Bus Devices:"); - for dev_idx in 0..=255u8 { - if let bios::FfiOption::Some(device_info) = (api.bus_get_info)(dev_idx) { - osprintln!(" {}: {:?}", dev_idx, device_info); +/// Called when the "lsmem" command is executed. +fn lsmem(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { + let api = API.get(); + let mut found = false; + osprintln!("Memory regions:"); + for region_idx in 0..=255u8 { + if let bios::FfiOption::Some(region) = (api.memory_get_region)(region_idx) { + osprintln!("\t{}: {}", region_idx, region); found = true; } } if !found { - osprintln!(" None"); + osprintln!("\tNone"); } +} - found = false; - - osprintln!("Audio Mixers:"); +/// Called when the "lsuart" command is executed. +fn lsuart(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { + let api = API.get(); + let mut found = false; + osprintln!("UART Devices:"); for dev_idx in 0..=255u8 { - if let bios::FfiOption::Some(device_info) = (api.audio_mixer_channel_get_info)(dev_idx) { - let dir = match device_info.direction { - bios::audio::Direction::Input => "In", - bios::audio::Direction::Output => "Out", - bios::audio::Direction::Loopback => "Loop", + if let bios::FfiOption::Some(device_info) = (api.serial_get_info)(dev_idx) { + let device_type = match device_info.device_type { + bios::serial::DeviceType::Rs232 => "RS232", + bios::serial::DeviceType::TtlUart => "TTL", + bios::serial::DeviceType::UsbCdc => "USB", + bios::serial::DeviceType::Midi => "MIDI", }; - osprintln!( - " {}: {:08} ({}) {}/{}", - dev_idx, - device_info.name, - dir, - device_info.current_level, - device_info.max_level - ); + osprintln!("\t{}: {} ({})", dev_idx, device_info.name, device_type); found = true; } } if !found { - osprintln!(" None"); + osprintln!("\tNone"); } } /// Called when the "shutdown" command is executed. fn shutdown(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { let api = API.get(); - if let Ok(Some(_)) = menu::argument_finder(item, args, "reboot") { osprintln!("Rebooting..."); (api.power_control)(bios::PowerMode::Reset); @@ -150,3 +234,100 @@ fn shutdown(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx (api.power_control)(bios::PowerMode::Off); } } + +/// Called when the "i2c" command is executed. +fn i2c(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { + let bus_idx = menu::argument_finder(item, args, "bus_idx").unwrap(); + let dev_addr = menu::argument_finder(item, args, "dev_addr").unwrap(); + let tx_bytes = menu::argument_finder(item, args, "tx_bytes").unwrap(); + let rx_count = menu::argument_finder(item, args, "rx_count").unwrap(); + + let (Some(bus_idx), Some(dev_addr), Some(tx_bytes), Some(rx_count)) = + (bus_idx, dev_addr, tx_bytes, rx_count) + else { + osprintln!("Missing arguments."); + return; + }; + + let mut tx_buffer: heapless::Vec = heapless::Vec::new(); + + for hex_pair in tx_bytes.as_bytes().chunks(2) { + let Some(top) = hex_digit(hex_pair[0]) else { + osprintln!("Bad hex."); + return; + }; + let Some(bottom) = hex_digit(hex_pair[1]) else { + osprintln!("Bad hex."); + return; + }; + let byte = top << 4 | bottom; + let Ok(_) = tx_buffer.push(byte) else { + osprintln!("Too much hex."); + return; + }; + } + + let Ok(bus_idx) = parse_u8(bus_idx) else { + osprintln!("Bad bus_idx"); + return; + }; + + let Ok(dev_addr) = parse_u8(dev_addr) else { + osprintln!("Bad dev_addr"); + return; + }; + + let Ok(rx_count) = parse_usize(rx_count) else { + osprintln!("Bad rx count."); + return; + }; + + let mut rx_buf = [0u8; 16]; + + let Some(rx_buf) = rx_buf.get_mut(0..rx_count) else { + osprintln!("Too much rx."); + return; + }; + + let api = API.get(); + + match (api.i2c_write_read)( + bus_idx, + dev_addr, + tx_buffer.as_slice().into(), + bios::FfiByteSlice::empty(), + rx_buf.into(), + ) { + bios::FfiResult::Ok(_) => { + osprintln!("Ok, got {:x?}", rx_buf); + } + bios::FfiResult::Err(e) => { + osprintln!("Failed: {:?}", e); + } + } +} + +/// Convert an ASCII hex digit into a number +fn hex_digit(input: u8) -> Option { + match input { + b'0' => Some(0), + b'1' => Some(1), + b'2' => Some(2), + b'3' => Some(3), + b'4' => Some(4), + b'5' => Some(5), + b'6' => Some(6), + b'7' => Some(7), + b'8' => Some(8), + b'9' => Some(9), + b'a' | b'A' => Some(10), + b'b' | b'B' => Some(11), + b'c' | b'C' => Some(12), + b'd' | b'D' => Some(13), + b'e' | b'E' => Some(14), + b'f' | b'F' => Some(15), + _ => None, + } +} + +// End of file diff --git a/src/commands/input.rs b/src/commands/input.rs index eccd3c7..82514e9 100644 --- a/src/commands/input.rs +++ b/src/commands/input.rs @@ -42,3 +42,5 @@ fn kbtest(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx } osprintln!("Finished."); } + +// End of file diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ce918f6..51afd26 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -19,14 +19,20 @@ pub static OS_MENU: menu::Menu = menu::Menu { items: &[ &timedate::DATE_ITEM, &config::COMMAND_ITEM, - &block::LSBLK_ITEM, + &hardware::LSBLK_ITEM, + &hardware::LSBUS_ITEM, + &hardware::LSI2C_ITEM, + &hardware::LSMEM_ITEM, + &hardware::LSUART_ITEM, + &hardware::I2C_ITEM, &block::READ_ITEM, &fs::DIR_ITEM, - &hardware::LSHW_ITEM, &ram::HEXDUMP_ITEM, &ram::RUN_ITEM, &ram::LOAD_ITEM, &fs::LOAD_ITEM, + &fs::EXEC_ITEM, + &fs::TYPE_ITEM, &screen::CLS_ITEM, &screen::MODE_ITEM, &input::KBTEST_ITEM, @@ -37,3 +43,44 @@ pub static OS_MENU: menu::Menu = menu::Menu { entry: None, exit: None, }; + +/// Parse a string into a `usize` +/// +/// Numbers like `0x123` are hex. Numbers like `123` are decimal. +fn parse_usize(input: &str) -> Result { + if let Some(digits) = input.strip_prefix("0x") { + // Parse as hex + usize::from_str_radix(digits, 16) + } else { + // Parse as decimal + input.parse::() + } +} + +/// Parse a string into a `u8` +/// +/// Numbers like `0x123` are hex. Numbers like `123` are decimal. +fn parse_u8(input: &str) -> Result { + if let Some(digits) = input.strip_prefix("0x") { + // Parse as hex + u8::from_str_radix(digits, 16) + } else { + // Parse as decimal + input.parse::() + } +} + +/// Parse a string into a `u64` +/// +/// Numbers like `0x123` are hex. Numbers like `123` are decimal. +fn parse_u64(input: &str) -> Result { + if let Some(digits) = input.strip_prefix("0x") { + // Parse as hex + u64::from_str_radix(digits, 16) + } else { + // Parse as decimal + input.parse::() + } +} + +// End of file diff --git a/src/commands/ram.rs b/src/commands/ram.rs index 5d0c45c..a7ebac6 100644 --- a/src/commands/ram.rs +++ b/src/commands/ram.rs @@ -1,5 +1,6 @@ //! Raw RAM read/write related commands for Neotron OS +use super::parse_usize; use crate::{osprint, osprintln, Ctx}; pub static HEXDUMP_ITEM: menu::Item = menu::Item { @@ -47,16 +48,6 @@ pub static LOAD_ITEM: menu::Item = menu::Item { help: Some("Copy a program from ROM/RAM into the application area"), }; -fn parse_usize(input: &str) -> Result { - if let Some(digits) = input.strip_prefix("0x") { - // Parse as hex - usize::from_str_radix(digits, 16) - } else { - // Parse as decimal - input.parse::() - } -} - /// Called when the "hexdump" command is executed. /// /// If you ask for an address that generates a HardFault, the OS will crash. So @@ -146,3 +137,5 @@ fn loadf(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: & } } } + +// End of file diff --git a/src/commands/sound.rs b/src/commands/sound.rs index 853046b..36a6ce2 100644 --- a/src/commands/sound.rs +++ b/src/commands/sound.rs @@ -99,7 +99,7 @@ fn mixer(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: & .unwrap_or(true) { osprintln!( - "#{}: {} ({}) {}/{}", + "\t{}: {} ({}) {}/{}", mixer_id, mixer_info.name, dir_str, @@ -127,14 +127,9 @@ fn play(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &m let time = crate::fs::BiosTime(); let mut mgr = embedded_sdmmc::VolumeManager::new(bios_block, time); // Open the first partition - let mut volume = mgr.get_volume(embedded_sdmmc::VolumeIdx(0))?; - let root_dir = mgr.open_root_dir(&volume)?; - let mut file = mgr.open_file_in_dir( - &mut volume, - &root_dir, - file_name, - embedded_sdmmc::Mode::ReadOnly, - )?; + let volume = mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; + let root_dir = mgr.open_root_dir(volume)?; + let file = mgr.open_file_in_dir(root_dir, file_name, embedded_sdmmc::Mode::ReadOnly)?; osprintln!("Press Q to quit, P to pause/unpause..."); @@ -145,9 +140,10 @@ fn play(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &m let mut delta = 0; let mut pause = false; - 'playback: while !file.eof() { + + 'playback: while !mgr.file_eof(file).unwrap() { if !pause { - let bytes_read = mgr.read(&volume, &mut file, buffer)?; + let bytes_read = mgr.read(file, buffer)?; let mut buffer = &buffer[0..bytes_read]; while !buffer.is_empty() { let slice = bios::FfiByteSlice::new(buffer); @@ -194,3 +190,5 @@ fn play(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &m osprintln!("\nError during playback: {:?}", e); } } + +// End of file diff --git a/src/commands/timedate.rs b/src/commands/timedate.rs index a73fd54..2c9464c 100644 --- a/src/commands/timedate.rs +++ b/src/commands/timedate.rs @@ -41,3 +41,5 @@ fn date(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &m time.nanosecond() ); } + +// End of file diff --git a/src/config.rs b/src/config.rs index c7a71f5..6c19c87 100644 --- a/src/config.rs +++ b/src/config.rs @@ -88,3 +88,5 @@ impl core::default::Default for Config { } } } + +// End of file diff --git a/src/fs.rs b/src/fs.rs index af7bb5f..5422eda 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -80,3 +80,5 @@ impl embedded_sdmmc::TimeSource for BiosTime { } } } + +// End of file diff --git a/src/lib.rs b/src/lib.rs index 5781905..d2530cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -325,6 +325,9 @@ impl StdInput { pub struct Ctx { config: config::Config, tpa: program::TransientProgramArea, + /// This flag is set if the "run" command is entered. It tells us + /// to take our input bytes from the TPA. + exec_tpa: Option, } impl core::fmt::Write for Ctx { @@ -441,6 +444,7 @@ pub extern "C" fn os_main(api: &bios::Api) -> ! { // We have to trust the values given to us by the BIOS. If it lies, we will crash. program::TransientProgramArea::new(tpa_start, tpa_size) }, + exec_tpa: None, }; osprintln!( @@ -461,6 +465,35 @@ pub extern "C" fn os_main(api: &bios::Api) -> ! { for b in &buffer[0..count] { menu.input_byte(*b); } + // TODO: Consider recursively executing scripts, so that scripts can + // call scripts. + if let Some(n) = menu.context.exec_tpa { + menu.context.exec_tpa = None; + let ptr = menu.context.tpa.steal_top(n); + osprintln!("\rExecuting TPA..."); + let mut has_chars = false; + let slice = unsafe { core::slice::from_raw_parts(ptr, n) }; + // TODO: Give the user some way to break out of the loop. + for b in slice { + // Files contain `\n` or `\r\n` line endings. + // menu wants `\r` line endings. + if *b == b'\n' { + if has_chars { + // Execute this line + menu.input_byte(b'\r'); + has_chars = false; + } + } else if *b == b'\r' { + // Drop carriage returns + } else { + menu.input_byte(*b); + has_chars = true; + } + } + unsafe { + menu.context.tpa.restore_top(n); + } + } (api.power_idle)(); } } diff --git a/src/program.rs b/src/program.rs index 1784aff..4e3a315 100644 --- a/src/program.rs +++ b/src/program.rs @@ -61,8 +61,7 @@ impl From>> for Error { /// Something the ELF loader can use to get bytes off the disk struct FileSource { mgr: core::cell::RefCell>, - volume: embedded_sdmmc::Volume, - file: core::cell::RefCell, + file: embedded_sdmmc::File, buffer: core::cell::RefCell<[u8; Self::BUFFER_LEN]>, offset_cached: core::cell::Cell>, } @@ -70,15 +69,10 @@ struct FileSource { impl FileSource { const BUFFER_LEN: usize = 128; - fn new( - mgr: embedded_sdmmc::VolumeManager, - volume: embedded_sdmmc::Volume, - file: File, - ) -> FileSource { + fn new(mgr: embedded_sdmmc::VolumeManager, file: File) -> FileSource { FileSource { mgr: core::cell::RefCell::new(mgr), - file: core::cell::RefCell::new(file), - volume, + file, buffer: core::cell::RefCell::new([0u8; 128]), offset_cached: core::cell::Cell::new(None), } @@ -90,10 +84,11 @@ impl FileSource { out_buffer: &mut [u8], ) -> Result<(), embedded_sdmmc::Error> { osprintln!("Reading from {}", offset); - self.file.borrow_mut().seek_from_start(offset).unwrap(); self.mgr .borrow_mut() - .read(&self.volume, &mut self.file.borrow_mut(), out_buffer)?; + .file_seek_from_start(self.file, offset) + .unwrap(); + self.mgr.borrow_mut().read(self.file, out_buffer)?; Ok(()) } } @@ -117,12 +112,13 @@ impl neotron_loader::traits::Source for &FileSource { } osprintln!("Reading from {}", offset); - self.file.borrow_mut().seek_from_start(offset).unwrap(); - self.mgr.borrow_mut().read( - &self.volume, - &mut self.file.borrow_mut(), - self.buffer.borrow_mut().as_mut_slice(), - )?; + self.mgr + .borrow_mut() + .file_seek_from_start(self.file, offset) + .unwrap(); + self.mgr + .borrow_mut() + .read(self.file, self.buffer.borrow_mut().as_mut_slice())?; self.offset_cached.set(Some(offset)); chunk.copy_from_slice(&self.buffer.borrow()[0..chunk.len()]); @@ -181,12 +177,7 @@ impl TransientProgramArea { /// Borrow the TPA region as a slice of words pub fn as_slice_u32(&mut self) -> &mut [u32] { - unsafe { - core::slice::from_raw_parts_mut( - self.memory_bottom, - self.memory_top.offset_from(self.memory_bottom) as usize, - ) - } + unsafe { core::slice::from_raw_parts_mut(self.memory_bottom, self.size_words()) } } /// Borrow the TPA region as a slice of bytes @@ -194,12 +185,16 @@ impl TransientProgramArea { unsafe { core::slice::from_raw_parts_mut( self.memory_bottom as *mut u8, - (self.memory_top.offset_from(self.memory_bottom) as usize) - * core::mem::size_of::(), + self.size_words() * core::mem::size_of::(), ) } } + /// Size of the TPA in 32-bit words + fn size_words(&self) -> usize { + unsafe { self.memory_top.offset_from(self.memory_bottom) as usize } + } + /// Loads a program from disk into the Transient Program Area. /// /// The program must be in the Neotron Executable format. @@ -209,16 +204,11 @@ impl TransientProgramArea { let time = crate::fs::BiosTime(); let mut mgr = embedded_sdmmc::VolumeManager::new(bios_block, time); // Open the first partition - let mut volume = mgr.get_volume(embedded_sdmmc::VolumeIdx(0))?; - let root_dir = mgr.open_root_dir(&volume)?; - let file = mgr.open_file_in_dir( - &mut volume, - &root_dir, - file_name, - embedded_sdmmc::Mode::ReadOnly, - )?; - - let source = FileSource::new(mgr, volume, file); + let volume = mgr.open_volume(embedded_sdmmc::VolumeIdx(0))?; + let root_dir = mgr.open_root_dir(volume)?; + let file = mgr.open_file_in_dir(root_dir, file_name, embedded_sdmmc::Mode::ReadOnly)?; + + let source = FileSource::new(mgr, file); let loader = neotron_loader::Loader::new(&source)?; let mut iter = loader.iter_program_headers(); @@ -279,6 +269,38 @@ impl TransientProgramArea { self.last_entry = 0; Ok(result) } + + /// Move data to the top of TPA and make TPA shorter. + /// + /// Moves `size` bytes to the top of the TPA, and then pretends the TPA is + /// `size` bytes shorter than it was. + /// + /// `size` will be rounded up to a multiple of 4. + /// + /// Panics if `n` is too big to fit in the TPA. + /// + /// Returns a pointer to the data that now sits outside of the TPA. There + /// will be `size` bytes at this address but you must manage the lifetimes + /// yourself. + pub fn steal_top(&mut self, size: usize) -> *const u8 { + let stolen_words = (size + 3) / 4; + if stolen_words >= self.size_words() { + panic!("Stole too much from TPA!"); + } + unsafe { + // Top goes down to free memory above it + let new_top = self.memory_top.sub(stolen_words); + // Copy the data from the bottom to above the newly reduced TPA + core::ptr::copy(self.memory_bottom, new_top, stolen_words); + new_top as *mut u8 + } + } + + /// Restore the TPA back where it was. + pub unsafe fn restore_top(&mut self, size: usize) { + let restored_words = (size + 3) / 4; + self.memory_top = self.memory_top.add(restored_words); + } } /// Application API to print things to the console. diff --git a/src/vgaconsole.rs b/src/vgaconsole.rs index 695bf43..b8f70ab 100644 --- a/src/vgaconsole.rs +++ b/src/vgaconsole.rs @@ -199,12 +199,11 @@ impl ConsoleInner { /// We defer this so you can write the last char on the last line without /// causing it to scroll pre-emptively. fn scroll_as_required(&mut self) { - assert!(self.row <= self.height); - if self.col >= self.width { - self.col = 0; + while self.col >= self.width { + self.col -= self.width; self.row += 1; } - if self.row == self.height { + while self.row >= self.height { self.row -= 1; self.scroll_page(); } @@ -507,6 +506,9 @@ impl vte::Perform for ConsoleInner { b'\r' => { self.col = 0; } + b'\t' => { + self.col = (self.col + 8) & !7; + } b'\n' => { self.col = 0; self.row += 1;