diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f69fd..e91d6a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,17 @@ # Change Log -## Unreleased changes ([Source](https://github.com/neotron-compute/neotron-os/tree/develop) | [Changes](https://github.com/neotron-compute/neotron-os/compare/v0.7.1...develop)) +## Unreleased changes ([Source](https://github.com/neotron-compute/neotron-os/tree/develop) | [Changes](https://github.com/neotron-compute/neotron-os/compare/v0.8.0...develop)) * None +## v0.8.0 - 2024-04-11 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.8.0) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.8.0)) + +* Added a global `FILESYSTEM` object +* Updated to embedded-sdmmc 0.7 +* Updated to Neotron Common BIOS 0.12 +* Add a bitmap viewer command - `gfx` +* Treat text buffer as 32-bit values + ## v0.7.1 - 2023-10-21 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.7.1) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.7.1)) * Update `Cargo.lock` so build string no longer shows build as *dirty* diff --git a/Cargo.lock b/Cargo.lock index 30fe5b8..6c87658 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,9 +165,9 @@ checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" [[package]] name = "neotron-api" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c00f842e7006421002e67a53866b90ddd6f7d86137b52d2147f5e38b35e82f" +checksum = "67d6c96706b6f3ec069abfb042cadfd2d701980fa4940f407c0bc28ee1e1c493" dependencies = [ "bitflags", "neotron-ffi", @@ -198,7 +198,7 @@ checksum = "b9b8634a088b9d5b338a96b3f6ef45a3bc0b9c0f0d562c7d00e498265fd96e8f" [[package]] name = "neotron-os" -version = "0.7.1" +version = "0.8.0" dependencies = [ "chrono", "embedded-graphics", diff --git a/Cargo.toml b/Cargo.toml index 5bc2437..8639b83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "neotron-os" -version = "0.7.1" +version = "0.8.0" authors = [ "Jonathan 'theJPster' Pallant ", "The Neotron Developers" @@ -46,7 +46,7 @@ embedded-graphics = "0.8.1" embedded-sdmmc = { version = "0.7", default-features = false } heapless = "0.7" menu = "0.3" -neotron-api = "0.1" +neotron-api = "0.2" neotron-common-bios = "0.12.0" neotron-loader = "0.1" pc-keyboard = "0.7" diff --git a/src/commands/ram.rs b/src/commands/ram.rs index a7ebac6..85c5163 100644 --- a/src/commands/ram.rs +++ b/src/commands/ram.rs @@ -24,10 +24,27 @@ pub static HEXDUMP_ITEM: menu::Item = menu::Item { pub static RUN_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { function: run, - parameters: &[], + parameters: &[ + menu::Parameter::Optional { + parameter_name: "arg1", + help: None, + }, + menu::Parameter::Optional { + parameter_name: "arg2", + help: None, + }, + menu::Parameter::Optional { + parameter_name: "arg3", + help: None, + }, + menu::Parameter::Optional { + parameter_name: "arg4", + help: None, + }, + ], }, command: "run", - help: Some("Jump to start of application area"), + help: Some("Run a program (with up to four arguments)"), }; pub static LOAD_ITEM: menu::Item = menu::Item { @@ -90,8 +107,8 @@ fn hexdump(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], _ctx } /// Called when the "run" command is executed. -fn run(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], ctx: &mut Ctx) { - match ctx.tpa.execute() { +fn run(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { + match ctx.tpa.execute(args) { Ok(0) => { osprintln!(); } diff --git a/src/fs.rs b/src/fs.rs index 9adc436..a010968 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -110,6 +110,11 @@ impl File { FILESYSTEM.file_read(self, buffer) } + /// Write to a file + pub fn write(&self, buffer: &[u8]) -> Result<(), Error> { + FILESYSTEM.file_write(self, buffer) + } + /// Are we at the end of the file pub fn is_eof(&self) -> bool { FILESYSTEM @@ -202,6 +207,17 @@ impl Filesystem { Ok(bytes_read) } + /// Write to an open file + pub fn file_write(&self, file: &File, buffer: &[u8]) -> Result<(), Error> { + let mut fs = self.volume_manager.lock(); + if fs.is_none() { + *fs = Some(embedded_sdmmc::VolumeManager::new(BiosBlock(), BiosTime())); + } + let fs = fs.as_mut().unwrap(); + fs.write(file.inner, buffer)?; + Ok(()) + } + /// How large is a file? pub fn file_length(&self, file: &File) -> Result { let mut fs = self.volume_manager.lock(); diff --git a/src/program.rs b/src/program.rs index 5755667..8523c8e 100644 --- a/src/program.rs +++ b/src/program.rs @@ -1,6 +1,8 @@ //! Program Loading and Execution -use crate::{osprint, osprintln, FILESYSTEM}; +use neotron_api::FfiByteSlice; + +use crate::{fs, osprintln, refcell::CsRefCell, API, FILESYSTEM}; #[allow(unused)] static CALLBACK_TABLE: neotron_api::Api = neotron_api::Api { @@ -27,6 +29,43 @@ static CALLBACK_TABLE: neotron_api::Api = neotron_api::Api { free: api_free, }; +/// The different kinds of state each open handle can be in +pub enum OpenHandle { + /// Represents Standard Input + StdIn, + /// Represents Standard Output + Stdout, + /// Represents Standard Error + StdErr, + /// Represents an open file in the filesystem + File(fs::File), + /// Represents a closed handle. + /// + /// This is the default state for handles. + Closed, + /// Represents the audio device, + Audio, +} + +/// The open handle table +/// +/// This is indexed by the file descriptors (or handles) that the application +/// uses. When an application says "write to handle 4", we look at the 4th entry +/// in here to work out what they are writing to. +/// +/// The table is initialised when a program is started, and any open files are +/// closed when the program ends. +static OPEN_HANDLES: CsRefCell<[OpenHandle; 8]> = CsRefCell::new([ + OpenHandle::Closed, + OpenHandle::Closed, + OpenHandle::Closed, + OpenHandle::Closed, + OpenHandle::Closed, + OpenHandle::Closed, + OpenHandle::Closed, + OpenHandle::Closed, +]); + /// Ways in which loading a program can fail. #[derive(Debug)] pub enum Error { @@ -71,7 +110,6 @@ impl FileSource { } fn uncached_read(&self, offset: u32, out_buffer: &mut [u8]) -> Result<(), crate::fs::Error> { - osprintln!("Reading from {}", offset); self.file.seek_from_start(offset)?; self.file.read(out_buffer)?; Ok(()) @@ -96,7 +134,6 @@ impl neotron_loader::traits::Source for &FileSource { } } - osprintln!("Reading from {}", offset); self.file.seek_from_start(offset)?; self.file.read(self.buffer.borrow_mut().as_mut_slice())?; self.offset_cached.set(Some(offset)); @@ -230,17 +267,40 @@ impl TransientProgramArea { /// an exit code that is non-zero is not considered a failure from the point /// of view of this API. You wanted to run a program, and the program was /// run. - pub fn execute(&mut self) -> Result { + pub fn execute(&mut self, args: &[&str]) -> Result { if self.last_entry == 0 { return Err(Error::NothingLoaded); } + // Setup the default file handles + let mut open_handles = OPEN_HANDLES.lock(); + open_handles[0] = OpenHandle::StdIn; + open_handles[1] = OpenHandle::Stdout; + open_handles[2] = OpenHandle::StdErr; + drop(open_handles); + + // We support a maximum of four arguments. + #[allow(clippy::get_first)] + let ffi_args = [ + neotron_api::FfiString::new(args.get(0).unwrap_or(&"")), + neotron_api::FfiString::new(args.get(1).unwrap_or(&"")), + neotron_api::FfiString::new(args.get(2).unwrap_or(&"")), + neotron_api::FfiString::new(args.get(3).unwrap_or(&"")), + ]; + let result = unsafe { - let code: extern "C" fn(*const neotron_api::Api) -> i32 = + let code: neotron_api::AppStartFn = ::core::mem::transmute(self.last_entry as *const ()); - code(&CALLBACK_TABLE) + code(&CALLBACK_TABLE, args.len(), ffi_args.as_ptr()) }; + // Close any files the program left open + let mut open_handles = OPEN_HANDLES.lock(); + for h in open_handles.iter_mut() { + *h = OpenHandle::Closed; + } + drop(open_handles); + self.last_entry = 0; Ok(result) } @@ -278,15 +338,15 @@ impl TransientProgramArea { } } -/// Application API to print things to the console. -#[allow(unused)] -extern "C" fn print_fn(data: *const u8, len: usize) { - let slice = unsafe { core::slice::from_raw_parts(data, len) }; - if let Ok(s) = core::str::from_utf8(slice) { - osprint!("{}", s); - } else { - // Ignore App output - not UTF-8 +/// Store an open handle, or fail if we're out of space +fn allocate_handle(h: OpenHandle) -> Result { + for (idx, slot) in OPEN_HANDLES.lock().iter_mut().enumerate() { + if matches!(*slot, OpenHandle::Closed) { + *slot = h; + return Ok(idx); + } } + Err(h) } /// Open a file, given a path as UTF-8 string. @@ -296,15 +356,49 @@ extern "C" fn print_fn(data: *const u8, len: usize) { /// Path may be relative to current directory, or it may be an absolute /// path. extern "C" fn api_open( - _path: neotron_api::FfiString, + path: neotron_api::FfiString, _flags: neotron_api::file::Flags, ) -> neotron_api::Result { - neotron_api::Result::Err(neotron_api::Error::Unimplemented) + // Check for special devices + if path.as_str().eq_ignore_ascii_case("AUDIO:") { + match allocate_handle(OpenHandle::Audio) { + Ok(n) => { + return neotron_api::Result::Ok(neotron_api::file::Handle::new(n as u8)); + } + Err(_f) => { + return neotron_api::Result::Err(neotron_api::Error::OutOfMemory); + } + } + } + + // OK, let's assume it's a file relative to the root of our one and only volume + let f = match FILESYSTEM.open_file(path.as_str(), embedded_sdmmc::Mode::ReadOnly) { + Ok(f) => f, + Err(fs::Error::Io(embedded_sdmmc::Error::NotFound)) => { + return neotron_api::Result::Err(neotron_api::Error::InvalidPath); + } + Err(_e) => { + return neotron_api::Result::Err(neotron_api::Error::DeviceSpecific); + } + }; + + // 1. Put the file into the open handles array and get the index (or return an error) + match allocate_handle(OpenHandle::File(f)) { + Ok(n) => neotron_api::Result::Ok(neotron_api::file::Handle::new(n as u8)), + Err(_f) => neotron_api::Result::Err(neotron_api::Error::OutOfMemory), + } } /// Close a previously opened file. -extern "C" fn api_close(_fd: neotron_api::file::Handle) -> neotron_api::Result<()> { - neotron_api::Result::Err(neotron_api::Error::Unimplemented) +extern "C" fn api_close(fd: neotron_api::file::Handle) -> neotron_api::Result<()> { + let mut open_handles = OPEN_HANDLES.lock(); + match open_handles.get_mut(fd.value() as usize) { + Some(h) => { + *h = OpenHandle::Closed; + neotron_api::Result::Ok(()) + } + None => neotron_api::Result::Err(neotron_api::Error::BadHandle), + } } /// Write to an open file handle, blocking until everything is written. @@ -314,19 +408,47 @@ extern "C" fn api_write( fd: neotron_api::file::Handle, buffer: neotron_api::FfiByteSlice, ) -> neotron_api::Result<()> { - if fd == neotron_api::file::Handle::new_stdout() { - let mut guard = crate::VGA_CONSOLE.lock(); - if let Some(console) = guard.as_mut() { - console.write_bstr(buffer.as_slice()); + let mut open_handles = OPEN_HANDLES.lock(); + let Some(h) = open_handles.get_mut(fd.value() as usize) else { + return neotron_api::Result::Err(neotron_api::Error::BadHandle); + }; + match h { + OpenHandle::StdErr | OpenHandle::Stdout => { + // Treat stderr and stdout the same + let mut guard = crate::VGA_CONSOLE.lock(); + if let Some(console) = guard.as_mut() { + console.write_bstr(buffer.as_slice()); + } + let mut guard = crate::SERIAL_CONSOLE.lock(); + if let Some(console) = guard.as_mut() { + // Ignore serial errors on stdout + let _ = console.write_bstr(buffer.as_slice()); + } + neotron_api::Result::Ok(()) } - let mut guard = crate::SERIAL_CONSOLE.lock(); - if let Some(console) = guard.as_mut() { - // Ignore serial errors on stdout - let _ = console.write_bstr(buffer.as_slice()); + OpenHandle::File(f) => match f.write(buffer.as_slice()) { + Ok(_) => neotron_api::Result::Ok(()), + Err(_e) => neotron_api::Result::Err(neotron_api::Error::DeviceSpecific), + }, + OpenHandle::Audio => { + let api = API.get(); + let mut slice = buffer.as_slice(); + // loop until we've sent all of it + while !slice.is_empty() { + let result = unsafe { (api.audio_output_data)(FfiByteSlice::new(slice)) }; + let this_time = match result { + neotron_common_bios::FfiResult::Ok(n) => n, + neotron_common_bios::FfiResult::Err(_e) => { + return neotron_api::Result::Err(neotron_api::Error::DeviceSpecific); + } + }; + slice = &slice[this_time..]; + } + neotron_api::Result::Ok(()) + } + OpenHandle::StdIn | OpenHandle::Closed => { + neotron_api::Result::Err(neotron_api::Error::BadHandle) } - neotron_api::Result::Ok(()) - } else { - neotron_api::Result::Err(neotron_api::Error::BadHandle) } } @@ -337,15 +459,41 @@ extern "C" fn api_read( fd: neotron_api::file::Handle, mut buffer: neotron_api::FfiBuffer, ) -> neotron_api::Result { - if fd == neotron_api::file::Handle::new_stdin() { - if let Some(buffer) = buffer.as_mut_slice() { - let count = { crate::STD_INPUT.lock().get_data(buffer) }; - Ok(count).into() - } else { - neotron_api::Result::Err(neotron_api::Error::DeviceSpecific) + let mut open_handles = OPEN_HANDLES.lock(); + let Some(h) = open_handles.get_mut(fd.value() as usize) else { + return neotron_api::Result::Err(neotron_api::Error::BadHandle); + }; + match h { + OpenHandle::StdIn => { + if let Some(buffer) = buffer.as_mut_slice() { + let count = { crate::STD_INPUT.lock().get_data(buffer) }; + Ok(count).into() + } else { + neotron_api::Result::Err(neotron_api::Error::DeviceSpecific) + } + } + OpenHandle::File(f) => { + let Some(buffer) = buffer.as_mut_slice() else { + return neotron_api::Result::Err(neotron_api::Error::InvalidArg); + }; + match f.read(buffer) { + Ok(n) => neotron_api::Result::Ok(n), + Err(_e) => neotron_api::Result::Err(neotron_api::Error::DeviceSpecific), + } + } + OpenHandle::Audio => { + let api = API.get(); + let result = unsafe { (api.audio_input_data)(buffer) }; + match result { + neotron_common_bios::FfiResult::Ok(n) => neotron_api::Result::Ok(n), + neotron_common_bios::FfiResult::Err(_e) => { + neotron_api::Result::Err(neotron_api::Error::DeviceSpecific) + } + } + } + OpenHandle::Stdout | OpenHandle::StdErr | OpenHandle::Closed => { + neotron_api::Result::Err(neotron_api::Error::BadHandle) } - } else { - neotron_api::Result::Err(neotron_api::Error::BadHandle) } } @@ -385,12 +533,84 @@ extern "C" fn api_rename( } /// Perform a special I/O control operation. +/// +/// # Audio Devices +/// +/// * `0` - get output sample rate/format (0xN000_0000_) where N indicates the sample format +/// * N = 0 => Eight bit mono, one byte per sample +/// * N = 1 => Eight bit stereo, two byte per samples +/// * N = 2 => Sixteen bit mono, two byte per samples +/// * N = 3 => Sixteen bit stereo, four byte per samples +/// * `1` - set output sample rate/format +/// * As above +/// * `2` - get output sample space available +/// * Gets a value in bytes extern "C" fn api_ioctl( - _fd: neotron_api::file::Handle, - _command: u64, - _value: u64, + fd: neotron_api::file::Handle, + command: u64, + value: u64, ) -> neotron_api::Result { - neotron_api::Result::Err(neotron_api::Error::Unimplemented) + let mut open_handles = OPEN_HANDLES.lock(); + let Some(h) = open_handles.get_mut(fd.value() as usize) else { + return neotron_api::Result::Err(neotron_api::Error::BadHandle); + }; + let api = API.get(); + match (h, command) { + (OpenHandle::Audio, 0) => { + // Getting sample rate + let neotron_common_bios::FfiResult::Ok(config) = (api.audio_output_get_config)() else { + return neotron_api::Result::Err(neotron_api::Error::DeviceSpecific); + }; + let mut result: u64 = config.sample_rate_hz as u64; + let nibble = match config.sample_format.make_safe() { + Ok(neotron_common_bios::audio::SampleFormat::EightBitMono) => 0, + Ok(neotron_common_bios::audio::SampleFormat::EightBitStereo) => 1, + Ok(neotron_common_bios::audio::SampleFormat::SixteenBitMono) => 2, + Ok(neotron_common_bios::audio::SampleFormat::SixteenBitStereo) => 3, + _ => { + return neotron_api::Result::Err(neotron_api::Error::DeviceSpecific); + } + }; + result |= nibble << 60; + neotron_api::Result::Ok(result) + } + (OpenHandle::Audio, 1) => { + // Setting sample rate + let sample_rate = value as u32; + let format = match value >> 60 { + 0 => neotron_common_bios::audio::SampleFormat::EightBitMono, + 1 => neotron_common_bios::audio::SampleFormat::EightBitStereo, + 2 => neotron_common_bios::audio::SampleFormat::SixteenBitMono, + 3 => neotron_common_bios::audio::SampleFormat::SixteenBitStereo, + _ => { + return neotron_api::Result::Err(neotron_api::Error::InvalidArg); + } + }; + let config = neotron_common_bios::audio::Config { + sample_format: format.make_ffi_safe(), + sample_rate_hz: sample_rate, + }; + match (api.audio_output_set_config)(config) { + neotron_common_bios::FfiResult::Ok(_) => { + osprintln!("audio {}, {:?}", sample_rate, format); + neotron_api::Result::Ok(0) + } + neotron_common_bios::FfiResult::Err(_) => { + neotron_api::Result::Err(neotron_api::Error::DeviceSpecific) + } + } + } + (OpenHandle::Audio, 2) => { + // Setting sample space + match (api.audio_output_get_space)() { + neotron_common_bios::FfiResult::Ok(n) => neotron_api::Result::Ok(n as u64), + neotron_common_bios::FfiResult::Err(_) => { + neotron_api::Result::Err(neotron_api::Error::DeviceSpecific) + } + } + } + _ => neotron_api::Result::Err(neotron_api::Error::InvalidArg), + } } /// Open a directory, given a path as a UTF-8 string. @@ -421,9 +641,35 @@ extern "C" fn api_stat( /// Get information about an open file extern "C" fn api_fstat( - _fd: neotron_api::file::Handle, + fd: neotron_api::file::Handle, ) -> neotron_api::Result { - neotron_api::Result::Err(neotron_api::Error::Unimplemented) + let mut open_handles = OPEN_HANDLES.lock(); + match open_handles.get_mut(fd.value() as usize) { + Some(OpenHandle::File(f)) => { + let stat = neotron_api::file::Stat { + file_size: f.length() as u64, + ctime: neotron_api::file::Time { + year_since_1970: 0, + zero_indexed_month: 0, + zero_indexed_day: 0, + hours: 0, + minutes: 0, + seconds: 0, + }, + mtime: neotron_api::file::Time { + year_since_1970: 0, + zero_indexed_month: 0, + zero_indexed_day: 0, + hours: 0, + minutes: 0, + seconds: 0, + }, + attr: neotron_api::file::Attributes::empty(), + }; + neotron_api::Result::Ok(stat) + } + _ => neotron_api::Result::Err(neotron_api::Error::InvalidArg), + } } /// Delete a file.