From a6b822f5eeb0f023621c016ee58da18dbd7e2211 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 22 Jul 2023 21:58:28 +0100 Subject: [PATCH 01/15] Clean up console handling. Avoids panics if serial console is configured but BIOS returns errors. --- src/lib.rs | 306 ++++++++++++++++++++++++++----------------------- src/program.rs | 3 +- src/refcell.rs | 11 -- 3 files changed, 164 insertions(+), 156 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e68ed78..52ff0ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ // Modules and Imports // =========================================================================== -use core::sync::atomic::{AtomicBool, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use neotron_common_bios as bios; @@ -45,6 +45,12 @@ static VGA_CONSOLE: CsRefCell> = CsRefCell::new(N /// We store our serial console here. static SERIAL_CONSOLE: CsRefCell> = CsRefCell::new(None); +/// Our overall text output console. +/// +/// Writes to the VGA console and/or the serial console (depending on which is +/// configured). +static CONSOLE: Console = Console; + /// Note if we are panicking right now. /// /// If so, don't panic if a serial write fails. @@ -53,6 +59,157 @@ static IS_PANIC: AtomicBool = AtomicBool::new(false); /// Our keyboard controller static STD_INPUT: CsRefCell = CsRefCell::new(StdInput::new()); +// =========================================================================== +// Macros +// =========================================================================== + +/// Prints to the screen +#[macro_export] +macro_rules! osprint { + ($($arg:tt)*) => { + #[allow(unused)] + use core::fmt::Write as _; + let _ = write!(&$crate::CONSOLE, $($arg)*); + } +} + +/// Prints to the screen and puts a new-line on the end +#[macro_export] +macro_rules! osprintln { + () => ($crate::osprint!("\n")); + ($($arg:tt)*) => { + $crate::osprint!($($arg)*); + $crate::osprint!("\n"); + }; +} + +// =========================================================================== +// Local types +// =========================================================================== + +/// Represents the API supplied by the BIOS +struct Api { + bios: AtomicPtr, +} + +impl Api { + /// Create a new object with a null pointer for the BIOS API. + const fn new() -> Api { + Api { + bios: AtomicPtr::new(core::ptr::null_mut()), + } + } + + /// Change the stored BIOS API pointer. + /// + /// The pointed-at object must have static lifetime. + unsafe fn store(&self, api: *const bios::Api) { + self.bios.store(api as *mut bios::Api, Ordering::SeqCst) + } + + /// Get the BIOS API as a reference. + /// + /// Will panic if the stored pointer is null. + fn get(&self) -> &'static bios::Api { + let ptr = self.bios.load(Ordering::SeqCst) as *const bios::Api; + let api_ref = unsafe { ptr.as_ref() }.expect("BIOS API should be non-null"); + api_ref + } + + /// Get the current time + fn get_time(&self) -> chrono::NaiveDateTime { + let api = self.get(); + let bios_time = (api.time_clock_get)(); + let secs = i64::from(bios_time.secs) + SECONDS_BETWEEN_UNIX_AND_NEOTRON_EPOCH; + let nsecs = bios_time.nsecs; + chrono::NaiveDateTime::from_timestamp_opt(secs, nsecs).unwrap() + } + + /// Set the current time + fn set_time(&self, timestamp: chrono::NaiveDateTime) { + let api = self.get(); + let nanos = timestamp.timestamp_nanos(); + let bios_time = bios::Time { + secs: ((nanos / 1_000_000_000) - SECONDS_BETWEEN_UNIX_AND_NEOTRON_EPOCH) as u32, + nsecs: (nanos % 1_000_000_000) as u32, + }; + (api.time_clock_set)(bios_time); + } +} + +/// Represents the serial port we can use as a text input/output device. +struct SerialConsole(u8); + +impl SerialConsole { + /// Write some bytes to the serial console + fn write_bstr(&mut self, mut data: &[u8]) -> Result<(), bios::Error> { + let api = API.get(); + while !data.is_empty() { + let res: Result = (api.serial_write)( + // Which port + self.0, + // Data + bios::FfiByteSlice::new(data), + // No timeout + bios::FfiOption::None, + ) + .into(); + let count = match res { + Ok(n) => n, + Err(_e) => { + // If we can't write to the serial port, let's not break any + // other consoles we might have configured. Instead, just + // quit now and pretend we wrote it all. + return Ok(()); + } + }; + data = &data[count..]; + } + Ok(()) + } + + /// Try and get as many bytes as we can from the serial console. + fn read_data(&mut self, buffer: &mut [u8]) -> Result { + let api = API.get(); + let ffi_buffer = bios::FfiBuffer::new(buffer); + let res = (api.serial_read)( + self.0, + ffi_buffer, + bios::FfiOption::Some(bios::Timeout::new_ms(0)), + ); + res.into() + } +} + +impl core::fmt::Write for SerialConsole { + fn write_str(&mut self, data: &str) -> core::fmt::Result { + self.write_bstr(data.as_bytes()) + .map_err(|_e| core::fmt::Error) + } +} + +/// Represents either or both of the VGA console and the serial console. +struct Console; + +impl core::fmt::Write for &Console { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + if let Ok(mut guard) = VGA_CONSOLE.try_lock() { + if let Some(vga_console) = guard.as_mut() { + vga_console.write_str(s)?; + } + } + + if let Ok(mut guard) = SERIAL_CONSOLE.try_lock() { + if let Some(serial_console) = guard.as_mut() { + serial_console.write_str(s)?; + } + } + + Ok(()) + } +} + +/// Represents the standard input of our console struct StdInput { keyboard: pc_keyboard::EventDecoder, buffer: heapless::spsc::Queue, @@ -161,149 +318,10 @@ impl StdInput { } } -// =========================================================================== -// Macros -// =========================================================================== - -/// Prints to the screen -#[macro_export] -macro_rules! osprint { - ($($arg:tt)*) => { - $crate::VGA_CONSOLE.with(|guard| { - if let Some(console) = guard.as_mut() { - #[allow(unused)] - use core::fmt::Write as _; - write!(console, $($arg)*).unwrap(); - } - }).unwrap(); - $crate::SERIAL_CONSOLE.with(|guard| { - if let Some(console) = guard.as_mut() { - #[allow(unused)] - use core::fmt::Write as _; - write!(console, $($arg)*).unwrap(); - } - }).unwrap(); - }; -} - -/// Prints to the screen and puts a new-line on the end -#[macro_export] -macro_rules! osprintln { - () => ($crate::osprint!("\n")); - ($($arg:tt)*) => { - $crate::osprint!($($arg)*); - $crate::osprint!("\n"); - }; -} - -// =========================================================================== -// Local types -// =========================================================================== - -/// Represents the API supplied by the BIOS -struct Api { - bios: core::sync::atomic::AtomicPtr, -} - -impl Api { - /// Create a new object with a null pointer for the BIOS API. - const fn new() -> Api { - Api { - bios: core::sync::atomic::AtomicPtr::new(core::ptr::null_mut()), - } - } - - /// Change the stored BIOS API pointer. - /// - /// The pointed-at object must have static lifetime. - unsafe fn store(&self, api: *const bios::Api) { - self.bios - .store(api as *mut bios::Api, core::sync::atomic::Ordering::SeqCst) - } - - /// Get the BIOS API as a reference. - /// - /// Will panic if the stored pointer is null. - fn get(&self) -> &'static bios::Api { - let ptr = self.bios.load(core::sync::atomic::Ordering::SeqCst) as *const bios::Api; - let api_ref = unsafe { ptr.as_ref() }.expect("BIOS API should be non-null"); - api_ref - } - - /// Get the current time - fn get_time(&self) -> chrono::NaiveDateTime { - let api = self.get(); - let bios_time = (api.time_clock_get)(); - let secs = i64::from(bios_time.secs) + SECONDS_BETWEEN_UNIX_AND_NEOTRON_EPOCH; - let nsecs = bios_time.nsecs; - chrono::NaiveDateTime::from_timestamp_opt(secs, nsecs).unwrap() - } - - /// Set the current time - fn set_time(&self, timestamp: chrono::NaiveDateTime) { - let api = self.get(); - let nanos = timestamp.timestamp_nanos(); - let bios_time = bios::Time { - secs: ((nanos / 1_000_000_000) - SECONDS_BETWEEN_UNIX_AND_NEOTRON_EPOCH) as u32, - nsecs: (nanos % 1_000_000_000) as u32, - }; - (api.time_clock_set)(bios_time); - } -} - -/// Represents the serial port we can use as a text input/output device. -struct SerialConsole(u8); - -impl SerialConsole { - /// Write some bytes to the serial console - fn write_bstr(&mut self, data: &[u8]) { - let api = API.get(); - let is_panic = IS_PANIC.load(Ordering::Relaxed); - let res = (api.serial_write)( - // Which port - self.0, - // Data - bios::FfiByteSlice::new(data), - // No timeout - bios::FfiOption::None, - ); - if !is_panic { - res.unwrap(); - } - } - - /// Try and get as many bytes as we can from the serial console. - fn read_data(&mut self, buffer: &mut [u8]) -> Result { - let api = API.get(); - let ffi_buffer = bios::FfiBuffer::new(buffer); - let res = (api.serial_read)( - self.0, - ffi_buffer, - bios::FfiOption::Some(bios::Timeout::new_ms(0)), - ); - res.into() - } -} - -impl core::fmt::Write for SerialConsole { - fn write_str(&mut self, data: &str) -> core::fmt::Result { - let api = API.get(); - let is_panic = IS_PANIC.load(Ordering::Relaxed); - let res = (api.serial_write)( - // Which port - self.0, - // Data - bios::FfiByteSlice::new(data.as_bytes()), - // No timeout - bios::FfiOption::None, - ); - if !is_panic { - res.unwrap(); - } - Ok(()) - } -} - +/// Local context used by the main menu. +/// +/// Stuff goes here in preference, but we take it out of here and make it a +/// global if we have to. pub struct Ctx { config: config::Config, tpa: program::TransientProgramArea, diff --git a/src/program.rs b/src/program.rs index 5540438..1313b0e 100644 --- a/src/program.rs +++ b/src/program.rs @@ -325,7 +325,8 @@ extern "C" fn api_write( } let mut guard = crate::SERIAL_CONSOLE.lock(); if let Some(console) = guard.as_mut() { - console.write_bstr(buffer.as_slice()); + // Ignore serial errors on stdout + let _ = console.write_bstr(buffer.as_slice()); } neotron_api::Result::Ok(()) } else { diff --git a/src/refcell.rs b/src/refcell.rs index b9d1969..fd85cf1 100644 --- a/src/refcell.rs +++ b/src/refcell.rs @@ -51,17 +51,6 @@ impl CsRefCell { } } - /// Try and do something with the lock. - pub fn with(&self, f: F) -> Result - where - F: FnOnce(&mut CsRefCellGuard) -> U, - { - let mut guard = self.try_lock()?; - let result = f(&mut guard); - drop(guard); - Ok(result) - } - /// Lock the cell. /// /// If you can't lock it (because it is already locked), this function will panic. From 440219ba1089f6ceac81911cdd60a7a0e9712e19 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Sat, 22 Jul 2023 22:01:25 +0100 Subject: [PATCH 02/15] Fix typo in Cargo.toml. --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40162fa..c1c6b49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,7 +314,7 @@ checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "utf8parse" version = "0.2.1" -source = "git+https://github.com/alacritty/vte#94e74f3a64f42d5dad4e3d42dbe8c23291038214" +source = "git+https://github.com/alacritty/vte?rev=94e74f3a64f42d5dad4e3d42dbe8c23291038214#94e74f3a64f42d5dad4e3d42dbe8c23291038214" [[package]] name = "void" @@ -325,7 +325,7 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "vte" version = "0.11.1" -source = "git+https://github.com/alacritty/vte#94e74f3a64f42d5dad4e3d42dbe8c23291038214" +source = "git+https://github.com/alacritty/vte?rev=94e74f3a64f42d5dad4e3d42dbe8c23291038214#94e74f3a64f42d5dad4e3d42dbe8c23291038214" dependencies = [ "arrayvec", "utf8parse", @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "vte_generate_state_changes" version = "0.1.1" -source = "git+https://github.com/alacritty/vte#94e74f3a64f42d5dad4e3d42dbe8c23291038214" +source = "git+https://github.com/alacritty/vte?rev=94e74f3a64f42d5dad4e3d42dbe8c23291038214#94e74f3a64f42d5dad4e3d42dbe8c23291038214" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ad3c4e6..621355d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ chrono = { version = "0.4", default-features = false } embedded-sdmmc = { version = "0.5", default-features = false } neotron-api = "0.1" neotron-loader = "0.1" -vte = { git = "https://github.com/alacritty/vte", commit="94e74f3a64f42d5dad4e3d42dbe8c23291038214" } +vte = { git = "https://github.com/alacritty/vte", rev = "94e74f3a64f42d5dad4e3d42dbe8c23291038214" } [features] lib-mode = [] From 789fd34c90fb48d836bfb965659804fc56e38f13 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Wed, 2 Aug 2023 22:51:07 +0100 Subject: [PATCH 03/15] Remove most screen commands. They can be apps now. Rename clear to "cls". --- src/commands/mod.rs | 5 +- src/commands/screen.rs | 150 +++-------------------------------------- 2 files changed, 9 insertions(+), 146 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8dd58cc..f5e2447 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -26,10 +26,7 @@ pub static OS_MENU: menu::Menu = menu::Menu { &ram::RUN_ITEM, &ram::LOAD_ITEM, &fs::LOAD_ITEM, - &screen::CLEAR_ITEM, - &screen::BENCH_ITEM, - &screen::FILL_ITEM, - &screen::MANDEL_ITEM, + &screen::CLS_ITEM, &input::KBTEST_ITEM, &hardware::SHUTDOWN_ITEM, ], diff --git a/src/commands/screen.rs b/src/commands/screen.rs index a27c45a..79d664b 100644 --- a/src/commands/screen.rs +++ b/src/commands/screen.rs @@ -1,152 +1,18 @@ //! Screen-related commands for Neotron OS -use neotron_common_bios::video::{Attr, TextBackgroundColour, TextForegroundColour}; +use crate::{osprint, Ctx}; -use crate::{osprint, osprintln, Ctx, API, VGA_CONSOLE}; - -pub static CLEAR_ITEM: menu::Item = menu::Item { +pub static CLS_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { - function: clear, + function: cls, parameters: &[], }, - command: "screen_clear", + command: "cls", help: Some("Clear the screen"), }; -pub static FILL_ITEM: menu::Item = menu::Item { - item_type: menu::ItemType::Callback { - function: fill, - parameters: &[], - }, - command: "screen_fill", - help: Some("Fill the screen with characters"), -}; - -pub static BENCH_ITEM: menu::Item = menu::Item { - item_type: menu::ItemType::Callback { - function: bench, - parameters: &[], - }, - command: "screen_bench", - help: Some("Time how long to put 1,000,000 characters on the screen, with scrolling."), -}; - -pub static MANDEL_ITEM: menu::Item = menu::Item { - item_type: menu::ItemType::Callback { - function: mandel, - parameters: &[], - }, - command: "screen_mandel", - help: Some("Calculate the Mandelbrot set"), -}; - -/// Called when the "clear" command is executed. -fn clear(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { - let mut guard = VGA_CONSOLE.try_lock().unwrap(); - if let Some(vga_console) = guard.as_mut() { - vga_console.clear(); - } -} - -/// Called when the "fill" command is executed. -fn fill(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { - let mut guard = VGA_CONSOLE.try_lock().unwrap(); - if let Some(console) = guard.as_mut() { - console.clear(); - let api = API.get(); - let mode = (api.video_get_mode)(); - let (Some(width), Some(height)) = (mode.text_width(), mode.text_height()) else { - osprintln!("Unable to get console size"); - return; - }; - // A range of printable ASCII compatible characters - let mut char_cycle = (b' '..=b'~').cycle(); - let mut remaining = height * width; - - // Scroll two screen fulls - 'outer: for bg in (0..=7).cycle() { - let bg_colour = TextBackgroundColour::new(bg).unwrap(); - for fg in 1..=15 { - if fg == bg { - continue; - } - let fg_colour = TextForegroundColour::new(fg).unwrap(); - remaining -= 1; - if remaining == 0 { - break 'outer; - } - let attr = Attr::new(fg_colour, bg_colour, false); - let glyph = char_cycle.next().unwrap(); - console.set_attr(attr); - console.write_bstr(&[glyph]); - } - } - let attr = Attr::new( - TextForegroundColour::WHITE, - TextBackgroundColour::BLACK, - false, - ); - console.set_attr(attr); - } -} - -/// Called when the "bench" command is executed. -fn bench(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { - const NUM_CHARS: u64 = 1_000_000; - let mut guard = VGA_CONSOLE.try_lock().unwrap(); - if let Some(console) = guard.as_mut() { - let api = API.get(); - let start = (api.time_ticks_get)(); - console.clear(); - let glyphs = &[b'x']; - for _idx in 0..NUM_CHARS { - console.write_bstr(glyphs); - } - let end = (api.time_ticks_get)(); - let delta = end.0 - start.0; - let chars_per_second = (NUM_CHARS * (api.time_ticks_per_second)().0) / delta; - osprintln!( - "{} chars in {} ticks, or {} chars per second", - NUM_CHARS, - delta, - chars_per_second - ); - } -} - -/// Called when the "mandel" command is executed. -fn mandel(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { - fn mandelbrot(cx: f64, cy: f64, max_loops: u32) -> u32 { - let mut x = cx; - let mut y = cy; - for i in 1..max_loops { - let x_squared = x * x; - let y_squared = y * y; - if x_squared + y_squared > 4.0 { - return i; - } - let x1 = x_squared - y_squared + cx; - let y1 = (2.0 * x * y) + cy; - x = x1; - y = y1; - } - 0 - } - - let api = API.get(); - let mode = (api.video_get_mode)(); - let (Some(width), Some(height)) = (mode.text_width(), mode.text_height()) else { - osprintln!("Unable to get screen size"); - return; - }; - - let glyphs = b" .,'~!^:;[/<&?oxOX# "; - for y_pos in 0..height - 2 { - let y = (f64::from(y_pos) * 4.0 / f64::from(height)) - 2.0; - for x_pos in 0..width { - let x = (f64::from(x_pos) * 4.0 / f64::from(width)) - 2.0; - let result = mandelbrot(x, y, 20); - osprint!("{}", glyphs[result as usize] as char); - } - } +/// Called when the "cls" command is executed. +fn cls(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { + // Reset SGR, go home, clear screen, + let _ = osprint!("\u{001b}[0m\u{001b}[1;1H\u{001b}[2J"); } From 4a4447117c622cc5922380b0bb5ca6a7045664be Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Wed, 2 Aug 2023 22:51:27 +0100 Subject: [PATCH 04/15] Fix use of osprint. --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 52ff0ac..626445a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,11 +66,11 @@ static STD_INPUT: CsRefCell = CsRefCell::new(StdInput::new()); /// Prints to the screen #[macro_export] macro_rules! osprint { - ($($arg:tt)*) => { + ($($arg:tt)*) => { { #[allow(unused)] use core::fmt::Write as _; let _ = write!(&$crate::CONSOLE, $($arg)*); - } + } } } /// Prints to the screen and puts a new-line on the end From 82be20714e02fc68e2cc2a00b50d49a168a06818 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Wed, 2 Aug 2023 22:51:47 +0100 Subject: [PATCH 05/15] Add mixer command. Only reads mixer values. --- src/commands/mod.rs | 2 ++ src/commands/sound.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/commands/sound.rs diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f5e2447..e3360bb 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -11,6 +11,7 @@ mod hardware; mod input; mod ram; mod screen; +mod sound; mod timedate; pub static OS_MENU: menu::Menu = menu::Menu { @@ -29,6 +30,7 @@ pub static OS_MENU: menu::Menu = menu::Menu { &screen::CLS_ITEM, &input::KBTEST_ITEM, &hardware::SHUTDOWN_ITEM, + &sound::MIXER_ITEM, ], entry: None, exit: None, diff --git a/src/commands/sound.rs b/src/commands/sound.rs new file mode 100644 index 0000000..f79790d --- /dev/null +++ b/src/commands/sound.rs @@ -0,0 +1,41 @@ +//! Sound related commands for Neotron OS + +use crate::{osprintln, Ctx, API}; + +pub static MIXER_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: mixer, + parameters: &[], + }, + command: "mixer", + help: Some("Control the audio mixer"), +}; + +/// Called when the "mixer" command is executed. +fn mixer(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { + let api = API.get(); + osprintln!("Mixers:"); + for mixer_id in 0u8..=255u8 { + match (api.audio_mixer_channel_get_info)(mixer_id) { + neotron_common_bios::FfiOption::Some(mixer_info) => { + let dir_str = match mixer_info.direction { + neotron_common_bios::audio::Direction::Input => "In", + neotron_common_bios::audio::Direction::Loopback => "Loop", + neotron_common_bios::audio::Direction::Output => "Out", + }; + osprintln!( + "#{}: {} ({}) {}/{}", + mixer_id, + mixer_info.name, + dir_str, + mixer_info.current_level, + mixer_info.max_level + ); + } + neotron_common_bios::FfiOption::None => { + // Run out of mixers + break; + } + } + } +} From 8b9de6cd3b4c459d3915e5b3358ebe4ee59c35f4 Mon Sep 17 00:00:00 2001 From: Jonathan 'theJPster' Pallant Date: Wed, 9 Aug 2023 17:05:56 +0100 Subject: [PATCH 06/15] Add raw PCM player. --- src/commands/mod.rs | 1 + src/commands/sound.rs | 149 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 139 insertions(+), 11 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e3360bb..782fc17 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -31,6 +31,7 @@ pub static OS_MENU: menu::Menu = menu::Menu { &input::KBTEST_ITEM, &hardware::SHUTDOWN_ITEM, &sound::MIXER_ITEM, + &sound::PLAY_ITEM, ], entry: None, exit: None, diff --git a/src/commands/sound.rs b/src/commands/sound.rs index f79790d..a581e74 100644 --- a/src/commands/sound.rs +++ b/src/commands/sound.rs @@ -1,19 +1,86 @@ //! Sound related commands for Neotron OS -use crate::{osprintln, Ctx, API}; +use crate::{osprint, osprintln, Ctx, API}; pub static MIXER_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { function: mixer, - parameters: &[], + parameters: &[ + menu::Parameter::Optional { + parameter_name: "mixer", + help: Some("Which mixer to adjust"), + }, + menu::Parameter::Optional { + parameter_name: "level", + help: Some("New level for this mixer, as an integer."), + }, + ], }, command: "mixer", help: Some("Control the audio mixer"), }; +pub static PLAY_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: play, + parameters: &[menu::Parameter::Mandatory { + parameter_name: "filename", + help: Some("Which file to play"), + }], + }, + command: "play", + help: Some("Play a raw 16-bit LE 48 kHz stereo file"), +}; + /// Called when the "mixer" command is executed. -fn mixer(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { +fn mixer(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { + let selected_mixer = menu::argument_finder(item, args, "mixer").unwrap(); + let level_str = menu::argument_finder(item, args, "level").unwrap(); + + let level_int = if let Some(level_str) = level_str { + let Ok(value) = level_str.parse::() else { + osprintln!("{} is not an integer", level_str); + return; + }; + Some(value) + } else { + None + }; + let api = API.get(); + + if let (Some(selected_mixer), Some(level_int)) = (selected_mixer, level_int) { + let mut found = false; + for mixer_id in 0u8..=255u8 { + match (api.audio_mixer_channel_get_info)(mixer_id) { + neotron_common_bios::FfiOption::Some(mixer_info) => { + if mixer_info.name.as_str() == selected_mixer { + if let Err(e) = + (api.audio_mixer_channel_set_level)(mixer_id, level_int).into() + { + osprintln!( + "Failed to set mixer {:?} (id {}) to {}: {:?}", + selected_mixer, + mixer_id, + level_int, + e + ); + } + found = true; + break; + } + } + neotron_common_bios::FfiOption::None => { + break; + } + } + } + + if !found { + osprintln!("Don't know mixer {:?}", selected_mixer); + } + } + osprintln!("Mixers:"); for mixer_id in 0u8..=255u8 { match (api.audio_mixer_channel_get_info)(mixer_id) { @@ -23,14 +90,19 @@ fn mixer(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: neotron_common_bios::audio::Direction::Loopback => "Loop", neotron_common_bios::audio::Direction::Output => "Out", }; - osprintln!( - "#{}: {} ({}) {}/{}", - mixer_id, - mixer_info.name, - dir_str, - mixer_info.current_level, - mixer_info.max_level - ); + if selected_mixer + .and_then(|s| Some(s == mixer_info.name.as_str())) + .unwrap_or(true) + { + osprintln!( + "#{}: {} ({}) {}/{}", + mixer_id, + mixer_info.name, + dir_str, + mixer_info.current_level, + mixer_info.max_level + ); + } } neotron_common_bios::FfiOption::None => { // Run out of mixers @@ -39,3 +111,58 @@ fn mixer(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: } } } + +/// Called when the "play" command is executed. +fn play(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { + fn play_inner( + file_name: &str, + scratch: &mut [u8], + ) -> Result<(), embedded_sdmmc::Error> { + osprintln!("Loading /{} from Block Device 0", file_name); + 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 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 api = API.get(); + + let mut buffer = &mut scratch[0..4096]; + let mut bytes = 0; + let mut delta = 0; + + while !file.eof() { + let bytes_read = mgr.read(&mut volume, &mut file, &mut buffer)?; + let mut buffer = &buffer[0..bytes_read]; + while !buffer.is_empty() { + let slice = neotron_common_bios::FfiByteSlice::new(buffer); + let played = unsafe { (api.audio_output_data)(slice).unwrap() }; + buffer = &buffer[played..]; + delta += played; + if delta > 48000 { + bytes += delta; + delta = 0; + let milliseconds = bytes / ((48000 / 1000) * 4); + osprint!( + "\rPlayed: {}:{} ms", + milliseconds / 1000, + milliseconds % 1000 + ); + } + } + } + osprintln!(); + Ok(()) + } + + if let Err(e) = play_inner(args[0], ctx.tpa.as_slice_u8()) { + osprintln!("\nError during playback: {:?}", e); + } +} From c87c26623b62a7101af7528950316d4f1d577b4b Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sun, 1 Oct 2023 15:30:23 +0100 Subject: [PATCH 07/15] Can list and change video modes. --- Cargo.lock | 3 +- Cargo.toml | 2 +- src/commands/mod.rs | 1 + src/commands/screen.rs | 106 +++++++++++++++++++++++++++++++++++++++-- src/vgaconsole.rs | 11 +++++ 5 files changed, 116 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1c6b49..93d96fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,8 +150,7 @@ dependencies = [ [[package]] name = "neotron-common-bios" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9adad98cd2c761f72321c516359f53b8e0cd56637ee6332e7d8628d8144cad3a" +source = "git+https://github.com/neotron-compute/neotron-common-bios?branch=make-things-inline#c569e0303cd98d40cbd7cdf1ca5a7145b1763330" dependencies = [ "chrono", "neotron-ffi", diff --git a/Cargo.toml b/Cargo.toml index 621355d..b2d55a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ panic = "abort" panic = "abort" [dependencies] -neotron-common-bios = "0.11" +neotron-common-bios = { git = "https://github.com/neotron-compute/neotron-common-bios", branch = "make-things-inline" } pc-keyboard = "0.7" r0 = "1.0" heapless = "0.7" diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 782fc17..ce918f6 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -28,6 +28,7 @@ pub static OS_MENU: menu::Menu = menu::Menu { &ram::LOAD_ITEM, &fs::LOAD_ITEM, &screen::CLS_ITEM, + &screen::MODE_ITEM, &input::KBTEST_ITEM, &hardware::SHUTDOWN_ITEM, &sound::MIXER_ITEM, diff --git a/src/commands/screen.rs b/src/commands/screen.rs index 79d664b..8a3745f 100644 --- a/src/commands/screen.rs +++ b/src/commands/screen.rs @@ -1,18 +1,116 @@ //! Screen-related commands for Neotron OS -use crate::{osprint, Ctx}; +use crate::{osprint, osprintln, Ctx}; +use neotron_common_bios::{ + video::{Format, Mode}, + ApiResult, +}; pub static CLS_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { - function: cls, + function: cls_cmd, parameters: &[], }, command: "cls", help: Some("Clear the screen"), }; +pub static MODE_ITEM: menu::Item = menu::Item { + item_type: menu::ItemType::Callback { + function: mode_cmd, + parameters: &[menu::Parameter::Optional { + parameter_name: "new_mode", + help: Some("The new text mode to change to"), + }], + }, + command: "mode", + help: Some("List possible video modes"), +}; + /// Called when the "cls" command is executed. -fn cls(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { +fn cls_cmd(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { // Reset SGR, go home, clear screen, - let _ = osprint!("\u{001b}[0m\u{001b}[1;1H\u{001b}[2J"); + osprint!("\u{001b}[0m\u{001b}[1;1H\u{001b}[2J"); } + +/// Called when the "mode" command is executed +fn mode_cmd(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { + if let Some(new_mode) = menu::argument_finder(item, args, "new_mode").unwrap() { + let Ok(mode_num) = new_mode.parse::() else { + osprintln!("Invalid integer {:?}", new_mode); + return; + }; + let Some(mode) = Mode::try_from_u8(mode_num) else { + osprintln!("Invalid mode {:?}", new_mode); + return; + }; + let has_vga = { + let mut guard = crate::VGA_CONSOLE.lock(); + guard.as_mut().is_some() + }; + if !has_vga { + osprintln!("No VGA console."); + return; + } + let api = crate::API.get(); + match mode.format() { + Format::Text8x16 => {} + Format::Text8x8 => {} + _ => { + osprintln!("Not a text mode?"); + return; + } + } + match (api.video_set_mode)(mode) { + ApiResult::Ok(_) => { + let mut guard = crate::VGA_CONSOLE.lock(); + if let Some(console) = guard.as_mut() { + console.change_mode(mode); + } + osprintln!("Now in mode {}", mode.as_u8()); + } + ApiResult::Err(e) => { + osprintln!("Failed to change mode: {:?}", e); + } + } + } else { + print_modes(); + } +} + +/// Print out all supported video modes +fn print_modes() { + let api = crate::API.get(); + let current_mode = (api.video_get_mode)(); + let mut any_mode = false; + for mode_no in 0..255 { + // Note (unsafe): we'll test if it's right before we try and use it + let Some(m) = Mode::try_from_u8(mode_no) else { + continue; + }; + let is_supported = (api.video_is_valid_mode)(m); + if is_supported { + any_mode = true; + let is_current = if current_mode == m { "*" } else { " " }; + let text_rows = m.text_height(); + let text_cols = m.text_width(); + let f = m.format(); + let width = m.horizontal_pixels(); + let height = m.vertical_lines(); + let hz = m.frame_rate_hz(); + if let (Some(text_rows), Some(text_cols)) = (text_rows, text_cols) { + // It's a text mode + osprintln!("{mode_no:3}{is_current}: {width} x {height} @ {hz} Hz {f} ({text_cols} x {text_rows})"); + } else { + // It's a framebuffer mode + let f = m.format(); + osprintln!("{mode_no:3}{is_current}: {width} x {height} @ {hz} Hz {f}"); + } + } + } + if !any_mode { + osprintln!("No valid modes found"); + } +} + +// End of file diff --git a/src/vgaconsole.rs b/src/vgaconsole.rs index 7bb7354..278fcd1 100644 --- a/src/vgaconsole.rs +++ b/src/vgaconsole.rs @@ -71,6 +71,17 @@ impl VgaConsole { } } + /// Change the video mode + /// + /// Non text modes are ignored. + pub fn change_mode(&mut self, mode: neotron_common_bios::video::Mode) { + if let (Some(height), Some(width)) = (mode.text_height(), mode.text_width()) { + self.inner.height = height as isize; + self.inner.width = width as isize; + self.clear(); + } + } + /// Clear the screen. /// /// Every character on the screen is replaced with an space (U+0020). From 9161fd74b34fb4f1eca5a2bd792c19124d4b149d Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sun, 1 Oct 2023 19:49:34 +0100 Subject: [PATCH 08/15] store VGA mode in config. --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- src/commands/config.rs | 34 +++++++++++++++++++++++++--------- src/config.rs | 12 ++++++------ src/lib.rs | 15 +++++++-------- 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93d96fb..cfe5990 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,8 +149,9 @@ dependencies = [ [[package]] name = "neotron-common-bios" -version = "0.11.0" -source = "git+https://github.com/neotron-compute/neotron-common-bios?branch=make-things-inline#c569e0303cd98d40cbd7cdf1ca5a7145b1763330" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78126c13b56ba6f62487db499cff62b0761477bd3ab6a1aa13affc03663bc276" dependencies = [ "chrono", "neotron-ffi", diff --git a/Cargo.toml b/Cargo.toml index b2d55a8..b95ae6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ panic = "abort" panic = "abort" [dependencies] -neotron-common-bios = { git = "https://github.com/neotron-compute/neotron-common-bios", branch = "make-things-inline" } +neotron-common-bios = "0.11.1" pc-keyboard = "0.7" r0 = "1.0" heapless = "0.7" diff --git a/src/commands/config.rs b/src/commands/config.rs index 54e37fe..f5aeea9 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -1,6 +1,6 @@ //! Configuration related commands for Neotron OS -use crate::{config, osprintln, Ctx}; +use crate::{bios, config, osprintln, Ctx}; pub static COMMAND_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -42,16 +42,25 @@ fn command(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: } }, "vga" => match args.get(1).cloned() { - Some("on") => { - ctx.config.set_vga_console(true); - osprintln!("VGA now on"); - } Some("off") => { - ctx.config.set_vga_console(false); + ctx.config.set_vga_console(None); osprintln!("VGA now off"); } + Some(mode_str) => { + let Some(mode) = mode_str + .parse::() + .ok() + .and_then(bios::video::Mode::try_from_u8) + .filter(|m| m.is_text_mode()) + else { + osprintln!("Not a valid text mode"); + return; + }; + ctx.config.set_vga_console(Some(mode)); + osprintln!("VGA set to mode {}", mode.as_u8()); + } _ => { - osprintln!("Give on or off as argument"); + osprintln!("Give integer or off as argument"); } }, "serial" => match (args.get(1).cloned(), args.get(1).map(|s| s.parse::())) { @@ -68,7 +77,14 @@ fn command(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: } }, "print" => { - osprintln!("VGA : {}", ctx.config.get_vga_console()); + match ctx.config.get_vga_console() { + Some(m) => { + osprintln!("VGA : Mode {}", m.as_u8()); + } + None => { + osprintln!("VGA : off"); + } + }; match ctx.config.get_serial_console() { None => { osprintln!("Serial: off"); @@ -83,7 +99,7 @@ fn command(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: osprintln!("config help - print this help text"); osprintln!("config reset - load config from BIOS store"); osprintln!("config save - save config to BIOS store"); - osprintln!("config vga on - turn VGA on"); + osprintln!("config vga - enable VGA in Mode "); osprintln!("config vga off - turn VGA off"); osprintln!("config serial off - turn serial console off"); osprintln!("config serial - turn serial console on with given baud rate"); diff --git a/src/config.rs b/src/config.rs index 1d77e69..c7a71f5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; /// Represents our configuration information that we ask the BIOS to serialise #[derive(Debug, Serialize, Deserialize)] pub struct Config { - vga_console: bool, + vga_console: Option, serial_console: bool, serial_baud: u32, } @@ -39,13 +39,13 @@ impl Config { } /// Should this system use the VGA console? - pub fn get_vga_console(&self) -> bool { - self.vga_console + pub fn get_vga_console(&self) -> Option { + self.vga_console.and_then(bios::video::Mode::try_from_u8) } // Set whether this system should use the VGA console. - pub fn set_vga_console(&mut self, new_value: bool) { - self.vga_console = new_value; + pub fn set_vga_console(&mut self, new_value: Option) { + self.vga_console = new_value.map(|m| m.as_u8()); } /// Should this system use the UART console? @@ -82,7 +82,7 @@ impl Config { impl core::default::Default for Config { fn default() -> Config { Config { - vga_console: true, + vga_console: Some(0), serial_console: false, serial_baud: 115200, } diff --git a/src/lib.rs b/src/lib.rs index 626445a..5781905 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -376,20 +376,19 @@ pub extern "C" fn os_main(api: &bios::Api) -> ! { } let api = API.get(); - if (api.api_version_get)() != neotron_common_bios::API_VERSION { + if (api.api_version_get)() != bios::API_VERSION { panic!("API mismatch!"); } let config = config::Config::load().unwrap_or_default(); - if config.get_vga_console() { - // Try and set 80x30 mode for maximum compatibility - (api.video_set_mode)(bios::video::Mode::new( - bios::video::Timing::T640x480, - bios::video::Format::Text8x16, - )); + if let Some(mut mode) = config.get_vga_console() { + // Set the configured mode + if let bios::FfiResult::Err(_e) = (api.video_set_mode)(mode) { + // Failed to change mode - check what mode we're in + mode = (api.video_get_mode)(); + }; // Work with whatever we get - let mode = (api.video_get_mode)(); let (width, height) = (mode.text_width(), mode.text_height()); if let (Some(width), Some(height)) = (width, height) { From 5bcebf21b34b228383129ddc06d0348a51f38a64 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sun, 1 Oct 2023 19:49:41 +0100 Subject: [PATCH 09/15] Clean up imports. --- src/commands/screen.rs | 10 ++++++---- src/commands/sound.rs | 20 ++++++++++---------- src/fs.rs | 3 +-- src/program.rs | 19 +++++++++---------- src/vgaconsole.rs | 14 ++------------ 5 files changed, 28 insertions(+), 38 deletions(-) diff --git a/src/commands/screen.rs b/src/commands/screen.rs index 8a3745f..23b6283 100644 --- a/src/commands/screen.rs +++ b/src/commands/screen.rs @@ -1,9 +1,11 @@ //! Screen-related commands for Neotron OS -use crate::{osprint, osprintln, Ctx}; -use neotron_common_bios::{ - video::{Format, Mode}, - ApiResult, +use crate::{ + bios::{ + video::{Format, Mode}, + ApiResult, + }, + osprint, osprintln, Ctx, }; pub static CLS_ITEM: menu::Item = menu::Item { diff --git a/src/commands/sound.rs b/src/commands/sound.rs index a581e74..a58ee65 100644 --- a/src/commands/sound.rs +++ b/src/commands/sound.rs @@ -1,6 +1,6 @@ //! Sound related commands for Neotron OS -use crate::{osprint, osprintln, Ctx, API}; +use crate::{bios, osprint, osprintln, Ctx, API}; pub static MIXER_ITEM: menu::Item = menu::Item { item_type: menu::ItemType::Callback { @@ -53,7 +53,7 @@ fn mixer(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: & let mut found = false; for mixer_id in 0u8..=255u8 { match (api.audio_mixer_channel_get_info)(mixer_id) { - neotron_common_bios::FfiOption::Some(mixer_info) => { + bios::FfiOption::Some(mixer_info) => { if mixer_info.name.as_str() == selected_mixer { if let Err(e) = (api.audio_mixer_channel_set_level)(mixer_id, level_int).into() @@ -70,7 +70,7 @@ fn mixer(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: & break; } } - neotron_common_bios::FfiOption::None => { + bios::FfiOption::None => { break; } } @@ -84,11 +84,11 @@ fn mixer(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: & osprintln!("Mixers:"); for mixer_id in 0u8..=255u8 { match (api.audio_mixer_channel_get_info)(mixer_id) { - neotron_common_bios::FfiOption::Some(mixer_info) => { + bios::FfiOption::Some(mixer_info) => { let dir_str = match mixer_info.direction { - neotron_common_bios::audio::Direction::Input => "In", - neotron_common_bios::audio::Direction::Loopback => "Loop", - neotron_common_bios::audio::Direction::Output => "Out", + bios::audio::Direction::Input => "In", + bios::audio::Direction::Loopback => "Loop", + bios::audio::Direction::Output => "Out", }; if selected_mixer .and_then(|s| Some(s == mixer_info.name.as_str())) @@ -104,7 +104,7 @@ fn mixer(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: & ); } } - neotron_common_bios::FfiOption::None => { + bios::FfiOption::None => { // Run out of mixers break; } @@ -117,7 +117,7 @@ fn play(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &m fn play_inner( file_name: &str, scratch: &mut [u8], - ) -> Result<(), embedded_sdmmc::Error> { + ) -> Result<(), embedded_sdmmc::Error> { osprintln!("Loading /{} from Block Device 0", file_name); let bios_block = crate::fs::BiosBlock(); let time = crate::fs::BiosTime(); @@ -142,7 +142,7 @@ fn play(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &m let bytes_read = mgr.read(&mut volume, &mut file, &mut buffer)?; let mut buffer = &buffer[0..bytes_read]; while !buffer.is_empty() { - let slice = neotron_common_bios::FfiByteSlice::new(buffer); + let slice = bios::FfiByteSlice::new(buffer); let played = unsafe { (api.audio_output_data)(slice).unwrap() }; buffer = &buffer[played..]; delta += played; diff --git a/src/fs.rs b/src/fs.rs index 12c7afd..af7bb5f 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,9 +1,8 @@ //! Filesystem related types use chrono::{Datelike, Timelike}; -use neotron_common_bios as bios; -use crate::API; +use crate::{bios, API}; pub struct BiosBlock(); diff --git a/src/program.rs b/src/program.rs index 1313b0e..1784aff 100644 --- a/src/program.rs +++ b/src/program.rs @@ -3,6 +3,7 @@ use embedded_sdmmc::File; use crate::{ + bios, fs::{BiosBlock, BiosTime}, osprint, osprintln, }; @@ -38,23 +39,21 @@ pub enum Error { /// The file was too large for RAM. ProgramTooLarge, /// A filesystem error occurred - Filesystem(embedded_sdmmc::Error), + Filesystem(embedded_sdmmc::Error), /// An ELF error occurred - Elf(neotron_loader::Error>), + Elf(neotron_loader::Error>), /// Tried to run when nothing was loaded NothingLoaded, } -impl From> for Error { - fn from(value: embedded_sdmmc::Error) -> Self { +impl From> for Error { + fn from(value: embedded_sdmmc::Error) -> Self { Error::Filesystem(value) } } -impl From>> for Error { - fn from( - value: neotron_loader::Error>, - ) -> Self { +impl From>> for Error { + fn from(value: neotron_loader::Error>) -> Self { Error::Elf(value) } } @@ -89,7 +88,7 @@ impl FileSource { &self, offset: u32, out_buffer: &mut [u8], - ) -> Result<(), embedded_sdmmc::Error> { + ) -> Result<(), embedded_sdmmc::Error> { osprintln!("Reading from {}", offset); self.file.borrow_mut().seek_from_start(offset).unwrap(); self.mgr @@ -100,7 +99,7 @@ impl FileSource { } impl neotron_loader::traits::Source for &FileSource { - type Error = embedded_sdmmc::Error; + type Error = embedded_sdmmc::Error; fn read(&self, mut offset: u32, out_buffer: &mut [u8]) -> Result<(), Self::Error> { for chunk in out_buffer.chunks_mut(FileSource::BUFFER_LEN) { diff --git a/src/vgaconsole.rs b/src/vgaconsole.rs index 278fcd1..695bf43 100644 --- a/src/vgaconsole.rs +++ b/src/vgaconsole.rs @@ -24,7 +24,7 @@ // Modules and Imports // =========================================================================== -use neotron_common_bios::video::{Attr, TextBackgroundColour, TextForegroundColour}; +use crate::bios::video::{Attr, Mode, TextBackgroundColour, TextForegroundColour}; // =========================================================================== // Global Variables @@ -74,7 +74,7 @@ impl VgaConsole { /// Change the video mode /// /// Non text modes are ignored. - pub fn change_mode(&mut self, mode: neotron_common_bios::video::Mode) { + pub fn change_mode(&mut self, mode: Mode) { if let (Some(height), Some(width)) = (mode.text_height(), mode.text_width()) { self.inner.height = height as isize; self.inner.width = width as isize; @@ -91,11 +91,6 @@ impl VgaConsole { self.inner.cursor_enable(); } - /// Set the default attribute for any future text. - pub fn set_attr(&mut self, attr: Attr) { - self.inner.set_attr(attr); - } - /// Write a UTF-8 byte string to the console. /// /// Is parsed for ANSI codes, and Unicode is converted to Code Page 850 for @@ -225,11 +220,6 @@ impl ConsoleInner { self.home(); } - /// Set the default attribute for any future text. - fn set_attr(&mut self, attr: Attr) { - self.attr = attr; - } - /// Put a glyph at the current position on the screen. /// /// Don't do this if the cursor is enabled. From e5b1feab4d4db454ebe1d71e47a4190f8fc619c6 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sun, 1 Oct 2023 19:53:58 +0100 Subject: [PATCH 10/15] Update CHANGELOG --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c56f13..c94ed7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ ## Unreleased changes -* None +* Can set/set video mode +* Stores video mode as part of config +* Removed demo commands (they should be applications) +* Added raw PCM sound playback +* Added mixer command +* Switch to neotron-common-bios 0.11.1 ## v0.5.0 From d5ce07f9cb79b2cfa39365def1d85c0245bd826c Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sun, 1 Oct 2023 20:10:25 +0100 Subject: [PATCH 11/15] Can pause sound playback. --- src/commands/sound.rs | 48 ++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/commands/sound.rs b/src/commands/sound.rs index a58ee65..aafef5f 100644 --- a/src/commands/sound.rs +++ b/src/commands/sound.rs @@ -132,29 +132,53 @@ fn play(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &m embedded_sdmmc::Mode::ReadOnly, )?; + osprintln!("Press Q to quit, P to pause/unpause..."); + let api = API.get(); let mut buffer = &mut scratch[0..4096]; let mut bytes = 0; let mut delta = 0; - while !file.eof() { - let bytes_read = mgr.read(&mut volume, &mut file, &mut buffer)?; - let mut buffer = &buffer[0..bytes_read]; - while !buffer.is_empty() { - let slice = bios::FfiByteSlice::new(buffer); - let played = unsafe { (api.audio_output_data)(slice).unwrap() }; - buffer = &buffer[played..]; - delta += played; - if delta > 48000 { - bytes += delta; - delta = 0; + let mut pause = false; + 'playback: while !file.eof() { + if !pause { + let bytes_read = mgr.read(&mut volume, &mut file, &mut buffer)?; + let mut buffer = &buffer[0..bytes_read]; + while !buffer.is_empty() { + let slice = bios::FfiByteSlice::new(buffer); + let played = unsafe { (api.audio_output_data)(slice).unwrap() }; + buffer = &buffer[played..]; + delta += played; + if delta > 48000 { + bytes += delta; + delta = 0; + let milliseconds = bytes / ((48000 / 1000) * 4); + osprint!( + "\rPlayed: {}.{:03} s", + milliseconds / 1000, + milliseconds % 1000 + ); + } + } + } + + let mut buffer = [0u8; 16]; + let count = { crate::STD_INPUT.lock().get_data(&mut buffer) }; + for b in &buffer[0..count] { + if *b == b'q' || *b == b'Q' { + osprintln!("\nQuitting playback!"); + break 'playback; + } else if (*b == b'p' || *b == b'P') && pause { + pause = false; + } else if (*b == b'p' || *b == b'P') && !pause { let milliseconds = bytes / ((48000 / 1000) * 4); osprint!( - "\rPlayed: {}:{} ms", + "\rPaused: {}.{:03} s", milliseconds / 1000, milliseconds % 1000 ); + pause = true; } } } From 1a4b3a7b5e02c813133092e07f59adc59cc2aaa2 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sun, 8 Oct 2023 11:53:05 +0100 Subject: [PATCH 12/15] Nicer formatting of mixer info. Also lets you refer to a mixer by index. --- src/commands/hardware.rs | 14 +++++++++++++- src/commands/sound.rs | 16 ++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/commands/hardware.rs b/src/commands/hardware.rs index 3ff608f..3b52d93 100644 --- a/src/commands/hardware.rs +++ b/src/commands/hardware.rs @@ -114,7 +114,19 @@ fn lshw(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: osprintln!("Audio Mixers:"); for dev_idx in 0..=255u8 { if let bios::FfiOption::Some(device_info) = (api.audio_mixer_channel_get_info)(dev_idx) { - osprintln!(" {}: {:?}", dev_idx, device_info); + let dir = match device_info.direction { + bios::audio::Direction::Input => "In", + bios::audio::Direction::Output => "Out", + bios::audio::Direction::Loopback => "Loop", + }; + osprintln!( + " {}: {:08} ({}) {}/{}", + dev_idx, + device_info.name, + dir, + device_info.current_level, + device_info.max_level + ); found = true; } } diff --git a/src/commands/sound.rs b/src/commands/sound.rs index aafef5f..853046b 100644 --- a/src/commands/sound.rs +++ b/src/commands/sound.rs @@ -47,6 +47,8 @@ fn mixer(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: & None }; + let mixer_int = selected_mixer.and_then(|n| n.parse::().ok()); + let api = API.get(); if let (Some(selected_mixer), Some(level_int)) = (selected_mixer, level_int) { @@ -54,7 +56,8 @@ fn mixer(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: & for mixer_id in 0u8..=255u8 { match (api.audio_mixer_channel_get_info)(mixer_id) { bios::FfiOption::Some(mixer_info) => { - if mixer_info.name.as_str() == selected_mixer { + if (Some(mixer_id) == mixer_int) || (mixer_info.name.as_str() == selected_mixer) + { if let Err(e) = (api.audio_mixer_channel_set_level)(mixer_id, level_int).into() { @@ -90,9 +93,10 @@ fn mixer(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: & bios::audio::Direction::Loopback => "Loop", bios::audio::Direction::Output => "Out", }; - if selected_mixer - .and_then(|s| Some(s == mixer_info.name.as_str())) - .unwrap_or(true) + if (Some(mixer_id) == mixer_int) + || selected_mixer + .map(|s| s == mixer_info.name.as_str()) + .unwrap_or(true) { osprintln!( "#{}: {} ({}) {}/{}", @@ -136,14 +140,14 @@ fn play(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &m let api = API.get(); - let mut buffer = &mut scratch[0..4096]; + let buffer = &mut scratch[0..4096]; let mut bytes = 0; let mut delta = 0; let mut pause = false; 'playback: while !file.eof() { if !pause { - let bytes_read = mgr.read(&mut volume, &mut file, &mut buffer)?; + let bytes_read = mgr.read(&volume, &mut file, buffer)?; let mut buffer = &buffer[0..bytes_read]; while !buffer.is_empty() { let slice = bios::FfiByteSlice::new(buffer); From 04f6904fa91a694b0eb9ae0c51076d5ed3099db0 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sun, 8 Oct 2023 12:16:10 +0100 Subject: [PATCH 13/15] VTE 0.12 was released with our changes --- Cargo.lock | 11 +++++++---- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cfe5990..ac99d17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,7 +314,8 @@ checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "utf8parse" version = "0.2.1" -source = "git+https://github.com/alacritty/vte?rev=94e74f3a64f42d5dad4e3d42dbe8c23291038214#94e74f3a64f42d5dad4e3d42dbe8c23291038214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "void" @@ -324,8 +325,9 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "vte" -version = "0.11.1" -source = "git+https://github.com/alacritty/vte?rev=94e74f3a64f42d5dad4e3d42dbe8c23291038214#94e74f3a64f42d5dad4e3d42dbe8c23291038214" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "401dc1020e10f74d38616c1f1ab92ccd85dc902705a29d0730e0fbea8534f91a" dependencies = [ "arrayvec", "utf8parse", @@ -335,7 +337,8 @@ dependencies = [ [[package]] name = "vte_generate_state_changes" version = "0.1.1" -source = "git+https://github.com/alacritty/vte?rev=94e74f3a64f42d5dad4e3d42dbe8c23291038214#94e74f3a64f42d5dad4e3d42dbe8c23291038214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b95ae6a..446b12b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ chrono = { version = "0.4", default-features = false } embedded-sdmmc = { version = "0.5", default-features = false } neotron-api = "0.1" neotron-loader = "0.1" -vte = { git = "https://github.com/alacritty/vte", rev = "94e74f3a64f42d5dad4e3d42dbe8c23291038214" } +vte = "0.12" [features] lib-mode = [] From 6723b0d57abcc49f6e0a79aa96390cb65856acfb Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sun, 8 Oct 2023 13:15:23 +0100 Subject: [PATCH 14/15] Update to 0.6.0 --- CHANGELOG.md | 31 +++++++++++++++++++------------ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 6 +++--- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c94ed7a..e2ff523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,28 +2,32 @@ ## Unreleased changes +* None + +## v0.6.0 (2023-10-08, [Github 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 * Removed demo commands (they should be applications) * Added raw PCM sound playback * Added mixer command -* Switch to neotron-common-bios 0.11.1 +* Switch to [`neotron-common-bios`] 0.11.1 -## v0.5.0 +## v0.5.0 (2023-07-21, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.5.0)) -* Switch to neotron-common-bios 0.11 +* Switch to [`neotron-common-bios`] 0.11 * Added "Shutdown" command * Added ANSI decoder for colour changes (SGI) and cursor position support * Added 'standard input' support for applications * Use new compare-and-swap BIOS API to implement mutexes, instead of `static mut` * OS now requires 256K Flash space -## v0.4.0 +## v0.4.0 (2023-06-25, [Github 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 +## v0.3.3 (2023-05-22, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.3.3)) * Add `dir` command * Change `load` command to load from disk @@ -31,31 +35,34 @@ * Update to `postcard` 1.0 * Fix `readblk` help text, and print 32 bytes per line -## v0.3.2 +## v0.3.2 (2023-05-05, [Github 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 +## v0.3.1 (2023-03-09, [Github 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 +## v0.3.0 (2023-02-12, [Github 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 +* Updated to [`neotron-common-bios`] v0.8.0 +* Use [`pc-keyboard`] for decoding HID events * Fix Windows library build * Added 'kbtest' command * Added 'lshw' command * Added 'config' command * Uses BIOS to store/load OS configuration -## v0.2.0 +[`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)) Adds HID support and basic shell, with 'mem' and 'fill' commands. -## v0.1.0 +## v0.1.0 (2022-03-18, [Github Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.1.0)) First version. diff --git a/Cargo.lock b/Cargo.lock index ac99d17..53592db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,7 +172,7 @@ checksum = "b9b8634a088b9d5b338a96b3f6ef45a3bc0b9c0f0d562c7d00e498265fd96e8f" [[package]] name = "neotron-os" -version = "0.5.0" +version = "0.6.0" dependencies = [ "chrono", "embedded-sdmmc", diff --git a/Cargo.toml b/Cargo.toml index 446b12b..8d82f88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "neotron-os" -version = "0.5.0" +version = "0.6.0" authors = [ "Jonathan 'theJPster' Pallant ", "The Neotron Developers" diff --git a/README.md b/README.md index 9a1496d..35aa893 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This OS is a work in progress. We intend to support: * [x] Running built-in commands from a shell * [x] Executing applications from RAM * [x] Applications can print to stdout - * [ ] Applications can read from stdin + * [x] Applications can read from stdin * [ ] Applications can open/close/read/write files * [x] MBR/FAT32 formatted block devices * [x] Read blocks @@ -22,9 +22,9 @@ This OS is a work in progress. We intend to support: * [ ] Delete files * [ ] Change directory * [x] Load ELF binaries from disk -* [ ] Changing text modes +* [x] Changing text modes * [ ] Basic networking -* [ ] Music playback +* [x] Music playback * [ ] Various keyboard layouts * [ ] Ethernet / WiFi networking * [ ] Built-in scripting language From 95288ecacf2d6f1486a55dff3a3bb3b0d305b79d Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Sun, 8 Oct 2023 13:36:29 +0100 Subject: [PATCH 15/15] Get cargo-binutils as a binary package. --- .github/workflows/rust.yml | 20 +++++++++++++++++++- build.sh | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f17285f..5895193 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,22 +11,40 @@ jobs: uses: actions/checkout@v3 with: submodules: true + - name: Check Syntax run: | cargo check + - name: Test run: | cargo test --lib + - name: Install Targets and Tools run: | rustup target add thumbv7em-none-eabi rustup target add thumbv7m-none-eabi rustup target add thumbv6m-none-eabi rustup component add llvm-tools-preview - cargo install cargo-binutils + + - name: Install tools + uses: taiki-e/install-action@v2 + with: + tool: cargo-binutils@0.3.6 + - name: Build run: | ./build.sh --verbose + + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + if: ${{success()}} + with: + name: Artifacts + if-no-files-found: error + path: | + ./release/ + - name: Upload files to Release if: github.event_name == 'push' && startswith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v1 diff --git a/build.sh b/build.sh index 4b17559..2024e27 100755 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ for TARGET_ARCH in thumbv6m-none-eabi thumbv7m-none-eabi thumbv7em-none-eabi; do echo "BINARY is ${BINARY}" cargo build $* --release --target=${TARGET_ARCH} --bin ${BINARY} # objcopy would do the build for us first, but it doesn't have good build output - cargo objcopy $* --release --target=${TARGET_ARCH} --bin ${BINARY} -- -O binary ${RELEASE_DIR}/${TARGET_ARCH}-${BINARY}-libneotron_os.bin + rust-objcopy -O binary ./target/${TARGET_ARCH}/release/${BINARY} ${RELEASE_DIR}/${TARGET_ARCH}-${BINARY}-libneotron_os.bin # Keep the ELF file too (for debugging) cp ./target/${TARGET_ARCH}/release/${BINARY} ${RELEASE_DIR}/${TARGET_ARCH}-${BINARY}-libneotron_os.elf done