├── .gitignore ├── .cargo └── config.toml ├── utilities ├── flames │ ├── README.md │ ├── build.rs │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ └── lib.rs ├── snake │ ├── build.rs │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ └── lib.rs ├── neoplay │ ├── build.rs │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── main.rs │ │ └── player.rs └── README.md ├── .vscode └── settings.json ├── .github ├── workflows │ ├── clippy.yml │ ├── format.yml │ └── rust.yml └── FUNDING.yml ├── Cargo.toml ├── nbuild ├── README.md ├── Cargo.toml ├── src │ ├── lib.rs │ └── main.rs └── Cargo.lock ├── neotron-os ├── src │ ├── main.rs │ ├── commands │ │ ├── timedate.rs │ │ ├── input.rs │ │ ├── block.rs │ │ ├── mod.rs │ │ ├── ram.rs │ │ ├── config.rs │ │ ├── sound.rs │ │ ├── fs.rs │ │ ├── screen.rs │ │ └── hardware.rs │ ├── config.rs │ ├── refcell.rs │ ├── fs.rs │ └── lib.rs ├── Cargo.toml ├── build.rs ├── neotron-os-arm.ld └── Cargo.lock ├── gdb.cfg ├── CHANGELOG.md ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /nbuild/target 2 | /target 3 | **/*.rs.bk 4 | /release 5 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | nbuild = "run --manifest-path nbuild/Cargo.toml --" 3 | -------------------------------------------------------------------------------- /utilities/flames/README.md: -------------------------------------------------------------------------------- 1 | # Flames 2 | 3 | Displays a simple flame graphic using ANSI text. 4 | -------------------------------------------------------------------------------- /utilities/flames/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg-bin=flames=-Tneotron-cortex-m.ld"); 3 | } 4 | -------------------------------------------------------------------------------- /utilities/snake/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg-bin=snake=-Tneotron-cortex-m.ld"); 3 | } 4 | -------------------------------------------------------------------------------- /utilities/neoplay/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-arg-bin=neoplay=-Tneotron-cortex-m.ld"); 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.checkOnSave.allTargets": false, 3 | "rust-analyzer.linkedProjects": [ 4 | "./Cargo.toml", 5 | "./nbuild/Cargo.toml" 6 | ] 7 | } -------------------------------------------------------------------------------- /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: Clippy 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | clippy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | submodules: true 12 | - run: rustup component add clippy 13 | - run: cargo nbuild clippy 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | format: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | submodules: true 12 | - run: rustup component add rustfmt 13 | - run: cargo nbuild format --check 14 | -------------------------------------------------------------------------------- /utilities/snake/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "snake" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | authors = ["Jonathan 'theJPster' Pallant "] 7 | description = "ANSI Snake for Neotron systems" 8 | 9 | [dependencies] 10 | neotron-sdk = { workspace = true } 11 | 12 | # See workspace for profile settings 13 | -------------------------------------------------------------------------------- /utilities/flames/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flames" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | authors = ["Jonathan 'theJPster' Pallant "] 7 | description = "ANSI Flames for Neotron systems" 8 | 9 | [dependencies] 10 | neotron-sdk = { workspace = true } 11 | 12 | # See workspace for profile settings 13 | -------------------------------------------------------------------------------- /utilities/flames/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(target_os = "none", no_std)] 2 | #![cfg_attr(target_os = "none", no_main)] 3 | 4 | #[cfg(not(target_os = "none"))] 5 | fn main() { 6 | neotron_sdk::init(); 7 | } 8 | 9 | static mut APP: flames::App = flames::App::new(80, 25); 10 | 11 | #[no_mangle] 12 | extern "C" fn neotron_main() -> i32 { 13 | unsafe { APP.play() } 14 | 0 15 | } 16 | -------------------------------------------------------------------------------- /utilities/snake/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(target_os = "none", no_std)] 2 | #![cfg_attr(target_os = "none", no_main)] 3 | 4 | #[cfg(not(target_os = "none"))] 5 | fn main() { 6 | neotron_sdk::init(); 7 | } 8 | 9 | static mut APP: snake::App = snake::App::new(80, 25); 10 | 11 | #[no_mangle] 12 | extern "C" fn neotron_main() -> i32 { 13 | unsafe { APP.play() } 14 | 0 15 | } 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "neotron-os", 4 | "utilities/*", 5 | ] 6 | resolver = "2" 7 | exclude = [ 8 | "nbuild" 9 | ] 10 | 11 | [workspace.dependencies] 12 | neotron-sdk = "0.2.0" 13 | 14 | [profile.release] 15 | lto = true 16 | debug = true 17 | codegen-units = 1 18 | opt-level = "z" 19 | panic = "abort" 20 | 21 | [profile.dev] 22 | panic = "abort" 23 | 24 | -------------------------------------------------------------------------------- /utilities/README.md: -------------------------------------------------------------------------------- 1 | # Neotron OS Utilities 2 | 3 | These are executables which are shipped with Neotron OS. 4 | 5 | If you're used to MS-DOS, these are the equivalents of things like `MODE.COM` 6 | and `FORMAT.COM` that were usually installed to `C:\DOS`. 7 | 8 | We pack them into a ROMFS, which Neotron OS then includes as a disk image. 9 | 10 | ## List of Utilities 11 | 12 | * [Flames](./flames) - draws an ANSI flame animation 13 | -------------------------------------------------------------------------------- /nbuild/README.md: -------------------------------------------------------------------------------- 1 | # nbuild - the Neotron OS Build System 2 | 3 | Building Neotron OS involves: 4 | 5 | * Compiling the Neotron OS source code 6 | * Compiling and linking multiple Neotron OS utilities 7 | * Assembling the utilities into a ROMFS image 8 | * Linking the Neotron OS object code, including the ROMFS image 9 | 10 | This utility automates that process. 11 | 12 | Run `cargo nbuild --help` from the top level of the checkout for more information. 13 | -------------------------------------------------------------------------------- /utilities/neoplay/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "neoplay" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | authors = ["Jonathan 'theJPster' Pallant "] 7 | description = "4-channel ProTracker player for Neotro" 8 | 9 | [dependencies] 10 | grounded = { version = "0.2.0", features = ["critical-section", "cas"] } 11 | neotracker = { git = "https://github.com/thejpster/neotracker.git", rev = "2ee7a85006a9461b876bdf47e45b6105437a38f6" } 12 | neotron-sdk = { workspace = true } 13 | 14 | # See workspace for profile settings 15 | -------------------------------------------------------------------------------- /neotron-os/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Binary Neotron OS Image 2 | //! 3 | //! This is for Flash Addresses that start at `0x1002_0000`. 4 | //! 5 | //! Copyright (c) The Neotron Developers, 2022 6 | //! 7 | //! Licence: GPL v3 or higher (see ../LICENCE.md) 8 | 9 | #![no_std] 10 | #![no_main] 11 | 12 | /// This tells the BIOS how to start the OS. This must be the first four bytes 13 | /// of our portion of Flash. 14 | #[link_section = ".entry_point"] 15 | #[used] 16 | pub static ENTRY_POINT_ADDR: extern "C" fn(&neotron_common_bios::Api) -> ! = neotron_os::os_main; 17 | 18 | // End of file 19 | -------------------------------------------------------------------------------- /nbuild/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nbuild" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = [ 6 | "Jonathan 'theJPster' Pallant ", 7 | "The Neotron Developers" 8 | ] 9 | description = "The Neotron Operating System Build System" 10 | license = "GPL-3.0-or-later" 11 | readme = "README.md" 12 | repository = "https://github.com/neotron-compute/Neotron-OS" 13 | 14 | [dependencies] 15 | chrono = { version = "0.4.39", default-features = false, features = ["std"] } 16 | clap = { version = "4.5.23", features = ["derive"] } 17 | embedded-io = { version = "0.6.1", features = ["std"] } 18 | neotron-api = "0.2.0" 19 | neotron-romfs = "2.0" 20 | -------------------------------------------------------------------------------- /gdb.cfg: -------------------------------------------------------------------------------- 1 | target extended-remote :3333 2 | 3 | # print demangled symbols by default 4 | set print asm-demangle on 5 | 6 | monitor arm semihosting enable 7 | 8 | # # send captured ITM to the file itm.fifo 9 | # # (the microcontroller SWO pin must be connected to the programmer SWO pin) 10 | # # 8000000 must match the core clock frequency 11 | # monitor tpiu config internal itm.fifo uart off 8000000 12 | 13 | # # OR: make the microcontroller SWO pin output compatible with UART (8N1) 14 | # # 2000000 is the frequency of the SWO pin 15 | # monitor tpiu config external uart off 8000000 2000000 16 | 17 | # # enable ITM port 0 18 | # monitor itm port 0 on 19 | 20 | load 21 | monitor reset halt 22 | stepi 23 | 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Neotron-Compute 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /utilities/neoplay/README.md: -------------------------------------------------------------------------------- 1 | # Neoplay 2 | 3 | A ProTracker MOD player for the Neotron Pico. 4 | 5 | Runs at 11,025 Hz, quadrupling samples for the audio codec which runs at 44,100 Hz. 6 | 7 | ```console 8 | $ cargo build --release --target=thumbv6m-none-eabi 9 | $ cp ../target/thumbv6m-none-eabi/release/neoplay /media/USER/SDCARD/NEOPLAY.ELF 10 | 11 | ``` 12 | 13 | ```console 14 | > load neoplay.elf 15 | > run airwolf.mod 16 | Loading "airwolf.mod" 17 | audio 44100, SixteenBitStereo 18 | Playing "airwolf.mod" 19 | 20 | 000 000000 12 00fe 0f04|-- ---- ----|-- ---- ----|-- ---- ----| 21 | 000 000001 -- ---- ----|-- ---- ----|-- ---- ----|-- ---- ----| 22 | 000 000002 -- ---- ----|-- ---- ----|-- ---- ----|-- ---- ----| 23 | 000 000003 -- ---- ----|-- ---- ----|-- ---- ----|-- ---- ----| 24 | etc 25 | ``` 26 | 27 | Here's a video of it in action: https://youtu.be/ONZhDrZsmDU 28 | -------------------------------------------------------------------------------- /neotron-os/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "neotron-os" 3 | version = "0.8.1" 4 | authors = [ 5 | "Jonathan 'theJPster' Pallant ", 6 | "The Neotron Developers" 7 | ] 8 | edition = "2018" 9 | description = "The Neotron Operating System" 10 | license = "GPL-3.0-or-later" 11 | readme = "README.md" 12 | repository = "https://github.com/neotron-compute/Neotron-OS" 13 | 14 | [[bin]] 15 | name = "neotron-os" 16 | test = false 17 | bench = false 18 | 19 | [lib] 20 | crate-type = ["rlib", "cdylib"] 21 | required-features = ["native-log"] 22 | 23 | [dependencies] 24 | chrono = { version = "0.4", default-features = false } 25 | embedded-sdmmc = { version = "0.7", default-features = false } 26 | heapless = "0.7" 27 | menu = "0.3" 28 | neotron-api = "0.2" 29 | neotron-common-bios = "0.12.0" 30 | neotron-loader = "0.1" 31 | neotron-romfs = "1.0" 32 | pc-keyboard = "0.7" 33 | postcard = "1.0" 34 | r0 = "1.0" 35 | serde = { version = "1.0", default-features = false } 36 | vte = "0.12" 37 | 38 | [features] 39 | lib-mode = [] 40 | -------------------------------------------------------------------------------- /neotron-os/src/commands/timedate.rs: -------------------------------------------------------------------------------- 1 | //! CLI commands for getting/setting time/date 2 | 3 | use chrono::{Datelike, Timelike}; 4 | 5 | use crate::{osprintln, Ctx, API}; 6 | 7 | pub static DATE_ITEM: menu::Item = menu::Item { 8 | item_type: menu::ItemType::Callback { 9 | function: date, 10 | parameters: &[menu::Parameter::Optional { 11 | parameter_name: "timestamp", 12 | help: Some("The new date/time, in ISO8601 format"), 13 | }], 14 | }, 15 | command: "date", 16 | help: Some("Get/set the time and date"), 17 | }; 18 | 19 | /// Called when the "date" command is executed. 20 | fn date(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { 21 | if let Ok(Some(timestamp)) = menu::argument_finder(item, args, "timestamp") { 22 | osprintln!("Setting date/time to {:?}", timestamp); 23 | static DATE_FMT: &str = "%Y-%m-%dT%H:%M:%S"; 24 | let Ok(timestamp) = chrono::NaiveDateTime::parse_from_str(timestamp, DATE_FMT) else { 25 | osprintln!("Unable to parse date/time"); 26 | return; 27 | }; 28 | API.set_time(timestamp); 29 | } 30 | 31 | let time = API.get_time(); 32 | // Ensure this matches `DATE_FMT`, for consistency 33 | osprintln!( 34 | "The time is {:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}", 35 | time.year(), 36 | time.month(), 37 | time.day(), 38 | time.hour(), 39 | time.minute(), 40 | time.second(), 41 | time.nanosecond() 42 | ); 43 | } 44 | 45 | // End of file 46 | -------------------------------------------------------------------------------- /neotron-os/src/commands/input.rs: -------------------------------------------------------------------------------- 1 | //! Input related commands for Neotron OS 2 | 3 | use crate::{osprintln, Ctx}; 4 | 5 | pub static KBTEST_ITEM: menu::Item = menu::Item { 6 | item_type: menu::ItemType::Callback { 7 | function: kbtest, 8 | parameters: &[], 9 | }, 10 | command: "input_kbtest", 11 | help: Some("Test the keyboard (press ESC to quit)"), 12 | }; 13 | 14 | /// Called when the "kbtest" command is executed. 15 | fn kbtest(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { 16 | osprintln!("Press Ctrl-X to quit"); 17 | const CTRL_X: u8 = 0x18; 18 | 'outer: loop { 19 | if let Some(ev) = crate::STD_INPUT.lock().get_raw() { 20 | osprintln!("Event: {ev:?}"); 21 | if ev == pc_keyboard::DecodedKey::Unicode(CTRL_X as char) { 22 | break 'outer; 23 | } 24 | } 25 | let mut buffer = [0u8; 8]; 26 | let count = if let Some(serial) = crate::SERIAL_CONSOLE.lock().as_mut() { 27 | serial 28 | .read_data(&mut buffer) 29 | .ok() 30 | .and_then(|n| if n == 0 { None } else { Some(n) }) 31 | } else { 32 | None 33 | }; 34 | if let Some(count) = count { 35 | osprintln!("Serial RX: {:x?}", &buffer[0..count]); 36 | for b in &buffer[0..count] { 37 | if *b == CTRL_X { 38 | break 'outer; 39 | } 40 | } 41 | } 42 | } 43 | osprintln!("Finished."); 44 | } 45 | 46 | // End of file 47 | -------------------------------------------------------------------------------- /neotron-os/src/commands/block.rs: -------------------------------------------------------------------------------- 1 | //! Block Device related commands for Neotron OS 2 | 3 | use super::{parse_u64, parse_u8}; 4 | use crate::{bios, osprint, osprintln, Ctx, API}; 5 | 6 | pub static READ_ITEM: menu::Item = menu::Item { 7 | item_type: menu::ItemType::Callback { 8 | function: read_block, 9 | parameters: &[ 10 | menu::Parameter::Mandatory { 11 | parameter_name: "device_idx", 12 | help: Some("The block device ID to fetch from"), 13 | }, 14 | menu::Parameter::Mandatory { 15 | parameter_name: "block_idx", 16 | help: Some("The block to fetch, 0..num_blocks"), 17 | }, 18 | ], 19 | }, 20 | command: "readblk", 21 | help: Some("Display one disk block, as hex"), 22 | }; 23 | 24 | /// Called when the "read_block" command is executed. 25 | fn read_block(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { 26 | let api = API.get(); 27 | let Ok(device_idx) = parse_u8(args[0]) else { 28 | osprintln!("Couldn't parse {:?}", args[0]); 29 | return; 30 | }; 31 | let Ok(block_idx) = parse_u64(args[1]) else { 32 | osprintln!("Couldn't parse {:?}", args[1]); 33 | return; 34 | }; 35 | osprintln!("Reading block {}:", block_idx); 36 | let mut buffer = [0u8; 512]; 37 | match (api.block_read)( 38 | device_idx, 39 | bios::block_dev::BlockIdx(block_idx), 40 | 1, 41 | bios::FfiBuffer::new(&mut buffer), 42 | ) { 43 | bios::ApiResult::Ok(_) => { 44 | // Carry on 45 | let mut count = 0; 46 | for chunk in buffer.chunks(32) { 47 | osprint!("{:03x}: ", count); 48 | for b in chunk { 49 | osprint!("{:02x}", *b); 50 | } 51 | count += chunk.len(); 52 | osprintln!(); 53 | } 54 | } 55 | bios::ApiResult::Err(e) => { 56 | osprintln!("Failed to read: {:?}", e); 57 | } 58 | } 59 | } 60 | 61 | // End of file 62 | -------------------------------------------------------------------------------- /neotron-os/build.rs: -------------------------------------------------------------------------------- 1 | use std::io::prelude::*; 2 | 3 | const LINKER_SCRIPT: &str = "neotron-os-arm.ld"; 4 | 5 | fn main() { 6 | if Ok("none") == std::env::var("CARGO_CFG_TARGET_OS").as_deref() { 7 | let start_address = std::env::var("NEOTRON_OS_START_ADDRESS"); 8 | let start_address = start_address.as_deref().unwrap_or("0x10020000"); 9 | copy_linker_script(start_address); 10 | println!("cargo::rustc-link-arg-bin=neotron-os=-T{}", LINKER_SCRIPT); 11 | println!("cargo::rerun-if-env-changed=NEOTRON_OS_START_ADDRESS"); 12 | } 13 | 14 | if let Ok(cmd_output) = std::process::Command::new("git") 15 | .arg("describe") 16 | .arg("--all") 17 | .arg("--dirty") 18 | .arg("--long") 19 | .output() 20 | { 21 | let git_version = std::str::from_utf8(&cmd_output.stdout).unwrap(); 22 | println!( 23 | "cargo::rustc-env=OS_VERSION={} (git:{})", 24 | env!("CARGO_PKG_VERSION"), 25 | git_version.trim() 26 | ); 27 | } else { 28 | println!("cargo::rustc-env=OS_VERSION={}", env!("CARGO_PKG_VERSION")); 29 | } 30 | 31 | if Ok("macos") == std::env::var("CARGO_CFG_TARGET_OS").as_deref() { 32 | println!("cargo::rustc-link-lib=c"); 33 | } 34 | 35 | if Ok("windows") == std::env::var("CARGO_CFG_TARGET_OS").as_deref() { 36 | println!("cargo::rustc-link-lib=dylib=msvcrt"); 37 | } 38 | 39 | if let Some(path) = option_env!("ROMFS_PATH") { 40 | println!("cargo::rustc-cfg=romfs_enabled=\"yes\""); 41 | println!("cargo::rerun-if-env-changed=ROMFS_PATH"); 42 | println!("cargo::rerun-if-changed={}", path); 43 | } 44 | println!("cargo::rustc-check-cfg=cfg(romfs_enabled, values(\"yes\"))"); 45 | } 46 | 47 | /// Put the given script in our output directory and ensure it's on the linker 48 | /// search path. 49 | fn copy_linker_script(start_address: &str) { 50 | let out = &std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); 51 | let contents = std::fs::read_to_string(LINKER_SCRIPT).expect("loading ld script"); 52 | let patched = contents.replace("${{start_address}}", start_address); 53 | std::fs::File::create(out.join(LINKER_SCRIPT)) 54 | .unwrap() 55 | .write_all(patched.as_bytes()) 56 | .unwrap(); 57 | println!("cargo::rustc-link-search={}", out.display()); 58 | } 59 | 60 | // End of file 61 | -------------------------------------------------------------------------------- /neotron-os/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | //! Commands for Neotron OS 2 | //! 3 | //! Defines the top-level menu, and the commands it can call. 4 | 5 | pub use super::Ctx; 6 | 7 | mod block; 8 | mod config; 9 | mod fs; 10 | mod hardware; 11 | mod input; 12 | mod ram; 13 | mod screen; 14 | mod sound; 15 | mod timedate; 16 | 17 | pub static OS_MENU: menu::Menu = menu::Menu { 18 | label: "root", 19 | items: &[ 20 | &timedate::DATE_ITEM, 21 | &config::COMMAND_ITEM, 22 | &hardware::LSBLK_ITEM, 23 | &hardware::LSBUS_ITEM, 24 | &hardware::LSI2C_ITEM, 25 | &hardware::LSMEM_ITEM, 26 | &hardware::LSUART_ITEM, 27 | &hardware::I2C_ITEM, 28 | &block::READ_ITEM, 29 | &fs::DIR_ITEM, 30 | &ram::HEXDUMP_ITEM, 31 | &ram::RUN_ITEM, 32 | &fs::LOAD_ITEM, 33 | &fs::EXEC_ITEM, 34 | &fs::TYPE_ITEM, 35 | &fs::ROM_ITEM, 36 | &screen::CLS_ITEM, 37 | &screen::MODE_ITEM, 38 | &screen::GFX_ITEM, 39 | &input::KBTEST_ITEM, 40 | &hardware::SHUTDOWN_ITEM, 41 | &sound::MIXER_ITEM, 42 | &sound::PLAY_ITEM, 43 | ], 44 | entry: None, 45 | exit: None, 46 | }; 47 | 48 | /// Parse a string into a `usize` 49 | /// 50 | /// Numbers like `0x123` are hex. Numbers like `123` are decimal. 51 | fn parse_usize(input: &str) -> Result { 52 | if let Some(digits) = input.strip_prefix("0x") { 53 | // Parse as hex 54 | usize::from_str_radix(digits, 16) 55 | } else { 56 | // Parse as decimal 57 | input.parse::() 58 | } 59 | } 60 | 61 | /// Parse a string into a `u8` 62 | /// 63 | /// Numbers like `0x123` are hex. Numbers like `123` are decimal. 64 | fn parse_u8(input: &str) -> Result { 65 | if let Some(digits) = input.strip_prefix("0x") { 66 | // Parse as hex 67 | u8::from_str_radix(digits, 16) 68 | } else { 69 | // Parse as decimal 70 | input.parse::() 71 | } 72 | } 73 | 74 | /// Parse a string into a `u64` 75 | /// 76 | /// Numbers like `0x123` are hex. Numbers like `123` are decimal. 77 | fn parse_u64(input: &str) -> Result { 78 | if let Some(digits) = input.strip_prefix("0x") { 79 | // Parse as hex 80 | u64::from_str_radix(digits, 16) 81 | } else { 82 | // Parse as decimal 83 | input.parse::() 84 | } 85 | } 86 | 87 | // End of file 88 | -------------------------------------------------------------------------------- /utilities/neoplay/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(target_os = "none", no_std)] 2 | #![cfg_attr(target_os = "none", no_main)] 3 | 4 | use core::{fmt::Write, ptr::addr_of_mut}; 5 | 6 | const FILE_BUFFER_LEN: usize = 192 * 1024; 7 | static mut FILE_BUFFER: [u8; FILE_BUFFER_LEN] = [0u8; FILE_BUFFER_LEN]; 8 | 9 | mod player; 10 | 11 | #[cfg(not(target_os = "none"))] 12 | fn main() { 13 | neotron_sdk::init(); 14 | } 15 | 16 | #[no_mangle] 17 | extern "C" fn neotron_main() -> i32 { 18 | if let Err(e) = real_main() { 19 | let mut stdout = neotron_sdk::stdout(); 20 | let _ = writeln!(stdout, "Error: {:?}", e); 21 | 1 22 | } else { 23 | 0 24 | } 25 | } 26 | 27 | fn real_main() -> Result<(), neotron_sdk::Error> { 28 | let mut stdout = neotron_sdk::stdout(); 29 | let stdin = neotron_sdk::stdin(); 30 | let Some(filename) = neotron_sdk::arg(0) else { 31 | return Err(neotron_sdk::Error::InvalidArg); 32 | }; 33 | let _ = writeln!(stdout, "Loading {:?}...", filename); 34 | let path = neotron_sdk::path::Path::new(&filename)?; 35 | let f = neotron_sdk::File::open(path, neotron_sdk::Flags::empty())?; 36 | let file_buffer = unsafe { 37 | let file_buffer = &mut *addr_of_mut!(FILE_BUFFER); 38 | let n = f.read(file_buffer)?; 39 | &file_buffer[0..n] 40 | }; 41 | drop(f); 42 | // Set 16-bit stereo, 44.1 kHz 43 | let dsp_path = neotron_sdk::path::Path::new("AUDIO:")?; 44 | let dsp = neotron_sdk::File::open(dsp_path, neotron_sdk::Flags::empty())?; 45 | if dsp.ioctl(1, 3 << 60 | 44100).is_err() { 46 | let _ = writeln!(stdout, "Failed to configure audio"); 47 | return neotron_sdk::Result::Err(neotron_sdk::Error::DeviceSpecific); 48 | } 49 | 50 | let mut player = match player::Player::new(file_buffer, 44100) { 51 | Ok(player) => player, 52 | Err(e) => { 53 | let _ = writeln!(stdout, "Failed to create player: {:?}", e); 54 | return Err(neotron_sdk::Error::InvalidArg); 55 | } 56 | }; 57 | 58 | let _ = writeln!(stdout, "Playing {:?}...", filename); 59 | let mut sample_buffer = [0u8; 1024]; 60 | // loop some some silence to give us a head-start 61 | for _i in 0..11 { 62 | let _ = dsp.write(&sample_buffer); 63 | } 64 | 65 | loop { 66 | for chunk in sample_buffer.chunks_exact_mut(4) { 67 | let (left, right) = player.next_sample(&mut stdout); 68 | let left_bytes = left.to_le_bytes(); 69 | let right_bytes = right.to_le_bytes(); 70 | chunk[0] = left_bytes[0]; 71 | chunk[1] = left_bytes[1]; 72 | chunk[2] = right_bytes[0]; 73 | chunk[3] = right_bytes[1]; 74 | } 75 | let _ = dsp.write(&sample_buffer); 76 | let mut in_buf = [0u8; 1]; 77 | if player.is_finished() { 78 | break; 79 | } 80 | if stdin.read(&mut in_buf).is_ok() && in_buf[0].to_ascii_lowercase() == b'q' { 81 | break; 82 | } 83 | } 84 | 85 | let _ = writeln!(stdout, "Bye!"); 86 | 87 | Ok(()) 88 | } 89 | -------------------------------------------------------------------------------- /neotron-os/src/config.rs: -------------------------------------------------------------------------------- 1 | //! # OS Configuration 2 | //! 3 | //! Handles persistently storing OS configuration, using the BIOS. 4 | 5 | use crate::{bios, API}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// Represents our configuration information that we ask the BIOS to serialise 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub struct Config { 11 | vga_console: Option, 12 | serial_console: bool, 13 | serial_baud: u32, 14 | } 15 | 16 | impl Config { 17 | pub fn load() -> Result { 18 | let api = API.get(); 19 | let mut buffer = [0u8; 64]; 20 | match (api.configuration_get)(bios::FfiBuffer::new(&mut buffer)) { 21 | bios::ApiResult::Ok(n) => { 22 | postcard::from_bytes(&buffer[0..n]).map_err(|_e| "Failed to parse config") 23 | } 24 | bios::ApiResult::Err(_e) => Err("Failed to load config"), 25 | } 26 | } 27 | 28 | pub fn save(&self) -> Result<(), &'static str> { 29 | let api = API.get(); 30 | let mut buffer = [0u8; 64]; 31 | let slice = postcard::to_slice(self, &mut buffer).map_err(|_e| "Failed to parse config")?; 32 | match (api.configuration_set)(bios::FfiByteSlice::new(slice)) { 33 | bios::ApiResult::Ok(_) => Ok(()), 34 | bios::ApiResult::Err(bios::Error::Unimplemented) => { 35 | Err("BIOS doesn't support this (yet)") 36 | } 37 | bios::ApiResult::Err(_) => Err("BIOS reported an error"), 38 | } 39 | } 40 | 41 | /// Should this system use the VGA console? 42 | pub fn get_vga_console(&self) -> Option { 43 | self.vga_console.and_then(bios::video::Mode::try_from_u8) 44 | } 45 | 46 | // Set whether this system should use the VGA console. 47 | pub fn set_vga_console(&mut self, new_value: Option) { 48 | self.vga_console = new_value.map(|m| m.as_u8()); 49 | } 50 | 51 | /// Should this system use the UART console? 52 | pub fn get_serial_console(&self) -> Option<(u8, bios::serial::Config)> { 53 | if self.serial_console { 54 | Some(( 55 | 0, 56 | bios::serial::Config { 57 | data_rate_bps: self.serial_baud, 58 | data_bits: bios::serial::DataBits::Eight.make_ffi_safe(), 59 | stop_bits: bios::serial::StopBits::One.make_ffi_safe(), 60 | parity: bios::serial::Parity::None.make_ffi_safe(), 61 | handshaking: bios::serial::Handshaking::None.make_ffi_safe(), 62 | }, 63 | )) 64 | } else { 65 | None 66 | } 67 | } 68 | 69 | /// Turn the serial console off 70 | pub fn set_serial_console_off(&mut self) { 71 | self.serial_console = false; 72 | self.serial_baud = 0; 73 | } 74 | 75 | /// Turn the serial console on 76 | pub fn set_serial_console_on(&mut self, serial_baud: u32) { 77 | self.serial_console = true; 78 | self.serial_baud = serial_baud; 79 | } 80 | } 81 | 82 | impl core::default::Default for Config { 83 | fn default() -> Config { 84 | Config { 85 | vga_console: Some(0), 86 | serial_console: false, 87 | serial_baud: 115200, 88 | } 89 | } 90 | } 91 | 92 | // End of file 93 | -------------------------------------------------------------------------------- /neotron-os/src/commands/ram.rs: -------------------------------------------------------------------------------- 1 | //! Raw RAM read/write related commands for Neotron OS 2 | 3 | use super::parse_usize; 4 | use crate::{osprint, osprintln, Ctx}; 5 | 6 | pub static HEXDUMP_ITEM: menu::Item = menu::Item { 7 | item_type: menu::ItemType::Callback { 8 | function: hexdump, 9 | parameters: &[ 10 | menu::Parameter::Mandatory { 11 | parameter_name: "address", 12 | help: Some("Start address"), 13 | }, 14 | menu::Parameter::Optional { 15 | parameter_name: "length", 16 | help: Some("Number of bytes"), 17 | }, 18 | ], 19 | }, 20 | command: "hexdump", 21 | help: Some("Dump the contents of RAM as hex"), 22 | }; 23 | 24 | pub static RUN_ITEM: menu::Item = menu::Item { 25 | item_type: menu::ItemType::Callback { 26 | function: run, 27 | parameters: &[ 28 | menu::Parameter::Optional { 29 | parameter_name: "arg1", 30 | help: None, 31 | }, 32 | menu::Parameter::Optional { 33 | parameter_name: "arg2", 34 | help: None, 35 | }, 36 | menu::Parameter::Optional { 37 | parameter_name: "arg3", 38 | help: None, 39 | }, 40 | menu::Parameter::Optional { 41 | parameter_name: "arg4", 42 | help: None, 43 | }, 44 | ], 45 | }, 46 | command: "run", 47 | help: Some("Run a program (with up to four arguments)"), 48 | }; 49 | 50 | /// Called when the "hexdump" command is executed. 51 | /// 52 | /// If you ask for an address that generates a HardFault, the OS will crash. So 53 | /// don't. 54 | fn hexdump(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { 55 | const BYTES_PER_LINE: usize = 16; 56 | 57 | let Some(address_str) = args.first() else { 58 | osprintln!("No address"); 59 | return; 60 | }; 61 | let Ok(address) = parse_usize(address_str) else { 62 | osprintln!("Bad address"); 63 | return; 64 | }; 65 | let len_str = args.get(1).unwrap_or(&"16"); 66 | let Ok(len) = parse_usize(len_str) else { 67 | osprintln!("Bad length"); 68 | return; 69 | }; 70 | 71 | let mut ptr = address as *const u8; 72 | 73 | let mut this_line = 0; 74 | osprint!("{:08x}: ", address); 75 | for count in 0..len { 76 | if this_line == BYTES_PER_LINE { 77 | osprintln!(); 78 | osprint!("{:08x}: ", address + count); 79 | this_line = 1; 80 | } else { 81 | this_line += 1; 82 | } 83 | 84 | let b = unsafe { ptr.read_volatile() }; 85 | osprint!("{:02x} ", b); 86 | ptr = unsafe { ptr.offset(1) }; 87 | } 88 | osprintln!(); 89 | } 90 | 91 | /// Called when the "run" command is executed. 92 | fn run(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { 93 | match ctx.tpa.execute(args) { 94 | Ok(0) => { 95 | osprintln!(); 96 | } 97 | Ok(n) => { 98 | osprintln!("\nError Code: {}", n); 99 | } 100 | Err(e) => { 101 | osprintln!("\nFailed to execute: {:?}", e); 102 | } 103 | } 104 | } 105 | 106 | // End of file 107 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | embedded-binaries: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | target: [thumbv6m-none-eabi, thumbv7em-none-eabi, thumbv8m.main-none-eabi] 11 | start_address: [0x0802_0000, 0x1002_0000] 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: true 16 | - run: rustup target add ${{ matrix.target }} 17 | - run: rustup component add llvm-tools-preview 18 | - run: cargo nbuild binary --target=${{ matrix.target }} --start-address=${{ matrix.start_address }} 19 | - uses: actions/upload-artifact@v4 20 | if: ${{success()}} 21 | with: 22 | name: ${{ matrix.target }}-${{ matrix.start_address }}-binary 23 | if-no-files-found: error 24 | path: | 25 | ./target/${{ matrix.target }}/release/neotron-os 26 | ./target/${{ matrix.target }}/release/neotron-os.bin 27 | ./target/${{ matrix.target }}/release/romfs.bin 28 | x64_64-unknown-linux-gnu-library: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | submodules: true 34 | - run: cargo nbuild library 35 | - uses: actions/upload-artifact@v4 36 | if: ${{success()}} 37 | with: 38 | name: x64_64-unknown-linux-gnu-library 39 | if-no-files-found: error 40 | path: | 41 | ./target/release/libneotron_os.so 42 | x86_64-pc-windows-msvc-library: 43 | runs-on: windows-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | with: 47 | submodules: true 48 | - run: cargo nbuild library 49 | - uses: actions/upload-artifact@v4 50 | if: ${{success()}} 51 | with: 52 | name: x86_64-pc-windows-msvc-library 53 | if-no-files-found: error 54 | path: | 55 | ./target/release/neotron_os.dll 56 | ./target/release/neotron_os.dll.exp 57 | ./target/release/neotron_os.dll.lib 58 | ./target/release/neotron_os.pdb 59 | aarch64-apple-darwin-library: 60 | runs-on: macos-latest 61 | steps: 62 | - uses: actions/checkout@v4 63 | with: 64 | submodules: true 65 | - run: cargo nbuild library 66 | - uses: actions/upload-artifact@v4 67 | if: ${{success()}} 68 | with: 69 | name: aarch64-apple-darwin-library 70 | if-no-files-found: error 71 | path: | 72 | ./target/release/libneotron_os.dylib 73 | run-tests: 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v4 77 | with: 78 | submodules: true 79 | - run: cargo nbuild test 80 | preview-release: 81 | runs-on: ubuntu-latest 82 | needs: [embedded-binaries, x64_64-unknown-linux-gnu-library, x86_64-pc-windows-msvc-library, aarch64-apple-darwin-library, run-tests] 83 | steps: 84 | - run: mkdir ./release 85 | - uses: actions/download-artifact@v4 86 | with: 87 | path: ./release-${{ github.ref_name }} 88 | - run: ls -lR ./release-${{ github.ref_name }} 89 | - run: zip -r ./release-${{ github.ref_name }}.zip ./release-${{ github.ref_name }} 90 | - uses: actions/upload-artifact@v4 91 | if: ${{success()}} 92 | with: 93 | name: release 94 | if-no-files-found: error 95 | path: | 96 | ./release-${{ github.ref_name }}.zip 97 | release: 98 | runs-on: ubuntu-latest 99 | needs: [preview-release] 100 | if: github.event_name == 'push' && startswith(github.ref, 'refs/tags/') 101 | steps: 102 | - uses: actions/download-artifact@v4 103 | with: 104 | name: release 105 | path: . 106 | - uses: softprops/action-gh-release@v1 107 | with: 108 | files: | 109 | ./release-${{ github.ref_name }}.zip 110 | -------------------------------------------------------------------------------- /neotron-os/src/commands/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration related commands for Neotron OS 2 | 3 | use crate::{bios, config, osprintln, Ctx}; 4 | 5 | pub static COMMAND_ITEM: menu::Item = menu::Item { 6 | item_type: menu::ItemType::Callback { 7 | function: command, 8 | parameters: &[ 9 | menu::Parameter::Optional { 10 | parameter_name: "command", 11 | help: Some("Which operation to perform (try help)"), 12 | }, 13 | menu::Parameter::Optional { 14 | parameter_name: "value", 15 | help: Some("new value for the setting"), 16 | }, 17 | ], 18 | }, 19 | command: "config", 20 | help: Some("Handle non-volatile OS configuration"), 21 | }; 22 | 23 | /// Called when the "config" command is executed. 24 | fn command(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { 25 | let command = args.first().cloned().unwrap_or("print"); 26 | match command { 27 | "reset" => match config::Config::load() { 28 | Ok(new_config) => { 29 | ctx.config = new_config; 30 | osprintln!("Loaded OK."); 31 | } 32 | Err(e) => { 33 | osprintln!("Error loading; {}", e); 34 | } 35 | }, 36 | "save" => match ctx.config.save() { 37 | Ok(_) => { 38 | osprintln!("Saved OK."); 39 | } 40 | Err(e) => { 41 | osprintln!("Error saving: {}", e); 42 | } 43 | }, 44 | "vga" => match args.get(1).cloned() { 45 | Some("off") => { 46 | ctx.config.set_vga_console(None); 47 | osprintln!("VGA now off"); 48 | } 49 | Some(mode_str) => { 50 | let Some(mode) = mode_str 51 | .parse::() 52 | .ok() 53 | .and_then(bios::video::Mode::try_from_u8) 54 | .filter(|m| m.is_text_mode()) 55 | else { 56 | osprintln!("Not a valid text mode"); 57 | return; 58 | }; 59 | ctx.config.set_vga_console(Some(mode)); 60 | osprintln!("VGA set to mode {}", mode.as_u8()); 61 | } 62 | _ => { 63 | osprintln!("Give integer or off as argument"); 64 | } 65 | }, 66 | "serial" => match (args.get(1).cloned(), args.get(1).map(|s| s.parse::())) { 67 | (_, Some(Ok(baud))) => { 68 | osprintln!("Turning serial console on at {} bps", baud); 69 | ctx.config.set_serial_console_on(baud); 70 | } 71 | (Some("off"), _) => { 72 | osprintln!("Turning serial console off"); 73 | ctx.config.set_serial_console_off(); 74 | } 75 | _ => { 76 | osprintln!("Give off or an integer as argument"); 77 | } 78 | }, 79 | "print" => { 80 | match ctx.config.get_vga_console() { 81 | Some(m) => { 82 | osprintln!("VGA : Mode {}", m.as_u8()); 83 | } 84 | None => { 85 | osprintln!("VGA : off"); 86 | } 87 | }; 88 | match ctx.config.get_serial_console() { 89 | None => { 90 | osprintln!("Serial: off"); 91 | } 92 | Some((_port, config)) => { 93 | osprintln!("Serial: {} bps", config.data_rate_bps); 94 | } 95 | } 96 | } 97 | _ => { 98 | osprintln!("config print - print the config"); 99 | osprintln!("config help - print this help text"); 100 | osprintln!("config reset - load config from BIOS store"); 101 | osprintln!("config save - save config to BIOS store"); 102 | osprintln!("config vga - enable VGA in Mode "); 103 | osprintln!("config vga off - turn VGA off"); 104 | osprintln!("config serial off - turn serial console off"); 105 | osprintln!("config serial - turn serial console on with given baud rate"); 106 | } 107 | } 108 | } 109 | 110 | // End of file 111 | -------------------------------------------------------------------------------- /neotron-os/src/refcell.rs: -------------------------------------------------------------------------------- 1 | //! # RefCells for Neotron OS. 2 | //! 3 | //! Like the `RefCell` in the standard library, except that it's thread-safe 4 | //! and uses the BIOS critical section to make it so. 5 | 6 | // =========================================================================== 7 | // Modules and Imports 8 | // =========================================================================== 9 | 10 | use core::{ 11 | cell::UnsafeCell, 12 | ops::{Deref, DerefMut}, 13 | sync::atomic::{AtomicBool, Ordering}, 14 | }; 15 | 16 | // =========================================================================== 17 | // Global Variables 18 | // =========================================================================== 19 | 20 | // None 21 | 22 | // =========================================================================== 23 | // Macros 24 | // =========================================================================== 25 | 26 | // None 27 | 28 | // =========================================================================== 29 | // Public types 30 | // =========================================================================== 31 | 32 | /// Indicates a failure to lock the refcell because it was already locked. 33 | #[derive(Debug)] 34 | pub struct LockError; 35 | 36 | /// A cell that gives you references, and is thread-safe. 37 | /// 38 | /// Uses the BIOS to ensure thread-safety whilst checking if the lock is taken 39 | /// or not. 40 | pub struct CsRefCell { 41 | inner: UnsafeCell, 42 | locked: AtomicBool, 43 | } 44 | 45 | impl CsRefCell { 46 | /// Create a new cell. 47 | pub const fn new(value: T) -> CsRefCell { 48 | CsRefCell { 49 | inner: UnsafeCell::new(value), 50 | locked: AtomicBool::new(false), 51 | } 52 | } 53 | 54 | /// Lock the cell. 55 | /// 56 | /// If you can't lock it (because it is already locked), this function will panic. 57 | pub fn lock(&self) -> CsRefCellGuard { 58 | self.try_lock().unwrap() 59 | } 60 | 61 | /// Try and grab the lock. 62 | /// 63 | /// It'll fail if it's already been taken. 64 | /// 65 | /// It'll panic if the global lock is in a bad state, or you try and 66 | /// re-enter this function from an interrupt whilst the global lock is held. 67 | /// Don't do that. 68 | pub fn try_lock(&self) -> Result, LockError> { 69 | let api = crate::API.get(); 70 | 71 | if (api.compare_and_swap_bool)(&self.locked, false, true) { 72 | // succesfully swapped `false` for `true` 73 | core::sync::atomic::fence(Ordering::Acquire); 74 | Ok(CsRefCellGuard { parent: self }) 75 | } else { 76 | // cell is already locked 77 | Err(LockError) 78 | } 79 | } 80 | } 81 | 82 | /// Mark our type as thread-safe. 83 | /// 84 | /// # Safety 85 | /// 86 | /// We use the BIOS critical sections to control access. Thus it is now 87 | /// thread-safe. 88 | unsafe impl Sync for CsRefCell {} 89 | 90 | /// Represents an active borrow of a [`CsRefCell`]. 91 | pub struct CsRefCellGuard<'a, T> { 92 | parent: &'a CsRefCell, 93 | } 94 | 95 | impl Deref for CsRefCellGuard<'_, T> { 96 | type Target = T; 97 | 98 | fn deref(&self) -> &Self::Target { 99 | let ptr = self.parent.inner.get(); 100 | unsafe { &*ptr } 101 | } 102 | } 103 | 104 | impl DerefMut for CsRefCellGuard<'_, T> { 105 | fn deref_mut(&mut self) -> &mut Self::Target { 106 | let ptr = self.parent.inner.get(); 107 | unsafe { &mut *ptr } 108 | } 109 | } 110 | 111 | impl Drop for CsRefCellGuard<'_, T> { 112 | fn drop(&mut self) { 113 | // We hold this refcell guard exclusively, so this can't race 114 | self.parent.locked.store(false, Ordering::Release); 115 | } 116 | } 117 | 118 | // =========================================================================== 119 | // Private types 120 | // =========================================================================== 121 | 122 | // None 123 | 124 | // =========================================================================== 125 | // Private functions 126 | // =========================================================================== 127 | 128 | // None 129 | 130 | // =========================================================================== 131 | // Public functions 132 | // =========================================================================== 133 | 134 | // None 135 | 136 | // =========================================================================== 137 | // Tests 138 | // =========================================================================== 139 | 140 | // None 141 | 142 | // =========================================================================== 143 | // End of file 144 | // =========================================================================== 145 | -------------------------------------------------------------------------------- /nbuild/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions 2 | 3 | /// The ways that spawning `cargo` can fail 4 | #[derive(Debug)] 5 | pub enum ProcessError { 6 | SpawnError(std::io::Error), 7 | RunError(std::process::ExitStatus), 8 | } 9 | 10 | impl std::fmt::Display for ProcessError { 11 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 12 | match self { 13 | ProcessError::SpawnError(error) => write!(f, "Failed to spawn command: {}", error), 14 | ProcessError::RunError(exit_status) => write!( 15 | f, 16 | "Failed to complete command ({}). There should be an error above", 17 | exit_status 18 | ), 19 | } 20 | } 21 | } 22 | 23 | /// The kinds of package we have 24 | #[derive(Debug, PartialEq, Eq)] 25 | pub enum PackageKind { 26 | Os, 27 | Utility, 28 | NBuild, 29 | } 30 | 31 | /// Can this package be tested? 32 | #[derive(Debug, PartialEq, Eq)] 33 | pub enum Testable { 34 | No, 35 | All, 36 | Libs, 37 | } 38 | 39 | /// Describes a package in this repository 40 | #[derive(Debug)] 41 | pub struct Package { 42 | pub name: &'static str, 43 | pub path: &'static std::path::Path, 44 | pub kind: PackageKind, 45 | pub testable: Testable, 46 | pub output_template: Option<&'static str>, 47 | } 48 | 49 | impl Package { 50 | pub fn output(&self, target: &str, profile: &str) -> Option { 51 | self.output_template 52 | .map(|s| s.replace("{target}", target).replace("{profile}", profile)) 53 | } 54 | } 55 | 56 | /// Parse an integer, with an optional `0x` prefix. 57 | /// 58 | /// Underscores are ignored. 59 | /// 60 | /// ```rust 61 | /// # use nbuild::parse_int; 62 | /// assert_eq!(parse_int("0x0000_000A"), Ok(10)); 63 | /// assert_eq!(parse_int("000_10"), Ok(10)); 64 | /// ``` 65 | pub fn parse_int(input: S) -> Result 66 | where 67 | S: AsRef, 68 | { 69 | let input = input.as_ref().replace('_', ""); 70 | if let Some(suffix) = input.strip_prefix("0x") { 71 | u32::from_str_radix(suffix, 16) 72 | } else { 73 | input.parse() 74 | } 75 | } 76 | 77 | /// Runs cargo 78 | pub fn cargo

( 79 | commands: &[&str], 80 | target: Option<&str>, 81 | manifest_path: P, 82 | ) -> Result<(), ProcessError> 83 | where 84 | P: AsRef, 85 | { 86 | cargo_with_env(commands, target, manifest_path, &[]) 87 | } 88 | 89 | /// Runs cargo with extra environment variables 90 | pub fn cargo_with_env

( 91 | commands: &[&str], 92 | target: Option<&str>, 93 | manifest_path: P, 94 | environment: &[(&'static str, String)], 95 | ) -> Result<(), ProcessError> 96 | where 97 | P: AsRef, 98 | { 99 | let mut command_line = std::process::Command::new("cargo"); 100 | command_line.stdout(std::process::Stdio::inherit()); 101 | command_line.stderr(std::process::Stdio::inherit()); 102 | command_line.args(commands); 103 | if let Some(target) = target { 104 | command_line.arg("--target"); 105 | command_line.arg(target); 106 | } 107 | command_line.arg("--manifest-path"); 108 | command_line.arg(manifest_path.as_ref()); 109 | for (k, v) in environment.iter() { 110 | command_line.env(k, v); 111 | } 112 | 113 | println!("Running: {:?}", command_line); 114 | 115 | let output = command_line.output().map_err(ProcessError::SpawnError)?; 116 | 117 | if output.status.success() { 118 | Ok(()) 119 | } else { 120 | Err(ProcessError::RunError(output.status)) 121 | } 122 | } 123 | 124 | /// Make a binary version of an ELF file 125 | pub fn make_bin

(path: P) -> Result 126 | where 127 | P: AsRef, 128 | { 129 | let path = path.as_ref(); 130 | println!("Making binary of: {}", path.display()); 131 | let output = std::process::Command::new("rustc") 132 | .arg("--print") 133 | .arg("target-libdir") 134 | .output() 135 | .expect("Failed to run rustc --print target-libdir"); 136 | let sysroot = String::from_utf8(output.stdout).expect("sysroot path isn't UTF-8"); 137 | let sysroot: std::path::PathBuf = sysroot.trim().into(); 138 | let mut objcopy = sysroot.clone(); 139 | objcopy.pop(); 140 | objcopy.push("bin"); 141 | objcopy.push("llvm-objcopy"); 142 | let mut command_line = std::process::Command::new(objcopy); 143 | command_line.args(["-O", "binary"]); 144 | command_line.arg(path); 145 | let output_file = path.with_extension("bin"); 146 | command_line.arg(&output_file); 147 | println!("Running: {:?}", command_line); 148 | let output = command_line.output().map_err(ProcessError::SpawnError)?; 149 | if output.status.success() { 150 | Ok(output_file) 151 | } else { 152 | Err(ProcessError::RunError(output.status)) 153 | } 154 | } 155 | 156 | // End of file 157 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Unreleased changes ([Source](https://github.com/neotron-compute/neotron-os/tree/develop) | [Changes](https://github.com/neotron-compute/neotron-os/compare/v0.8.1...develop)) 4 | 5 | * None 6 | 7 | ## v0.8.1 - 2024-05-17 ([Source](https://github.com/neotron-compute/neotron-os/tree/v0.8.1) | [Release](https://github.com/neotron-compute/neotron-os/releases/tag/v0.8.1)) 8 | 9 | * The `run` command now takes command-line arguments 10 | * Add `ioctl` API 11 | * Updated to `neotron-api` v0.2, to provide support for both of the above 12 | * Add `AUDIO:` device, including `ioctl` to get/set sample rate and get buffer space 13 | * Implement `fstat` for files (although only file size works) 14 | 15 | ## 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)) 16 | 17 | * Added a global `FILESYSTEM` object 18 | * Updated to embedded-sdmmc 0.7 19 | * Updated to Neotron Common BIOS 0.12 20 | * Add a bitmap viewer command - `gfx` 21 | * Treat text buffer as 32-bit values 22 | 23 | ## 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)) 24 | 25 | * Update `Cargo.lock` so build string no longer shows build as *dirty* 26 | 27 | ## 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)) 28 | 29 | * Add `i2c` command. 30 | * Support printing `\t`, with 8 character tab-stops 31 | * Add `type` command to print files 32 | * Add `exec` command to execute scripts containing commands 33 | * Update `embedded-sdmmc` crate 34 | * Split `lshw` into `lsblk`, `lsbus`, `lsi2c`, `lsmem` and `lsuart` 35 | 36 | ## 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)) 37 | 38 | * Can set/set video mode 39 | * Stores video mode as part of config 40 | * Removed demo commands (they should be applications) 41 | * Added raw PCM sound playback 42 | * Added mixer command 43 | * Switch to [`neotron-common-bios`] 0.11.1 44 | 45 | ## 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)) 46 | 47 | * Switch to [`neotron-common-bios`] 0.11 48 | * Added "Shutdown" command 49 | * Added ANSI decoder for colour changes (SGI) and cursor position support 50 | * Added 'standard input' support for applications 51 | * Use new compare-and-swap BIOS API to implement mutexes, instead of `static mut` 52 | * OS now requires 256K Flash space 53 | 54 | ## 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)) 55 | 56 | * The `load` command now takes ELF binaries, not raw binaries. 57 | * Neotron OS can now be used as a dependency within an application, if desired. 58 | 59 | ## 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)) 60 | 61 | * Add `dir` command 62 | * Change `load` command to load from disk 63 | * Repository includes `Cargo.lock` file 64 | * Update to `postcard` 1.0 65 | * Fix `readblk` help text, and print 32 bytes per line 66 | 67 | ## 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)) 68 | 69 | * Add `date` command. 70 | * Add `lsblk` and `blkread` commands. 71 | * Renamed `bioshw` to `lshw` 72 | 73 | ## 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)) 74 | 75 | * Add `hexdump`, `load` and `run` commands. 76 | * Set colour attributes correctly (White on Black only currently) 77 | 78 | ## 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)) 79 | 80 | * Updated to [`neotron-common-bios`] v0.8.0 81 | * Use [`pc-keyboard`] for decoding HID events 82 | * Fix Windows library build 83 | * Added 'kbtest' command 84 | * Added 'lshw' command 85 | * Added 'config' command 86 | * Uses BIOS to store/load OS configuration 87 | 88 | [`neotron-common-bios`]: https://crates.io/crates/neotron-common-bios 89 | [`pc-keyboard`]: https://crates.io/crates/pc-keyboard 90 | 91 | ## 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)) 92 | 93 | Adds HID support and basic shell, with 'mem' and 'fill' commands. 94 | 95 | ## 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)) 96 | 97 | First version. 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neotron OS 2 | 3 | This is the Neotron OS. It will run on any system which has an implementation 4 | of the [Neotron BIOS](https://github.com/neotron-compute/Neotron-Common-BIOS). 5 | 6 | ## Status 7 | 8 | This OS is a work in progress. We intend to support: 9 | 10 | * [x] Calling BIOS APIs 11 | * [x] Text mode VGA console 12 | * [x] Serial console 13 | * [x] Running built-in commands from a shell 14 | * [x] Executing applications from RAM 15 | * [x] Applications can print to stdout 16 | * [x] Applications can read from stdin 17 | * [x] Applications can open/close/read files 18 | * [ ] Applications can write to files 19 | * [x] MBR/FAT32 formatted block devices 20 | * [x] Read blocks 21 | * [x] Directory listing of / 22 | * [ ] Write to files 23 | * [ ] Delete files 24 | * [ ] Change directory 25 | * [x] Load ELF binaries from disk 26 | * [x] Load ELF binaries from ROM 27 | * [x] Changing text modes 28 | * [ ] Basic networking 29 | * [x] Music playback 30 | * [ ] Various keyboard layouts 31 | * [ ] Ethernet / WiFi networking 32 | * [ ] Built-in scripting language 33 | 34 | ## Build instructions 35 | 36 | Your board will need an appropriate Neotron BIOS installed, and you need to have 37 | OpenOCD or some other programming tool running for your particular board. See 38 | your BIOS instructions for more details. 39 | 40 | Building Neotron OS is handled by the `nbuild` tool, in this repository. Run `cargo nbuild help` for more information. 41 | 42 | To make an image for a board like the Neotron Pico, you want to run `cargo nbuild binary`. By default this will produce a `thumbv6m-none-eabi` image linked to run at address `0x1002_0000`, with a ROMFS containing various utilities, which is what you need on a Neotron Pico. Your BIOS should tell you if you need to change these options, and how to load the resulting image onto your system. 43 | 44 | ```console 45 | $ cargo nbuild binary 46 | ... 47 | $ ls ./target/thumbv6m-none-eabi/release 48 | build/ examples/ flames.d libflames.d libneotron_os.d neotron-os neotron-os.d 49 | deps/ flames incremental/ libflames.rlib libneotron_os.rlib neotron-os.bin romfs.bin 50 | ``` 51 | 52 | Here: 53 | 54 | * `romfs.bin` is the raw ROMFS image 55 | * `neotron-os` is an ELF file containing the OS and the ROMFS image 56 | * `neotron-os.bin` is an raw binary copy of the contents of the ELF file 57 | 58 | When the OS is running, programs in the ROMFS can be loaded with: 59 | 60 | ```text 61 | > rom 62 | flames (14212 bytes) 63 | > rom flames 64 | Loading 4256 bytes to 0x20001000 65 | Loading 532 bytes to 0x200020a0 66 | Loading 4908 bytes to 0x200022b4 67 | > run 68 | *Program starts running** 69 | ``` 70 | 71 | A better UI for loading files from ROM is being planned (maybe we should have drive letters, and the ROM can be `R:`). 72 | 73 | You can also build a *shared object* to load into a Windows/Linux/macOS application, like [Neotron Desktop BIOS](https://github.com/neotron-compute/neotron-desktop-bios): 74 | 75 | ```console 76 | $ cargo nbuild library 77 | ... 78 | $ ls ./target/debug/*.so 79 | ./target/debug/libneotron_os.so 80 | ``` 81 | 82 | ## Changelog 83 | 84 | See [`CHANGELOG.md`](./CHANGELOG.md) 85 | 86 | ## Licence 87 | 88 | ```text 89 | Copyright (c) 2019-2024 Jonathan 'theJPster' Pallant and The Neotron Developers 90 | 91 | This program is free software: you can redistribute it and/or modify 92 | it under the terms of the GNU General Public License as published by 93 | the Free Software Foundation, either version 3 of the License, or 94 | (at your option) any later version. 95 | 96 | This program is distributed in the hope that it will be useful, 97 | but WITHOUT ANY WARRANTY; without even the implied warranty of 98 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 99 | GNU General Public License for more details. 100 | 101 | You should have received a copy of the GNU General Public License 102 | along with this program. If not, see . 103 | ``` 104 | 105 | See the full text in [LICENSE.txt](./LICENSE.txt). Broadly, we (the developers) 106 | interpret this to mean (and note that we are not lawyers and this is not 107 | legal advice) that if you give someone a Neotron computer, you must also give them 108 | one of: 109 | 110 | * Complete and corresponding source code (e.g. on disk, or as a link to your 111 | **own** on-line Git repo) for any GPL components (e.g. the BIOS and the OS), 112 | as supplied on the Neotron computer. 113 | * A written offer to provide complete and corresponding source code on 114 | request. 115 | 116 | If you are not offering a Neotron computer commercially (i.e. you are not 117 | selling a board for commercial gain), and you are using an unmodified upstream 118 | version of the source code, then the third option is to give them: 119 | 120 | * A link to the tag/commit-hash on the relevant official Neotron Github 121 | repository - . 122 | 123 | This is to ensure everyone always has the freedom to access the source code in 124 | their Neotron based computer. 125 | 126 | ## Contribution 127 | 128 | Unless you explicitly state otherwise, any contribution intentionally 129 | submitted for inclusion in the work by you shall be licensed as above, without 130 | any additional terms or conditions. 131 | 132 | 133 | -------------------------------------------------------------------------------- /utilities/flames/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Game logic for the flames demo 2 | 3 | #![no_std] 4 | #![deny(missing_docs)] 5 | #![deny(unsafe_code)] 6 | 7 | use core::fmt::Write; 8 | 9 | /// Represents the Flames application 10 | pub struct App { 11 | seed: usize, 12 | width: usize, 13 | height: usize, 14 | buffer: [u8; Self::FLAME_BUFFER_LEN], 15 | stdout: neotron_sdk::File, 16 | stdin: neotron_sdk::File, 17 | bold: bool, 18 | colour: u8, 19 | } 20 | 21 | impl App { 22 | const MAX_WIDTH: usize = 80; 23 | const MAX_HEIGHT: usize = 60; 24 | const SIZE: usize = Self::MAX_WIDTH * Self::MAX_HEIGHT; 25 | const FLAME_BUFFER_LEN: usize = Self::SIZE + Self::MAX_WIDTH + 1; 26 | 27 | /// Make a new flames application. 28 | /// 29 | /// You can give the screen size in characters. 30 | pub const fn new(width: u8, height: u8) -> App { 31 | let width = if width as usize > Self::MAX_WIDTH { 32 | Self::MAX_WIDTH 33 | } else { 34 | width as usize 35 | }; 36 | let height = if height as usize > Self::MAX_HEIGHT { 37 | Self::MAX_HEIGHT 38 | } else { 39 | height as usize 40 | }; 41 | App { 42 | seed: 123456789, 43 | width, 44 | height, 45 | buffer: [0u8; Self::FLAME_BUFFER_LEN], 46 | stdout: neotron_sdk::stdout(), 47 | stdin: neotron_sdk::stdin(), 48 | bold: false, 49 | colour: 37, 50 | } 51 | } 52 | 53 | /// Run the flames demo 54 | pub fn play(&mut self) { 55 | neotron_sdk::console::cursor_off(&mut self.stdout); 56 | neotron_sdk::console::clear_screen(&mut self.stdout); 57 | loop { 58 | self.draw_fire(); 59 | let mut buffer = [0u8; 1]; 60 | if let Ok(1) = self.stdin.read(&mut buffer) { 61 | break; 62 | } 63 | neotron_sdk::delay(core::time::Duration::from_millis(17)); 64 | } 65 | writeln!(self.stdout, "Bye!").unwrap(); 66 | neotron_sdk::console::cursor_on(&mut self.stdout); 67 | } 68 | 69 | /// Draws a flame effect. 70 | /// Based on https://gist.github.com/msimpson/1096950. 71 | fn draw_fire(&mut self) { 72 | const CHARS: [char; 10] = [' ', '`', ':', '^', '*', 'x', '░', '▒', '▓', '█']; 73 | const COLOURS: [(bool, u8); 16] = [ 74 | (true, 37), 75 | (true, 37), 76 | (true, 37), 77 | (true, 37), 78 | (true, 37), 79 | (true, 33), 80 | (true, 33), 81 | (true, 33), 82 | (true, 33), 83 | (true, 33), 84 | (true, 31), 85 | (true, 31), 86 | (true, 31), 87 | (true, 31), 88 | (true, 31), 89 | (false, 35), 90 | ]; 91 | neotron_sdk::console::move_cursor( 92 | &mut self.stdout, 93 | neotron_sdk::console::Position::origin(), 94 | ); 95 | // Seed the fire just off-screen 96 | for _i in 0..5 { 97 | let idx = (self.width * self.height) + self.random_up_to(self.width - 1); 98 | self.buffer[idx] = 100; 99 | } 100 | // Cascade the flames 101 | for idx in 0..(self.width * (self.height + 1)) { 102 | self.buffer[idx] = ((u32::from(self.buffer[idx]) 103 | + u32::from(self.buffer[idx + 1]) 104 | + u32::from(self.buffer[idx + self.width]) 105 | + u32::from(self.buffer[idx + self.width + 1])) 106 | / 4) as u8; 107 | let glyph = CHARS 108 | .get(self.buffer[idx] as usize) 109 | .unwrap_or(CHARS.last().unwrap()); 110 | let colour = COLOURS 111 | .get(self.buffer[idx] as usize) 112 | .unwrap_or(COLOURS.last().unwrap()); 113 | // Only draw what is on screen 114 | if idx < (self.width * self.height) { 115 | self.set_colour(colour.0, colour.1); 116 | write!(self.stdout, "{}", glyph).unwrap(); 117 | } 118 | } 119 | } 120 | 121 | /// Set the colour of any future text 122 | fn set_colour(&mut self, bold: bool, colour: u8) { 123 | if self.bold != bold { 124 | self.bold = bold; 125 | if bold { 126 | write!(self.stdout, "\u{001b}[1m").unwrap(); 127 | } else { 128 | write!(self.stdout, "\u{001b}[22m").unwrap(); 129 | } 130 | } 131 | if self.colour != colour { 132 | self.colour = colour; 133 | write!(self.stdout, "\u{001b}[{}m", colour).unwrap(); 134 | } 135 | } 136 | 137 | /// Generates a number in the range [0, limit) 138 | fn random_up_to(&mut self, limit: usize) -> usize { 139 | let buckets = usize::MAX / limit; 140 | let upper_edge = buckets * limit; 141 | loop { 142 | let attempt = self.random(); 143 | if attempt < upper_edge { 144 | return attempt / buckets; 145 | } 146 | } 147 | } 148 | 149 | /// Generate a random 32-bit number 150 | fn random(&mut self) -> usize { 151 | self.seed = (self.seed.wrapping_mul(1103515245)).wrapping_add(12345); 152 | self.seed 153 | } 154 | } 155 | 156 | // End of file 157 | -------------------------------------------------------------------------------- /neotron-os/neotron-os-arm.ld: -------------------------------------------------------------------------------- 1 | /* # Developer notes 2 | 3 | - Symbols that start with a double underscore (__) are considered "private" 4 | 5 | - Symbols that start with a single underscore (_) are considered "semi-public"; they can be 6 | overridden in a user linker script, but should not be referred from user code (e.g. `extern "C" { 7 | static mut __sbss }`). 8 | 9 | - `EXTERN` forces the linker to keep a symbol in the final binary. We use this to make sure a 10 | symbol if not dropped if it appears in or near the front of the linker arguments and "it's not 11 | needed" by any of the preceding objects (linker arguments) 12 | 13 | - `PROVIDE` is used to provide default values that can be overridden by a user linker script 14 | 15 | - On alignment: it's important for correctness that the VMA boundaries of both .bss and .data *and* 16 | the LMA of .data are all 4-byte aligned. These alignments are assumed by the RAM initialization 17 | routine. There's also a second benefit: 4-byte aligned boundaries means that you won't see 18 | "Address (..) is out of bounds" in the disassembly produced by `objdump`. 19 | */ 20 | 21 | /* Provides information about the memory layout of the device */ 22 | MEMORY 23 | { 24 | /* The BIOS is before the OS, we get the rest. We place a large upper bound on the length. */ 25 | FLASH (rx) : ORIGIN = ${{start_address}}, LENGTH = 4096K 26 | /* 27 | * We get the bottom 4KB of RAM. Anything above that is for applications 28 | * (up to wherever the BIOS tells us we can use.) 29 | */ 30 | RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 4K 31 | } 32 | 33 | /* # Entry point = what the BIOS calls to start the OS */ 34 | ENTRY(os_main); 35 | 36 | /* 37 | Where the Transient Program Area starts. 38 | */ 39 | _tpa_start = ORIGIN(RAM) + LENGTH(RAM); 40 | 41 | /* # Sections */ 42 | SECTIONS 43 | { 44 | 45 | /* ## Sections in FLASH */ 46 | .entry_point ORIGIN(FLASH) : 47 | { 48 | KEEP(*(.entry_point)) 49 | } > FLASH 50 | 51 | PROVIDE(_stext = ADDR(.entry_point) + SIZEOF(.entry_point)); 52 | 53 | /* ### .text */ 54 | .text _stext : 55 | { 56 | *(.text .text.*); 57 | *(.HardFaultTrampoline); 58 | *(.HardFault.*); 59 | } > FLASH 60 | 61 | /* ### .rodata */ 62 | .rodata : ALIGN(4) 63 | { 64 | *(.rodata .rodata.*); 65 | 66 | /* 4-byte align the end (VMA) of this section. 67 | This is required by LLD to ensure the LMA of the following .data 68 | section will have the correct alignment. */ 69 | . = ALIGN(4); 70 | } > FLASH 71 | 72 | /* ## Sections in RAM */ 73 | /* ### .data */ 74 | .data : ALIGN(4) 75 | { 76 | . = ALIGN(4); 77 | __sdata = .; 78 | *(.data .data.*); 79 | . = ALIGN(4); /* 4-byte align the end (VMA) of this section */ 80 | __edata = .; 81 | } > RAM AT > FLASH 82 | 83 | /* LMA of .data */ 84 | __sidata = LOADADDR(.data); 85 | 86 | /* ### .bss */ 87 | .bss : ALIGN(4) 88 | { 89 | . = ALIGN(4); 90 | __sbss = .; 91 | *(.bss .bss.*); 92 | . = ALIGN(4); /* 4-byte align the end (VMA) of this section */ 93 | __ebss = .; 94 | } > RAM 95 | 96 | /* ### .uninit */ 97 | .uninit (NOLOAD) : ALIGN(4) 98 | { 99 | . = ALIGN(4); 100 | *(.uninit .uninit.*); 101 | . = ALIGN(4); 102 | } > RAM 103 | 104 | /* Place the heap right after `.uninit` */ 105 | . = ALIGN(4); 106 | __sheap = .; 107 | 108 | /* ## .got */ 109 | /* Dynamic relocations are unsupported. This section is only used to detect relocatable code in 110 | the input files and raise an error if relocatable code is found */ 111 | .got (NOLOAD) : 112 | { 113 | KEEP(*(.got .got.*)); 114 | } 115 | 116 | /* ## Discarded sections */ 117 | /DISCARD/ : 118 | { 119 | /* Unused exception related info that only wastes space */ 120 | *(.ARM.exidx); 121 | *(.ARM.exidx.*); 122 | *(.ARM.extab.*); 123 | } 124 | } 125 | 126 | /* Do not exceed this mark in the error messages below | */ 127 | /* # Alignment checks */ 128 | ASSERT(ORIGIN(FLASH) % 4 == 0, " 129 | ERROR(cortex-m-rt): the start of the FLASH region must be 4-byte aligned"); 130 | 131 | ASSERT(ORIGIN(RAM) % 4 == 0, " 132 | ERROR(cortex-m-rt): the start of the RAM region must be 4-byte aligned"); 133 | 134 | ASSERT(__sdata % 4 == 0 && __edata % 4 == 0, " 135 | BUG(cortex-m-rt): .data is not 4-byte aligned"); 136 | 137 | ASSERT(__sidata % 4 == 0, " 138 | BUG(cortex-m-rt): the LMA of .data is not 4-byte aligned"); 139 | 140 | ASSERT(__sbss % 4 == 0 && __ebss % 4 == 0, " 141 | BUG(cortex-m-rt): .bss is not 4-byte aligned"); 142 | 143 | ASSERT(__sheap % 4 == 0, " 144 | BUG(cortex-m-rt): start of .heap is not 4-byte aligned"); 145 | 146 | /* # Position checks */ 147 | 148 | /* ## .text */ 149 | ASSERT(_stext + SIZEOF(.text) < ORIGIN(FLASH) + LENGTH(FLASH), " 150 | ERROR(cortex-m-rt): The .text section must be placed inside the FLASH memory. 151 | Set _stext to an address smaller than 'ORIGIN(FLASH) + LENGTH(FLASH)'"); 152 | 153 | /* # Other checks */ 154 | ASSERT(SIZEOF(.got) == 0, " 155 | ERROR(cortex-m-rt): .got section detected in the input object files 156 | Dynamic relocations are not supported. If you are linking to C code compiled using 157 | the 'cc' crate then modify your build script to compile the C code _without_ 158 | the -fPIC flag. See the documentation of the `cc::Build.pic` method for details."); 159 | /* Do not exceed this mark in the error messages above | */ 160 | -------------------------------------------------------------------------------- /neotron-os/src/commands/sound.rs: -------------------------------------------------------------------------------- 1 | //! Sound related commands for Neotron OS 2 | 3 | use crate::{bios, osprint, osprintln, Ctx, API, FILESYSTEM}; 4 | 5 | pub static MIXER_ITEM: menu::Item = menu::Item { 6 | item_type: menu::ItemType::Callback { 7 | function: mixer, 8 | parameters: &[ 9 | menu::Parameter::Optional { 10 | parameter_name: "mixer", 11 | help: Some("Which mixer to adjust"), 12 | }, 13 | menu::Parameter::Optional { 14 | parameter_name: "level", 15 | help: Some("New level for this mixer, as an integer."), 16 | }, 17 | ], 18 | }, 19 | command: "mixer", 20 | help: Some("Control the audio mixer"), 21 | }; 22 | 23 | pub static PLAY_ITEM: menu::Item = menu::Item { 24 | item_type: menu::ItemType::Callback { 25 | function: play, 26 | parameters: &[menu::Parameter::Mandatory { 27 | parameter_name: "filename", 28 | help: Some("Which file to play"), 29 | }], 30 | }, 31 | command: "play", 32 | help: Some("Play a raw 16-bit LE 48 kHz stereo file"), 33 | }; 34 | 35 | /// Called when the "mixer" command is executed. 36 | fn mixer(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { 37 | let selected_mixer = menu::argument_finder(item, args, "mixer").unwrap(); 38 | let level_str = menu::argument_finder(item, args, "level").unwrap(); 39 | 40 | let level_int = if let Some(level_str) = level_str { 41 | let Ok(value) = level_str.parse::() else { 42 | osprintln!("{} is not an integer", level_str); 43 | return; 44 | }; 45 | Some(value) 46 | } else { 47 | None 48 | }; 49 | 50 | let mixer_int = selected_mixer.and_then(|n| n.parse::().ok()); 51 | 52 | let api = API.get(); 53 | 54 | if let (Some(selected_mixer), Some(level_int)) = (selected_mixer, level_int) { 55 | let mut found = false; 56 | for mixer_id in 0u8..=255u8 { 57 | match (api.audio_mixer_channel_get_info)(mixer_id) { 58 | bios::FfiOption::Some(mixer_info) => { 59 | if (Some(mixer_id) == mixer_int) || (mixer_info.name.as_str() == selected_mixer) 60 | { 61 | if let Err(e) = 62 | (api.audio_mixer_channel_set_level)(mixer_id, level_int).into() 63 | { 64 | osprintln!( 65 | "Failed to set mixer {:?} (id {}) to {}: {:?}", 66 | selected_mixer, 67 | mixer_id, 68 | level_int, 69 | e 70 | ); 71 | } 72 | found = true; 73 | break; 74 | } 75 | } 76 | bios::FfiOption::None => { 77 | break; 78 | } 79 | } 80 | } 81 | 82 | if !found { 83 | osprintln!("Don't know mixer {:?}", selected_mixer); 84 | } 85 | } 86 | 87 | osprintln!("Mixers:"); 88 | for mixer_id in 0u8..=255u8 { 89 | match (api.audio_mixer_channel_get_info)(mixer_id) { 90 | bios::FfiOption::Some(mixer_info) => { 91 | let dir_str = match mixer_info.direction.make_safe() { 92 | Ok(bios::audio::Direction::Input) => "In", 93 | Ok(bios::audio::Direction::Loopback) => "Loop", 94 | Ok(bios::audio::Direction::Output) => "Out", 95 | _ => "??", 96 | }; 97 | if (Some(mixer_id) == mixer_int) 98 | || selected_mixer 99 | .map(|s| s == mixer_info.name.as_str()) 100 | .unwrap_or(true) 101 | { 102 | osprintln!( 103 | "\t{}: {} ({}) {}/{}", 104 | mixer_id, 105 | mixer_info.name, 106 | dir_str, 107 | mixer_info.current_level, 108 | mixer_info.max_level 109 | ); 110 | } 111 | } 112 | bios::FfiOption::None => { 113 | // Run out of mixers 114 | break; 115 | } 116 | } 117 | } 118 | } 119 | 120 | /// Called when the "play" command is executed. 121 | fn play(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { 122 | fn play_inner(file_name: &str, scratch: &mut [u8]) -> Result<(), crate::fs::Error> { 123 | osprintln!("Loading /{} from Block Device 0", file_name); 124 | let file = FILESYSTEM.open_file(file_name, embedded_sdmmc::Mode::ReadOnly)?; 125 | 126 | osprintln!("Press Q to quit, P to pause/unpause..."); 127 | 128 | let api = API.get(); 129 | 130 | let buffer = &mut scratch[0..4096]; 131 | let mut bytes = 0; 132 | let mut delta = 0; 133 | 134 | let mut pause = false; 135 | 136 | 'playback: while !file.is_eof() { 137 | if !pause { 138 | let bytes_read = file.read(buffer)?; 139 | let mut buffer = &buffer[0..bytes_read]; 140 | while !buffer.is_empty() { 141 | let slice = bios::FfiByteSlice::new(buffer); 142 | let played = unsafe { (api.audio_output_data)(slice).unwrap() }; 143 | buffer = &buffer[played..]; 144 | delta += played; 145 | if delta > 48000 { 146 | bytes += delta; 147 | delta = 0; 148 | let milliseconds = bytes / ((48000 / 1000) * 4); 149 | osprint!( 150 | "\rPlayed: {}.{:03} s", 151 | milliseconds / 1000, 152 | milliseconds % 1000 153 | ); 154 | } 155 | } 156 | } 157 | 158 | let mut buffer = [0u8; 16]; 159 | let count = { crate::STD_INPUT.lock().get_data(&mut buffer) }; 160 | for b in &buffer[0..count] { 161 | if *b == b'q' || *b == b'Q' { 162 | osprintln!("\nQuitting playback!"); 163 | break 'playback; 164 | } else if (*b == b'p' || *b == b'P') && pause { 165 | pause = false; 166 | } else if (*b == b'p' || *b == b'P') && !pause { 167 | let milliseconds = bytes / ((48000 / 1000) * 4); 168 | osprint!( 169 | "\rPaused: {}.{:03} s", 170 | milliseconds / 1000, 171 | milliseconds % 1000 172 | ); 173 | pause = true; 174 | } 175 | } 176 | } 177 | osprintln!(); 178 | Ok(()) 179 | } 180 | 181 | if let Err(e) = play_inner(args[0], ctx.tpa.as_slice_u8()) { 182 | osprintln!("\nError during playback: {:?}", e); 183 | } 184 | } 185 | 186 | // End of file 187 | -------------------------------------------------------------------------------- /neotron-os/src/commands/fs.rs: -------------------------------------------------------------------------------- 1 | //! File Systems related commands for Neotron OS 2 | 3 | use crate::{osprint, osprintln, Ctx, FILESYSTEM}; 4 | 5 | pub static DIR_ITEM: menu::Item = menu::Item { 6 | item_type: menu::ItemType::Callback { 7 | function: dir, 8 | parameters: &[], 9 | }, 10 | command: "dir", 11 | help: Some("Dir the root directory on block device 0"), 12 | }; 13 | 14 | pub static LOAD_ITEM: menu::Item = menu::Item { 15 | item_type: menu::ItemType::Callback { 16 | function: load, 17 | parameters: &[menu::Parameter::Mandatory { 18 | parameter_name: "file", 19 | help: Some("The file to load"), 20 | }], 21 | }, 22 | command: "load", 23 | help: Some("Load a file into the application area"), 24 | }; 25 | 26 | pub static EXEC_ITEM: menu::Item = menu::Item { 27 | item_type: menu::ItemType::Callback { 28 | function: exec, 29 | parameters: &[menu::Parameter::Mandatory { 30 | parameter_name: "file", 31 | help: Some("The shell script to run"), 32 | }], 33 | }, 34 | command: "exec", 35 | help: Some("Execute a shell script"), 36 | }; 37 | 38 | pub static TYPE_ITEM: menu::Item = menu::Item { 39 | item_type: menu::ItemType::Callback { 40 | function: typefn, 41 | parameters: &[menu::Parameter::Mandatory { 42 | parameter_name: "file", 43 | help: Some("The file to type"), 44 | }], 45 | }, 46 | command: "type", 47 | help: Some("Type a file to the console"), 48 | }; 49 | 50 | pub static ROM_ITEM: menu::Item = menu::Item { 51 | item_type: menu::ItemType::Callback { 52 | function: romfn, 53 | parameters: &[menu::Parameter::Optional { 54 | parameter_name: "file", 55 | help: Some("The ROM utility to run"), 56 | }], 57 | }, 58 | command: "rom", 59 | help: Some("Run a program from ROM"), 60 | }; 61 | 62 | /// Called when the "dir" command is executed. 63 | fn dir(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { 64 | fn work() -> Result<(), crate::fs::Error> { 65 | osprintln!("Listing files on Block Device 0, /"); 66 | let mut total_bytes = 0; 67 | let mut num_files = 0; 68 | FILESYSTEM.iterate_root_dir(|dir_entry| { 69 | let padding = 8 - dir_entry.name.base_name().len(); 70 | for b in dir_entry.name.base_name() { 71 | let ch = *b as char; 72 | osprint!("{}", if ch.is_ascii_graphic() { ch } else { '?' }); 73 | } 74 | for _ in 0..padding { 75 | osprint!(" "); 76 | } 77 | osprint!(" "); 78 | let padding = 3 - dir_entry.name.extension().len(); 79 | for b in dir_entry.name.extension() { 80 | let ch = *b as char; 81 | osprint!("{}", if ch.is_ascii_graphic() { ch } else { '?' }); 82 | } 83 | for _ in 0..padding { 84 | osprint!(" "); 85 | } 86 | if dir_entry.attributes.is_directory() { 87 | osprint!("

"); 88 | } else { 89 | osprint!(" {:-13}", dir_entry.size,); 90 | } 91 | osprint!( 92 | " {:02}/{:02}/{:04}", 93 | dir_entry.mtime.zero_indexed_day + 1, 94 | dir_entry.mtime.zero_indexed_month + 1, 95 | u32::from(dir_entry.mtime.year_since_1970) + 1970 96 | ); 97 | osprintln!( 98 | " {:02}:{:02}", 99 | dir_entry.mtime.hours, 100 | dir_entry.mtime.minutes 101 | ); 102 | total_bytes += dir_entry.size as u64; 103 | num_files += 1; 104 | })?; 105 | osprintln!("{:-9} file(s) {:-13} bytes", num_files, total_bytes); 106 | Ok(()) 107 | } 108 | 109 | match work() { 110 | Ok(_) => {} 111 | Err(e) => { 112 | osprintln!("Error: {:?}", e); 113 | } 114 | } 115 | } 116 | 117 | /// Called when the "load" command is executed. 118 | fn load(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { 119 | let Some(filename) = args.first() else { 120 | osprintln!("Need a filename"); 121 | return; 122 | }; 123 | if let Err(e) = ctx.tpa.load_program(filename) { 124 | osprintln!("Error: {:?}", e); 125 | } 126 | } 127 | 128 | /// Called when the "exec" command is executed. 129 | fn exec(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { 130 | fn work(ctx: &mut Ctx, filename: &str) -> Result<(), crate::fs::Error> { 131 | let file = FILESYSTEM.open_file(filename, embedded_sdmmc::Mode::ReadOnly)?; 132 | let buffer = ctx.tpa.as_slice_u8(); 133 | let count = file.read(buffer)?; 134 | if count != file.length() as usize { 135 | osprintln!("File too large! Max {} bytes allowed.", buffer.len()); 136 | return Ok(()); 137 | } 138 | let Ok(s) = core::str::from_utf8(&buffer[0..count]) else { 139 | osprintln!("File is not valid UTF-8"); 140 | return Ok(()); 141 | }; 142 | // tell the main loop to run from these bytes next 143 | ctx.exec_tpa = Some(s.len()); 144 | Ok(()) 145 | } 146 | 147 | // index can't panic - we always have enough args 148 | let r = work(ctx, args[0]); 149 | match r { 150 | Ok(_) => {} 151 | Err(e) => { 152 | osprintln!("Error: {:?}", e); 153 | } 154 | } 155 | } 156 | 157 | /// Called when the "type" command is executed. 158 | fn typefn(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { 159 | fn work(ctx: &mut Ctx, filename: &str) -> Result<(), crate::fs::Error> { 160 | let file = FILESYSTEM.open_file(filename, embedded_sdmmc::Mode::ReadOnly)?; 161 | let buffer = ctx.tpa.as_slice_u8(); 162 | let count = file.read(buffer)?; 163 | if count != file.length() as usize { 164 | osprintln!("File too large! Max {} bytes allowed.", buffer.len()); 165 | return Ok(()); 166 | } 167 | let Ok(s) = core::str::from_utf8(&buffer[0..count]) else { 168 | osprintln!("File is not valid UTF-8"); 169 | return Ok(()); 170 | }; 171 | osprintln!("{}", s); 172 | Ok(()) 173 | } 174 | 175 | // index can't panic - we always have enough args 176 | let r = work(ctx, args[0]); 177 | // reset SGR 178 | osprint!("\u{001b}[0m"); 179 | match r { 180 | Ok(_) => {} 181 | Err(e) => { 182 | osprintln!("Error: {:?}", e); 183 | } 184 | } 185 | } 186 | 187 | /// Called when the "romfn" command is executed. 188 | fn romfn(_menu: &menu::Menu, _item: &menu::Item, args: &[&str], ctx: &mut Ctx) { 189 | let Ok(romfs) = neotron_romfs::RomFs::new(crate::ROMFS) else { 190 | osprintln!("No ROM available"); 191 | return; 192 | }; 193 | if let Some(arg) = args.first() { 194 | let Some(entry) = romfs.find(arg) else { 195 | osprintln!("Couldn't find {} in ROM", arg); 196 | return; 197 | }; 198 | if let Err(e) = ctx.tpa.load_rom_program(entry.contents) { 199 | osprintln!("Error: {:?}", e); 200 | } 201 | } else { 202 | for entry in romfs.into_iter().flatten() { 203 | osprintln!( 204 | "{} ({} bytes)", 205 | entry.metadata.file_name, 206 | entry.metadata.file_size 207 | ); 208 | } 209 | } 210 | } 211 | 212 | // End of file 213 | -------------------------------------------------------------------------------- /neotron-os/src/commands/screen.rs: -------------------------------------------------------------------------------- 1 | //! Screen-related commands for Neotron OS 2 | 3 | use crate::{ 4 | bios::{ 5 | video::{Format, Mode}, 6 | ApiResult, 7 | }, 8 | osprint, osprintln, Ctx, 9 | }; 10 | 11 | pub static CLS_ITEM: menu::Item = menu::Item { 12 | item_type: menu::ItemType::Callback { 13 | function: cls_cmd, 14 | parameters: &[], 15 | }, 16 | command: "cls", 17 | help: Some("Clear the screen"), 18 | }; 19 | 20 | pub static MODE_ITEM: menu::Item = menu::Item { 21 | item_type: menu::ItemType::Callback { 22 | function: mode_cmd, 23 | parameters: &[menu::Parameter::Optional { 24 | parameter_name: "new_mode", 25 | help: Some("The new text mode to change to"), 26 | }], 27 | }, 28 | command: "mode", 29 | help: Some("List/change video mode"), 30 | }; 31 | 32 | pub static GFX_ITEM: menu::Item = menu::Item { 33 | item_type: menu::ItemType::Callback { 34 | function: gfx_cmd, 35 | parameters: &[ 36 | menu::Parameter::Mandatory { 37 | parameter_name: "new_mode", 38 | help: Some("The new gfx mode to try"), 39 | }, 40 | menu::Parameter::Optional { 41 | parameter_name: "filename", 42 | help: Some("a file to display"), 43 | }, 44 | ], 45 | }, 46 | command: "gfx", 47 | help: Some("Test a graphics mode"), 48 | }; 49 | 50 | /// Called when the "cls" command is executed. 51 | fn cls_cmd(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { 52 | // Reset SGR, go home, clear screen, 53 | osprint!("\u{001b}[0m\u{001b}[1;1H\u{001b}[2J"); 54 | } 55 | 56 | /// Called when the "mode" command is executed 57 | fn mode_cmd(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { 58 | if let Some(new_mode) = menu::argument_finder(item, args, "new_mode").unwrap() { 59 | let Ok(mode_num) = new_mode.parse::() else { 60 | osprintln!("Invalid integer {:?}", new_mode); 61 | return; 62 | }; 63 | let Some(mode) = Mode::try_from_u8(mode_num) else { 64 | osprintln!("Invalid mode {:?}", new_mode); 65 | return; 66 | }; 67 | let has_vga = { 68 | let mut guard = crate::VGA_CONSOLE.lock(); 69 | guard.as_mut().is_some() 70 | }; 71 | if !has_vga { 72 | osprintln!("No VGA console."); 73 | return; 74 | } 75 | let api = crate::API.get(); 76 | match mode.format() { 77 | Format::Text8x16 => {} 78 | Format::Text8x8 => {} 79 | _ => { 80 | osprintln!("Not a text mode?"); 81 | return; 82 | } 83 | } 84 | if (api.video_mode_needs_vram)(mode) { 85 | // The OS currently has no VRAM for text modes 86 | osprintln!("That mode requires more VRAM than the BIOS has."); 87 | return; 88 | } 89 | // # Safety 90 | // 91 | // It's always OK to pass NULl to this API. 92 | match unsafe { (api.video_set_mode)(mode, core::ptr::null_mut()) } { 93 | ApiResult::Ok(_) => { 94 | let mut guard = crate::VGA_CONSOLE.lock(); 95 | if let Some(console) = guard.as_mut() { 96 | console.change_mode(mode); 97 | } 98 | osprintln!("Now in mode {}", mode.as_u8()); 99 | } 100 | ApiResult::Err(e) => { 101 | osprintln!("Failed to change mode: {:?}", e); 102 | } 103 | } 104 | } else { 105 | print_modes(); 106 | } 107 | } 108 | 109 | /// Called when the "gfx" command is executed 110 | fn gfx_cmd(_menu: &menu::Menu, item: &menu::Item, args: &[&str], ctx: &mut Ctx) { 111 | let Some(new_mode) = menu::argument_finder(item, args, "new_mode").unwrap() else { 112 | osprintln!("Missing arg"); 113 | return; 114 | }; 115 | let file_name = menu::argument_finder(item, args, "filename").unwrap(); 116 | let Ok(mode_num) = new_mode.parse::() else { 117 | osprintln!("Invalid integer {:?}", new_mode); 118 | return; 119 | }; 120 | let Some(mode) = Mode::try_from_u8(mode_num) else { 121 | osprintln!("Invalid mode {:?}", new_mode); 122 | return; 123 | }; 124 | let api = crate::API.get(); 125 | let old_mode = (api.video_get_mode)(); 126 | let old_ptr = (api.video_get_framebuffer)(); 127 | 128 | let buffer = ctx.tpa.as_slice_u8(); 129 | let buffer_ptr = buffer.as_mut_ptr() as *mut u32; 130 | if let Some(file_name) = file_name { 131 | let Ok(file) = crate::FILESYSTEM.open_file(file_name, embedded_sdmmc::Mode::ReadOnly) 132 | else { 133 | osprintln!("No such file."); 134 | return; 135 | }; 136 | let _ = file.read(buffer); 137 | } else { 138 | let (odd_pattern, even_pattern) = match mode.format() { 139 | // This is alternating hearts and diamonds 140 | Format::Text8x16 | Format::Text8x8 => ( 141 | u32::from_le_bytes(*b"\x03\x0F\x04\x70"), 142 | u32::from_le_bytes(*b"\x04\x70\x03\x0F"), 143 | ), 144 | // Can't do a checkerboard here - so stripes will do 145 | Format::Chunky32 => (0x0000_0000, 0x0000_0001), 146 | // These should produce black/white checkerboard, in the default 147 | // palette 148 | Format::Chunky16 => (0x0000_FFFF, 0xFFFF_0000), 149 | Format::Chunky8 => (0x000F_000F, 0x0F00_0F00), 150 | Format::Chunky4 => (0x0F0F_0F0F, 0xF0F0_F0F0), 151 | Format::Chunky2 => (0x3333_3333, 0xCCCC_CCCC), 152 | Format::Chunky1 => (0x5555_5555, 0xAAAA_AAAA), 153 | _ => todo!(), 154 | }; 155 | // draw a dummy non-zero data. In Chunky1 this is a checkerboard. 156 | let line_size_words = mode.line_size_bytes() / 4; 157 | for row in 0..mode.vertical_lines() as usize { 158 | let word = if (row % 2) == 0 { 159 | even_pattern 160 | } else { 161 | odd_pattern 162 | }; 163 | for col in 0..line_size_words { 164 | let idx = (row * line_size_words) + col; 165 | unsafe { 166 | buffer_ptr.add(idx).write_volatile(word); 167 | } 168 | } 169 | } 170 | } 171 | 172 | if let neotron_common_bios::FfiResult::Err(e) = 173 | unsafe { (api.video_set_mode)(mode, buffer_ptr) } 174 | { 175 | osprintln!("Couldn't set mode {}: {:?}", mode_num, e); 176 | } 177 | 178 | // Now wait for user input 179 | while crate::STD_INPUT.lock().get_raw().is_none() { 180 | // spin 181 | } 182 | 183 | // Put it back as it was 184 | unsafe { 185 | (api.video_set_mode)(old_mode, old_ptr); 186 | } 187 | } 188 | 189 | /// Print out all supported video modes 190 | fn print_modes() { 191 | let api = crate::API.get(); 192 | let current_mode = (api.video_get_mode)(); 193 | let mut any_mode = false; 194 | for mode_no in 0..255 { 195 | // Note (unsafe): we'll test if it's right before we try and use it 196 | let Some(m) = Mode::try_from_u8(mode_no) else { 197 | continue; 198 | }; 199 | let is_supported = (api.video_is_valid_mode)(m); 200 | if is_supported { 201 | any_mode = true; 202 | let is_current = if current_mode == m { "*" } else { " " }; 203 | let text_rows = m.text_height(); 204 | let text_cols = m.text_width(); 205 | let f = m.format(); 206 | let width = m.horizontal_pixels(); 207 | let height = m.vertical_lines(); 208 | let hz = m.frame_rate_hz(); 209 | if let (Some(text_rows), Some(text_cols)) = (text_rows, text_cols) { 210 | // It's a text mode 211 | osprintln!("{mode_no:3}{is_current}: {width} x {height} @ {hz} Hz {f} ({text_cols} x {text_rows})"); 212 | } else { 213 | // It's a framebuffer mode 214 | let f = m.format(); 215 | osprintln!("{mode_no:3}{is_current}: {width} x {height} @ {hz} Hz {f}"); 216 | } 217 | } 218 | } 219 | if !any_mode { 220 | osprintln!("No valid modes found"); 221 | } 222 | } 223 | 224 | // End of file 225 | -------------------------------------------------------------------------------- /neotron-os/src/fs.rs: -------------------------------------------------------------------------------- 1 | //! Filesystem related types 2 | 3 | use chrono::{Datelike, Timelike}; 4 | use embedded_sdmmc::RawVolume; 5 | 6 | use crate::{bios, refcell::CsRefCell, API, FILESYSTEM}; 7 | 8 | /// Represents a block device that reads/writes disk blocks using the BIOS. 9 | /// 10 | /// Currently only block device 0 is supported. 11 | pub struct BiosBlock(); 12 | 13 | impl embedded_sdmmc::BlockDevice for BiosBlock { 14 | type Error = bios::Error; 15 | 16 | fn read( 17 | &self, 18 | blocks: &mut [embedded_sdmmc::Block], 19 | start_block_idx: embedded_sdmmc::BlockIdx, 20 | _reason: &str, 21 | ) -> Result<(), Self::Error> { 22 | let api = API.get(); 23 | let byte_slice = unsafe { 24 | core::slice::from_raw_parts_mut( 25 | blocks.as_mut_ptr() as *mut u8, 26 | blocks.len() * embedded_sdmmc::Block::LEN, 27 | ) 28 | }; 29 | match (api.block_read)( 30 | 0, 31 | bios::block_dev::BlockIdx(u64::from(start_block_idx.0)), 32 | blocks.len() as u8, 33 | bios::FfiBuffer::new(byte_slice), 34 | ) { 35 | bios::ApiResult::Ok(_) => Ok(()), 36 | bios::ApiResult::Err(e) => Err(e), 37 | } 38 | } 39 | 40 | fn write( 41 | &self, 42 | blocks: &[embedded_sdmmc::Block], 43 | start_block_idx: embedded_sdmmc::BlockIdx, 44 | ) -> Result<(), Self::Error> { 45 | let api = API.get(); 46 | let byte_slice = unsafe { 47 | core::slice::from_raw_parts( 48 | blocks.as_ptr() as *const u8, 49 | blocks.len() * embedded_sdmmc::Block::LEN, 50 | ) 51 | }; 52 | match (api.block_write)( 53 | 0, 54 | bios::block_dev::BlockIdx(u64::from(start_block_idx.0)), 55 | blocks.len() as u8, 56 | bios::FfiByteSlice::new(byte_slice), 57 | ) { 58 | bios::ApiResult::Ok(_) => Ok(()), 59 | bios::ApiResult::Err(e) => Err(e), 60 | } 61 | } 62 | 63 | fn num_blocks(&self) -> Result { 64 | let api = API.get(); 65 | match (api.block_dev_get_info)(0) { 66 | bios::FfiOption::Some(info) => Ok(embedded_sdmmc::BlockCount(info.num_blocks as u32)), 67 | bios::FfiOption::None => Err(bios::Error::InvalidDevice), 68 | } 69 | } 70 | } 71 | 72 | /// A type that lets you fetch the current time from the BIOS. 73 | pub struct BiosTime(); 74 | 75 | impl embedded_sdmmc::TimeSource for BiosTime { 76 | fn get_timestamp(&self) -> embedded_sdmmc::Timestamp { 77 | let time = API.get_time(); 78 | embedded_sdmmc::Timestamp { 79 | year_since_1970: (time.year() - 1970) as u8, 80 | zero_indexed_month: time.month0() as u8, 81 | zero_indexed_day: time.day0() as u8, 82 | hours: time.hour() as u8, 83 | minutes: time.minute() as u8, 84 | seconds: time.second() as u8, 85 | } 86 | } 87 | } 88 | 89 | /// The errors this module can produce 90 | #[derive(Debug)] 91 | pub enum Error { 92 | /// Filesystem error 93 | Io(embedded_sdmmc::Error), 94 | } 95 | 96 | impl From> for Error { 97 | fn from(value: embedded_sdmmc::Error) -> Self { 98 | Error::Io(value) 99 | } 100 | } 101 | 102 | /// Represents an open file 103 | pub struct File { 104 | inner: embedded_sdmmc::RawFile, 105 | } 106 | 107 | impl File { 108 | /// Read from a file 109 | pub fn read(&self, buffer: &mut [u8]) -> Result { 110 | FILESYSTEM.file_read(self, buffer) 111 | } 112 | 113 | /// Write to a file 114 | pub fn write(&self, buffer: &[u8]) -> Result<(), Error> { 115 | FILESYSTEM.file_write(self, buffer) 116 | } 117 | 118 | /// Are we at the end of the file 119 | pub fn is_eof(&self) -> bool { 120 | FILESYSTEM 121 | .file_eof(self) 122 | .expect("File handle should be valid") 123 | } 124 | 125 | /// Seek to a position relative to the start of the file 126 | pub fn seek_from_start(&self, offset: u32) -> Result<(), Error> { 127 | FILESYSTEM.file_seek_from_start(self, offset) 128 | } 129 | 130 | /// What is the length of this file? 131 | pub fn length(&self) -> u32 { 132 | FILESYSTEM 133 | .file_length(self) 134 | .expect("File handle should be valid") 135 | } 136 | } 137 | 138 | impl Drop for File { 139 | fn drop(&mut self) { 140 | FILESYSTEM 141 | .close_raw_file(self.inner) 142 | .expect("Should only be dropping valid files!"); 143 | } 144 | } 145 | 146 | /// Represent all open files and filesystems 147 | pub struct Filesystem { 148 | volume_manager: CsRefCell>>, 149 | first_volume: CsRefCell>, 150 | } 151 | 152 | impl Filesystem { 153 | /// Create a new filesystem 154 | pub const fn new() -> Filesystem { 155 | Filesystem { 156 | volume_manager: CsRefCell::new(None), 157 | first_volume: CsRefCell::new(None), 158 | } 159 | } 160 | 161 | /// Open a file on the filesystem 162 | pub fn open_file(&self, name: &str, mode: embedded_sdmmc::Mode) -> Result { 163 | let mut fs = self.volume_manager.lock(); 164 | if fs.is_none() { 165 | *fs = Some(embedded_sdmmc::VolumeManager::new(BiosBlock(), BiosTime())); 166 | } 167 | let fs = fs.as_mut().unwrap(); 168 | let mut volume = self.first_volume.lock(); 169 | if volume.is_none() { 170 | *volume = Some(fs.open_raw_volume(embedded_sdmmc::VolumeIdx(0))?); 171 | } 172 | let volume = volume.unwrap(); 173 | let mut root = fs.open_root_dir(volume)?.to_directory(fs); 174 | let file = root.open_file_in_dir(name, mode)?; 175 | let raw_file = file.to_raw_file(); 176 | Ok(File { inner: raw_file }) 177 | } 178 | 179 | /// Walk through the root directory 180 | pub fn iterate_root_dir(&self, f: F) -> Result<(), Error> 181 | where 182 | F: FnMut(&embedded_sdmmc::DirEntry), 183 | { 184 | let mut fs = self.volume_manager.lock(); 185 | if fs.is_none() { 186 | *fs = Some(embedded_sdmmc::VolumeManager::new(BiosBlock(), BiosTime())); 187 | } 188 | let fs = fs.as_mut().unwrap(); 189 | let mut volume = self.first_volume.lock(); 190 | if volume.is_none() { 191 | *volume = Some(fs.open_raw_volume(embedded_sdmmc::VolumeIdx(0))?); 192 | } 193 | let volume = volume.unwrap(); 194 | let mut root = fs.open_root_dir(volume)?.to_directory(fs); 195 | root.iterate_dir(f)?; 196 | Ok(()) 197 | } 198 | 199 | /// Read from an open file 200 | pub fn file_read(&self, file: &File, buffer: &mut [u8]) -> Result { 201 | let mut fs = self.volume_manager.lock(); 202 | if fs.is_none() { 203 | *fs = Some(embedded_sdmmc::VolumeManager::new(BiosBlock(), BiosTime())); 204 | } 205 | let fs = fs.as_mut().unwrap(); 206 | let bytes_read = fs.read(file.inner, buffer)?; 207 | Ok(bytes_read) 208 | } 209 | 210 | /// Write to an open file 211 | pub fn file_write(&self, file: &File, buffer: &[u8]) -> Result<(), Error> { 212 | let mut fs = self.volume_manager.lock(); 213 | if fs.is_none() { 214 | *fs = Some(embedded_sdmmc::VolumeManager::new(BiosBlock(), BiosTime())); 215 | } 216 | let fs = fs.as_mut().unwrap(); 217 | fs.write(file.inner, buffer)?; 218 | Ok(()) 219 | } 220 | 221 | /// How large is a file? 222 | pub fn file_length(&self, file: &File) -> Result { 223 | let mut fs = self.volume_manager.lock(); 224 | if fs.is_none() { 225 | *fs = Some(embedded_sdmmc::VolumeManager::new(BiosBlock(), BiosTime())); 226 | } 227 | let fs = fs.as_mut().unwrap(); 228 | let length = fs.file_length(file.inner)?; 229 | Ok(length) 230 | } 231 | 232 | /// Seek a file with an offset from the start of the file. 233 | pub fn file_seek_from_start(&self, file: &File, offset: u32) -> Result<(), Error> { 234 | let mut fs = self.volume_manager.lock(); 235 | if fs.is_none() { 236 | *fs = Some(embedded_sdmmc::VolumeManager::new(BiosBlock(), BiosTime())); 237 | } 238 | let fs = fs.as_mut().unwrap(); 239 | fs.file_seek_from_start(file.inner, offset)?; 240 | Ok(()) 241 | } 242 | 243 | /// Are we at the end of the file 244 | pub fn file_eof(&self, file: &File) -> Result { 245 | let mut fs = self.volume_manager.lock(); 246 | if fs.is_none() { 247 | *fs = Some(embedded_sdmmc::VolumeManager::new(BiosBlock(), BiosTime())); 248 | } 249 | let fs = fs.as_mut().unwrap(); 250 | let is_eof = fs.file_eof(file.inner)?; 251 | Ok(is_eof) 252 | } 253 | 254 | /// Close an open file 255 | /// 256 | /// Only used by File's drop impl. 257 | fn close_raw_file(&self, file: embedded_sdmmc::RawFile) -> Result<(), Error> { 258 | let mut fs = self.volume_manager.lock(); 259 | if fs.is_none() { 260 | *fs = Some(embedded_sdmmc::VolumeManager::new(BiosBlock(), BiosTime())); 261 | } 262 | let fs = fs.as_mut().unwrap(); 263 | fs.close_file(file)?; 264 | Ok(()) 265 | } 266 | } 267 | 268 | // End of file 269 | -------------------------------------------------------------------------------- /nbuild/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.6" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys", 52 | ] 53 | 54 | [[package]] 55 | name = "autocfg" 56 | version = "1.4.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 59 | 60 | [[package]] 61 | name = "bitflags" 62 | version = "2.6.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 65 | 66 | [[package]] 67 | name = "chrono" 68 | version = "0.4.39" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 71 | dependencies = [ 72 | "num-traits", 73 | ] 74 | 75 | [[package]] 76 | name = "clap" 77 | version = "4.5.23" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 80 | dependencies = [ 81 | "clap_builder", 82 | "clap_derive", 83 | ] 84 | 85 | [[package]] 86 | name = "clap_builder" 87 | version = "4.5.23" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 90 | dependencies = [ 91 | "anstream", 92 | "anstyle", 93 | "clap_lex", 94 | "strsim", 95 | ] 96 | 97 | [[package]] 98 | name = "clap_derive" 99 | version = "4.5.18" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 102 | dependencies = [ 103 | "heck", 104 | "proc-macro2", 105 | "quote", 106 | "syn", 107 | ] 108 | 109 | [[package]] 110 | name = "clap_lex" 111 | version = "0.7.4" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 114 | 115 | [[package]] 116 | name = "colorchoice" 117 | version = "1.0.3" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 120 | 121 | [[package]] 122 | name = "embedded-io" 123 | version = "0.6.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" 126 | 127 | [[package]] 128 | name = "heck" 129 | version = "0.5.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 132 | 133 | [[package]] 134 | name = "is_terminal_polyfill" 135 | version = "1.70.1" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 138 | 139 | [[package]] 140 | name = "nbuild" 141 | version = "0.1.0" 142 | dependencies = [ 143 | "chrono", 144 | "clap", 145 | "embedded-io", 146 | "neotron-api", 147 | "neotron-romfs", 148 | ] 149 | 150 | [[package]] 151 | name = "neotron-api" 152 | version = "0.2.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "67d6c96706b6f3ec069abfb042cadfd2d701980fa4940f407c0bc28ee1e1c493" 155 | dependencies = [ 156 | "bitflags", 157 | "neotron-ffi", 158 | ] 159 | 160 | [[package]] 161 | name = "neotron-ffi" 162 | version = "0.1.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "d37886e73d87732421aaf5da617eead9d69a7daf6b0d059780f76157d9ce5372" 165 | 166 | [[package]] 167 | name = "neotron-romfs" 168 | version = "2.0.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "8f7987e34f25f780b5dd5a22b5da7ce9a566d93b8f608f78293a170f35f024c7" 171 | dependencies = [ 172 | "embedded-io", 173 | "neotron-api", 174 | ] 175 | 176 | [[package]] 177 | name = "num-traits" 178 | version = "0.2.19" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 181 | dependencies = [ 182 | "autocfg", 183 | ] 184 | 185 | [[package]] 186 | name = "proc-macro2" 187 | version = "1.0.92" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 190 | dependencies = [ 191 | "unicode-ident", 192 | ] 193 | 194 | [[package]] 195 | name = "quote" 196 | version = "1.0.37" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 199 | dependencies = [ 200 | "proc-macro2", 201 | ] 202 | 203 | [[package]] 204 | name = "strsim" 205 | version = "0.11.1" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 208 | 209 | [[package]] 210 | name = "syn" 211 | version = "2.0.90" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 214 | dependencies = [ 215 | "proc-macro2", 216 | "quote", 217 | "unicode-ident", 218 | ] 219 | 220 | [[package]] 221 | name = "unicode-ident" 222 | version = "1.0.14" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 225 | 226 | [[package]] 227 | name = "utf8parse" 228 | version = "0.2.2" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 231 | 232 | [[package]] 233 | name = "windows-sys" 234 | version = "0.59.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 237 | dependencies = [ 238 | "windows-targets", 239 | ] 240 | 241 | [[package]] 242 | name = "windows-targets" 243 | version = "0.52.6" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 246 | dependencies = [ 247 | "windows_aarch64_gnullvm", 248 | "windows_aarch64_msvc", 249 | "windows_i686_gnu", 250 | "windows_i686_gnullvm", 251 | "windows_i686_msvc", 252 | "windows_x86_64_gnu", 253 | "windows_x86_64_gnullvm", 254 | "windows_x86_64_msvc", 255 | ] 256 | 257 | [[package]] 258 | name = "windows_aarch64_gnullvm" 259 | version = "0.52.6" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 262 | 263 | [[package]] 264 | name = "windows_aarch64_msvc" 265 | version = "0.52.6" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 268 | 269 | [[package]] 270 | name = "windows_i686_gnu" 271 | version = "0.52.6" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 274 | 275 | [[package]] 276 | name = "windows_i686_gnullvm" 277 | version = "0.52.6" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 280 | 281 | [[package]] 282 | name = "windows_i686_msvc" 283 | version = "0.52.6" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 286 | 287 | [[package]] 288 | name = "windows_x86_64_gnu" 289 | version = "0.52.6" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 292 | 293 | [[package]] 294 | name = "windows_x86_64_gnullvm" 295 | version = "0.52.6" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 298 | 299 | [[package]] 300 | name = "windows_x86_64_msvc" 301 | version = "0.52.6" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 304 | -------------------------------------------------------------------------------- /neotron-os/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "android-tzdata" 7 | version = "0.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 10 | 11 | [[package]] 12 | name = "arrayvec" 13 | version = "0.7.4" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 16 | 17 | [[package]] 18 | name = "atomic-polyfill" 19 | version = "0.1.11" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" 22 | dependencies = [ 23 | "critical-section", 24 | ] 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.1.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "2.3.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" 37 | 38 | [[package]] 39 | name = "byteorder" 40 | version = "1.4.3" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 43 | 44 | [[package]] 45 | name = "chrono" 46 | version = "0.4.26" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" 49 | dependencies = [ 50 | "android-tzdata", 51 | "num-traits", 52 | ] 53 | 54 | [[package]] 55 | name = "cobs" 56 | version = "0.2.3" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" 59 | 60 | [[package]] 61 | name = "critical-section" 62 | version = "1.1.1" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "6548a0ad5d2549e111e1f6a11a6c2e2d00ce6a3dafe22948d67c2b443f775e52" 65 | 66 | [[package]] 67 | name = "embedded-hal" 68 | version = "1.0.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" 71 | 72 | [[package]] 73 | name = "embedded-sdmmc" 74 | version = "0.7.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "da528dbf3f1c1f0b321552bc334d04799bb17c1936de55bccfb643a4f39300d8" 77 | dependencies = [ 78 | "byteorder", 79 | "embedded-hal", 80 | "heapless", 81 | ] 82 | 83 | [[package]] 84 | name = "hash32" 85 | version = "0.2.1" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 88 | dependencies = [ 89 | "byteorder", 90 | ] 91 | 92 | [[package]] 93 | name = "heapless" 94 | version = "0.7.16" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" 97 | dependencies = [ 98 | "atomic-polyfill", 99 | "hash32", 100 | "rustc_version", 101 | "serde", 102 | "spin", 103 | "stable_deref_trait", 104 | ] 105 | 106 | [[package]] 107 | name = "lock_api" 108 | version = "0.4.10" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 111 | dependencies = [ 112 | "autocfg", 113 | "scopeguard", 114 | ] 115 | 116 | [[package]] 117 | name = "menu" 118 | version = "0.3.2" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "b03d7f798bfe97329ad6df937951142eec93886b37d87010502dd25e8cc75fd5" 121 | 122 | [[package]] 123 | name = "neotron-api" 124 | version = "0.2.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "67d6c96706b6f3ec069abfb042cadfd2d701980fa4940f407c0bc28ee1e1c493" 127 | dependencies = [ 128 | "bitflags", 129 | "neotron-ffi", 130 | ] 131 | 132 | [[package]] 133 | name = "neotron-common-bios" 134 | version = "0.12.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "24fdb319a4bdecd68d9917e5cc026a2b50d32572649880febb1f993a0bf9ad00" 137 | dependencies = [ 138 | "chrono", 139 | "neotron-ffi", 140 | "pc-keyboard", 141 | ] 142 | 143 | [[package]] 144 | name = "neotron-ffi" 145 | version = "0.1.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "d37886e73d87732421aaf5da617eead9d69a7daf6b0d059780f76157d9ce5372" 148 | 149 | [[package]] 150 | name = "neotron-loader" 151 | version = "0.1.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "b9b8634a088b9d5b338a96b3f6ef45a3bc0b9c0f0d562c7d00e498265fd96e8f" 154 | 155 | [[package]] 156 | name = "neotron-os" 157 | version = "0.8.1" 158 | dependencies = [ 159 | "chrono", 160 | "embedded-sdmmc", 161 | "heapless", 162 | "menu", 163 | "neotron-api", 164 | "neotron-common-bios", 165 | "neotron-loader", 166 | "pc-keyboard", 167 | "postcard", 168 | "r0", 169 | "serde", 170 | "vte", 171 | ] 172 | 173 | [[package]] 174 | name = "num-traits" 175 | version = "0.2.16" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" 178 | dependencies = [ 179 | "autocfg", 180 | ] 181 | 182 | [[package]] 183 | name = "pc-keyboard" 184 | version = "0.7.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "ed089a1fbffe3337a1a345501c981f1eb1e47e69de5a40e852433e12953c3174" 187 | 188 | [[package]] 189 | name = "postcard" 190 | version = "1.0.6" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb" 193 | dependencies = [ 194 | "cobs", 195 | "heapless", 196 | "serde", 197 | ] 198 | 199 | [[package]] 200 | name = "proc-macro2" 201 | version = "1.0.66" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 204 | dependencies = [ 205 | "unicode-ident", 206 | ] 207 | 208 | [[package]] 209 | name = "quote" 210 | version = "1.0.31" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" 213 | dependencies = [ 214 | "proc-macro2", 215 | ] 216 | 217 | [[package]] 218 | name = "r0" 219 | version = "1.0.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "bd7a31eed1591dcbc95d92ad7161908e72f4677f8fabf2a32ca49b4237cbf211" 222 | 223 | [[package]] 224 | name = "rustc_version" 225 | version = "0.4.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 228 | dependencies = [ 229 | "semver", 230 | ] 231 | 232 | [[package]] 233 | name = "scopeguard" 234 | version = "1.2.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 237 | 238 | [[package]] 239 | name = "semver" 240 | version = "1.0.18" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" 243 | 244 | [[package]] 245 | name = "serde" 246 | version = "1.0.174" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "3b88756493a5bd5e5395d53baa70b194b05764ab85b59e43e4b8f4e1192fa9b1" 249 | dependencies = [ 250 | "serde_derive", 251 | ] 252 | 253 | [[package]] 254 | name = "serde_derive" 255 | version = "1.0.174" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "6e5c3a298c7f978e53536f95a63bdc4c4a64550582f31a0359a9afda6aede62e" 258 | dependencies = [ 259 | "proc-macro2", 260 | "quote", 261 | "syn", 262 | ] 263 | 264 | [[package]] 265 | name = "spin" 266 | version = "0.9.8" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 269 | dependencies = [ 270 | "lock_api", 271 | ] 272 | 273 | [[package]] 274 | name = "stable_deref_trait" 275 | version = "1.2.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 278 | 279 | [[package]] 280 | name = "syn" 281 | version = "2.0.27" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" 284 | dependencies = [ 285 | "proc-macro2", 286 | "quote", 287 | "unicode-ident", 288 | ] 289 | 290 | [[package]] 291 | name = "unicode-ident" 292 | version = "1.0.11" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 295 | 296 | [[package]] 297 | name = "utf8parse" 298 | version = "0.2.1" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 301 | 302 | [[package]] 303 | name = "vte" 304 | version = "0.12.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "401dc1020e10f74d38616c1f1ab92ccd85dc902705a29d0730e0fbea8534f91a" 307 | dependencies = [ 308 | "arrayvec", 309 | "utf8parse", 310 | "vte_generate_state_changes", 311 | ] 312 | 313 | [[package]] 314 | name = "vte_generate_state_changes" 315 | version = "0.1.1" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" 318 | dependencies = [ 319 | "proc-macro2", 320 | "quote", 321 | ] 322 | -------------------------------------------------------------------------------- /nbuild/src/main.rs: -------------------------------------------------------------------------------- 1 | //! A series of utilities for building Neotron OS 2 | 3 | use clap::{Parser, Subcommand}; 4 | 5 | #[derive(Debug, Subcommand)] 6 | enum Commands { 7 | /// Builds the OS and the ROMFS 8 | Binary { 9 | /// The start address in Flash where Neotron OS should live 10 | #[clap(long, default_value = "0x1002_0000")] 11 | start_address: String, 12 | /// The target we're building Neotron OS for 13 | #[clap(long, default_value = "thumbv6m-none-eabi")] 14 | target: String, 15 | }, 16 | /// Builds the OS as a library, for the native machine 17 | Library { 18 | /// The target we're building Neotron OS for 19 | #[clap(long)] 20 | target: Option, 21 | }, 22 | /// Handles formatting of the Neotron OS source code 23 | Format { 24 | /// Whether to just check the formatting 25 | #[clap(long)] 26 | check: bool, 27 | }, 28 | /// Checks the Neotron OS source code using clippy 29 | Clippy, 30 | /// Runs any tests 31 | Test, 32 | } 33 | 34 | /// A simple utility for building Neotron OS and a suitable ROMFS image 35 | #[derive(Debug, Parser)] 36 | #[clap(name = "nbuild", version = "0.1.0", author = "The Neotron Developers")] 37 | pub struct NBuildApp { 38 | /// The task to perform 39 | #[command(subcommand)] 40 | command: Option, 41 | } 42 | 43 | fn packages() -> Vec { 44 | vec![ 45 | // *** build system *** 46 | nbuild::Package { 47 | name: "nbuild", 48 | path: std::path::Path::new("./nbuild/Cargo.toml"), 49 | output_template: None, 50 | kind: nbuild::PackageKind::NBuild, 51 | testable: nbuild::Testable::All, 52 | }, 53 | // *** utilities *** 54 | nbuild::Package { 55 | name: "flames", 56 | path: std::path::Path::new("./utilities/flames/Cargo.toml"), 57 | output_template: Some("./target/{target}/{profile}/flames"), 58 | kind: nbuild::PackageKind::Utility, 59 | testable: nbuild::Testable::No, 60 | }, 61 | nbuild::Package { 62 | name: "neoplay", 63 | path: std::path::Path::new("./utilities/neoplay/Cargo.toml"), 64 | output_template: Some("./target/{target}/{profile}/neoplay"), 65 | kind: nbuild::PackageKind::Utility, 66 | testable: nbuild::Testable::No, 67 | }, 68 | nbuild::Package { 69 | name: "snake", 70 | path: std::path::Path::new("./utilities/snake/Cargo.toml"), 71 | output_template: Some("./target/{target}/{profile}/snake"), 72 | kind: nbuild::PackageKind::Utility, 73 | testable: nbuild::Testable::No, 74 | }, 75 | // *** OS *** 76 | nbuild::Package { 77 | name: "Neotron OS", 78 | path: std::path::Path::new("./neotron-os/Cargo.toml"), 79 | output_template: Some("./target/{target}/{profile}/neotron-os"), 80 | kind: nbuild::PackageKind::Os, 81 | testable: nbuild::Testable::Libs, 82 | }, 83 | ] 84 | } 85 | 86 | fn main() { 87 | println!("Neotron OS nbuild tool"); 88 | let args = NBuildApp::parse(); 89 | let packages = packages(); 90 | match args.command { 91 | None => { 92 | // No command given 93 | println!("No command given. Try `cargo nbuild help`."); 94 | std::process::exit(1); 95 | } 96 | Some(Commands::Binary { 97 | start_address, 98 | target, 99 | }) => { 100 | binary(&packages, &start_address, &target); 101 | } 102 | Some(Commands::Library { target }) => library(&packages, target.as_deref()), 103 | Some(Commands::Format { check }) => format(&packages, check), 104 | Some(Commands::Clippy) => clippy(&packages), 105 | Some(Commands::Test) => test(&packages), 106 | } 107 | } 108 | 109 | /// Builds the utility and OS packages as binaries 110 | fn binary(packages: &[nbuild::Package], start_address: &str, target: &str) { 111 | use chrono::{Datelike, Timelike}; 112 | 113 | let mut is_error = false; 114 | let Ok(start_address) = nbuild::parse_int(start_address) else { 115 | eprintln!("{:?} was not a valid integer", start_address); 116 | std::process::exit(1); 117 | }; 118 | 119 | let mut romfs_entries = Vec::new(); 120 | // Build utilities 121 | for package in packages 122 | .iter() 123 | .filter(|p| p.kind == nbuild::PackageKind::Utility) 124 | { 125 | println!( 126 | "Cross-compiling {}, using target {:?}", 127 | package.name, target 128 | ); 129 | if let Err(e) = nbuild::cargo(&["build", "--release"], Some(target), package.path) { 130 | eprintln!("Build of {} failed: {}", package.name, e); 131 | is_error = true; 132 | } 133 | let package_output = package 134 | .output(target, "release") 135 | .expect("utilties should have an output"); 136 | let contents = match std::fs::read(&package_output) { 137 | Ok(contents) => contents, 138 | Err(e) => { 139 | eprintln!("Reading of {} failed: {}", package_output, e); 140 | continue; 141 | } 142 | }; 143 | let ctime = std::time::SystemTime::now(); 144 | let ctime = chrono::DateTime::::from(ctime); 145 | romfs_entries.push(neotron_romfs::Entry { 146 | metadata: neotron_romfs::EntryMetadata { 147 | file_name: package.name, 148 | ctime: neotron_api::file::Time { 149 | year_since_1970: (ctime.year() - 1970) as u8, 150 | zero_indexed_month: ctime.month0() as u8, 151 | zero_indexed_day: ctime.day0() as u8, 152 | hours: ctime.hour() as u8, 153 | minutes: ctime.minute() as u8, 154 | seconds: ctime.second() as u8, 155 | }, 156 | file_size: contents.len() as u32, 157 | }, 158 | contents, 159 | }); 160 | } 161 | 162 | // Build ROMFS 163 | let mut buffer = Vec::new(); 164 | let _size = match neotron_romfs::RomFs::construct_into(&mut buffer, &romfs_entries) { 165 | Ok(size) => size, 166 | Err(e) => { 167 | eprintln!("Making ROMFS failed: {:?}", e); 168 | std::process::exit(1); 169 | } 170 | }; 171 | let mut romfs_path = std::path::PathBuf::new(); 172 | romfs_path.push(std::env::current_dir().expect("We have no CWD?")); 173 | romfs_path.push("target"); 174 | romfs_path.push(target); 175 | romfs_path.push("release"); 176 | romfs_path.push("romfs.bin"); 177 | if let Err(e) = std::fs::write(&romfs_path, &buffer) { 178 | eprintln!("Writing ROMFS to {} failed: {:?}", romfs_path.display(), e); 179 | std::process::exit(1); 180 | } 181 | println!("Built ROMFS at {}", romfs_path.display()); 182 | 183 | // Build OS 184 | for package in packages 185 | .iter() 186 | .filter(|p| p.kind == nbuild::PackageKind::Os) 187 | { 188 | println!( 189 | "Cross-compiling {}, using start address 0x{:08x} and target {:?}", 190 | package.name, start_address, target 191 | ); 192 | let environment = [ 193 | ( 194 | "NEOTRON_OS_START_ADDRESS", 195 | format!("0x{:08x}", start_address), 196 | ), 197 | ("ROMFS_PATH", romfs_path.to_string_lossy().to_string()), 198 | ]; 199 | if let Err(e) = nbuild::cargo_with_env( 200 | &["build", "--release"], 201 | Some(target), 202 | package.path, 203 | &environment, 204 | ) { 205 | eprintln!("Build of {} failed: {}", package.name, e); 206 | is_error = true; 207 | } 208 | let package_output = package 209 | .output(target, "release") 210 | .expect("PackageKind::Os should always have output"); 211 | if let Err(e) = nbuild::make_bin(&package_output) { 212 | eprintln!("objcopy of {} failed: {}", package_output, e); 213 | is_error = true; 214 | } 215 | } 216 | if is_error { 217 | std::process::exit(1); 218 | } 219 | } 220 | 221 | /// Builds the OS packages as a library 222 | fn library(packages: &[nbuild::Package], target: Option<&str>) { 223 | let mut is_error = false; 224 | println!( 225 | "Compiling Neotron OS library, using target {:?}", 226 | target.unwrap_or("native") 227 | ); 228 | for package in packages 229 | .iter() 230 | .filter(|p| p.kind == nbuild::PackageKind::Os) 231 | { 232 | println!( 233 | "Compiling {}, target {:?}", 234 | package.name, 235 | target.unwrap_or("native") 236 | ); 237 | if let Err(e) = nbuild::cargo(&["build", "--release", "--lib"], target, package.path) { 238 | eprintln!("Build of {} failed: {}", package.name, e); 239 | is_error = true; 240 | } 241 | } 242 | if is_error { 243 | std::process::exit(1); 244 | } 245 | } 246 | 247 | /// Runs `cargo fmt` over all the packages 248 | fn format(packages: &[nbuild::Package], check: bool) { 249 | let mut is_error = false; 250 | let commands = if check { 251 | vec!["fmt", "--check"] 252 | } else { 253 | vec!["fmt"] 254 | }; 255 | for package in packages.iter() { 256 | println!("Formatting {}", package.name); 257 | if let Err(e) = nbuild::cargo(&commands, None, package.path) { 258 | eprintln!("Format failed: {}", e); 259 | is_error = true; 260 | } 261 | } 262 | if is_error { 263 | std::process::exit(1); 264 | } 265 | } 266 | 267 | /// Runs `cargo clippy` over all the packages 268 | fn clippy(packages: &[nbuild::Package]) { 269 | let mut is_error = false; 270 | for package in packages.iter() { 271 | println!("Linting {} with clippy", package.name); 272 | if let Err(e) = nbuild::cargo(&["clippy"], None, package.path) { 273 | eprintln!("Lint failed: {}", e); 274 | is_error = true; 275 | } 276 | } 277 | if is_error { 278 | std::process::exit(1); 279 | } 280 | } 281 | 282 | /// Runs `cargo test` over all the packages 283 | fn test(packages: &[nbuild::Package]) { 284 | let mut is_error = false; 285 | for package in packages 286 | .iter() 287 | .filter(|p| p.testable == nbuild::Testable::Libs) 288 | { 289 | println!("Testing {}", package.name); 290 | if let Err(e) = nbuild::cargo(&["test", "--lib"], None, package.path) { 291 | eprintln!("Test failed: {}", e); 292 | is_error = true; 293 | } 294 | } 295 | for package in packages 296 | .iter() 297 | .filter(|p| p.testable == nbuild::Testable::All) 298 | { 299 | println!("Testing {}", package.name); 300 | if let Err(e) = nbuild::cargo(&["test"], None, package.path) { 301 | eprintln!("Test failed: {}", e); 302 | is_error = true; 303 | } 304 | } 305 | if is_error { 306 | std::process::exit(1); 307 | } 308 | } 309 | 310 | // End of file 311 | -------------------------------------------------------------------------------- /neotron-os/src/commands/hardware.rs: -------------------------------------------------------------------------------- 1 | //! Hardware related commands for Neotron OS 2 | 3 | use crate::{bios, osprintln, Ctx, API}; 4 | 5 | use super::{parse_u8, parse_usize}; 6 | 7 | pub static LSBLK_ITEM: menu::Item = menu::Item { 8 | item_type: menu::ItemType::Callback { 9 | function: lsblk, 10 | parameters: &[], 11 | }, 12 | command: "lsblk", 13 | help: Some("List all the Block Devices"), 14 | }; 15 | 16 | pub static LSBUS_ITEM: menu::Item = menu::Item { 17 | item_type: menu::ItemType::Callback { 18 | function: lsbus, 19 | parameters: &[], 20 | }, 21 | command: "lsbus", 22 | help: Some("List all the Neotron Bus devices"), 23 | }; 24 | 25 | pub static LSI2C_ITEM: menu::Item = menu::Item { 26 | item_type: menu::ItemType::Callback { 27 | function: lsi2c, 28 | parameters: &[], 29 | }, 30 | command: "lsi2c", 31 | help: Some("List all the BIOS I2C devices"), 32 | }; 33 | 34 | pub static LSMEM_ITEM: menu::Item = menu::Item { 35 | item_type: menu::ItemType::Callback { 36 | function: lsmem, 37 | parameters: &[], 38 | }, 39 | command: "lsmem", 40 | help: Some("List all the BIOS Memory regions"), 41 | }; 42 | 43 | pub static LSUART_ITEM: menu::Item = menu::Item { 44 | item_type: menu::ItemType::Callback { 45 | function: lsuart, 46 | parameters: &[], 47 | }, 48 | command: "lsuart", 49 | help: Some("List all the BIOS UARTs"), 50 | }; 51 | 52 | pub static SHUTDOWN_ITEM: menu::Item = menu::Item { 53 | item_type: menu::ItemType::Callback { 54 | function: shutdown, 55 | parameters: &[ 56 | menu::Parameter::Named { 57 | parameter_name: "reboot", 58 | help: Some("Reboot after shutting down"), 59 | }, 60 | menu::Parameter::Named { 61 | parameter_name: "bootloader", 62 | help: Some("Reboot into the bootloader after shutting down"), 63 | }, 64 | ], 65 | }, 66 | command: "shutdown", 67 | help: Some("Shutdown the system"), 68 | }; 69 | 70 | pub static I2C_ITEM: menu::Item = menu::Item { 71 | item_type: menu::ItemType::Callback { 72 | function: i2c, 73 | parameters: &[ 74 | menu::Parameter::Mandatory { 75 | parameter_name: "bus_idx", 76 | help: Some("I2C bus index"), 77 | }, 78 | menu::Parameter::Mandatory { 79 | parameter_name: "dev_addr", 80 | help: Some("7-bit I2C device address"), 81 | }, 82 | menu::Parameter::Mandatory { 83 | parameter_name: "tx_bytes", 84 | help: Some("Hex string to transmit"), 85 | }, 86 | menu::Parameter::Mandatory { 87 | parameter_name: "rx_count", 88 | help: Some("How many bytes to receive"), 89 | }, 90 | ], 91 | }, 92 | command: "i2c", 93 | help: Some("Do an I2C transaction on a bus"), 94 | }; 95 | 96 | /// Called when the "lsblk" command is executed. 97 | fn lsblk(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { 98 | let api = API.get(); 99 | let mut found = false; 100 | 101 | osprintln!("Block Devices:"); 102 | for dev_idx in 0..=255u8 { 103 | if let bios::FfiOption::Some(device_info) = (api.block_dev_get_info)(dev_idx) { 104 | let (bsize, bunits, dsize, dunits) = 105 | match device_info.num_blocks * u64::from(device_info.block_size) { 106 | x if x < (1024 * 1024 * 1024) => { 107 | // Under 1 GiB, give it in 10s of MiB 108 | (10 * x / (1024 * 1024), "MiB", x / 100_000, "MB") 109 | } 110 | x => { 111 | // Anything else in GiB 112 | (10 * x / (1024 * 1024 * 1024), "GiB", x / 100_000_000, "GB") 113 | } 114 | }; 115 | osprintln!("Device {}:", dev_idx); 116 | osprintln!("\t Name: {}", device_info.name); 117 | osprintln!("\t Type: {:?}", device_info.device_type); 118 | osprintln!("\tBlock size: {}", device_info.block_size); 119 | osprintln!("\tNum Blocks: {}", device_info.num_blocks); 120 | osprintln!( 121 | "\t Card Size: {}.{} {} ({}.{} {})", 122 | bsize / 10, 123 | bsize % 10, 124 | bunits, 125 | dsize / 10, 126 | dsize % 10, 127 | dunits 128 | ); 129 | osprintln!("\t Ejectable: {}", device_info.ejectable); 130 | osprintln!("\t Removable: {}", device_info.removable); 131 | osprintln!("\t Read Only: {}", device_info.read_only); 132 | osprintln!( 133 | "\t Media: {}", 134 | if device_info.media_present { 135 | "Present" 136 | } else { 137 | "Missing" 138 | } 139 | ); 140 | found = true; 141 | } 142 | } 143 | if !found { 144 | osprintln!("\tNone"); 145 | } 146 | } 147 | 148 | /// Called when the "lsbus" command is executed. 149 | fn lsbus(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { 150 | let api = API.get(); 151 | let mut found = false; 152 | osprintln!("Neotron Bus Devices:"); 153 | for dev_idx in 0..=255u8 { 154 | if let bios::FfiOption::Some(device_info) = (api.bus_get_info)(dev_idx) { 155 | let kind = match device_info.kind.make_safe() { 156 | Ok(bios::bus::PeripheralKind::Slot) => "Slot", 157 | Ok(bios::bus::PeripheralKind::SdCard) => "SdCard", 158 | Ok(bios::bus::PeripheralKind::Reserved) => "Reserved", 159 | _ => "Unknown", 160 | }; 161 | osprintln!("\t{}: {} ({})", dev_idx, device_info.name, kind); 162 | found = true; 163 | } 164 | } 165 | if !found { 166 | osprintln!("\tNone"); 167 | } 168 | } 169 | 170 | /// Called when the "lsi2c" command is executed. 171 | fn lsi2c(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { 172 | let api = API.get(); 173 | let mut found = false; 174 | osprintln!("I2C Buses:"); 175 | for dev_idx in 0..=255u8 { 176 | if let bios::FfiOption::Some(device_info) = (api.i2c_bus_get_info)(dev_idx) { 177 | osprintln!("\t{}: {}", dev_idx, device_info.name); 178 | found = true; 179 | } 180 | } 181 | if !found { 182 | osprintln!("\tNone"); 183 | } 184 | } 185 | 186 | /// Called when the "lsmem" command is executed. 187 | fn lsmem(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { 188 | let api = API.get(); 189 | let mut found = false; 190 | osprintln!("Memory regions:"); 191 | for region_idx in 0..=255u8 { 192 | if let bios::FfiOption::Some(region) = (api.memory_get_region)(region_idx) { 193 | osprintln!("\t{}: {}", region_idx, region); 194 | found = true; 195 | } 196 | } 197 | if !found { 198 | osprintln!("\tNone"); 199 | } 200 | } 201 | 202 | /// Called when the "lsuart" command is executed. 203 | fn lsuart(_menu: &menu::Menu, _item: &menu::Item, _args: &[&str], _ctx: &mut Ctx) { 204 | let api = API.get(); 205 | let mut found = false; 206 | osprintln!("UART Devices:"); 207 | for dev_idx in 0..=255u8 { 208 | if let bios::FfiOption::Some(device_info) = (api.serial_get_info)(dev_idx) { 209 | let device_type = match device_info.device_type.make_safe() { 210 | Ok(bios::serial::DeviceType::Rs232) => "RS232", 211 | Ok(bios::serial::DeviceType::TtlUart) => "TTL", 212 | Ok(bios::serial::DeviceType::UsbCdc) => "USB", 213 | Ok(bios::serial::DeviceType::Midi) => "MIDI", 214 | _ => "Unknown", 215 | }; 216 | osprintln!("\t{}: {} ({})", dev_idx, device_info.name, device_type); 217 | found = true; 218 | } 219 | } 220 | if !found { 221 | osprintln!("\tNone"); 222 | } 223 | } 224 | 225 | /// Called when the "shutdown" command is executed. 226 | fn shutdown(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { 227 | let api = API.get(); 228 | if let Ok(Some(_)) = menu::argument_finder(item, args, "reboot") { 229 | osprintln!("Rebooting..."); 230 | (api.power_control)(bios::PowerMode::Reset.make_ffi_safe()); 231 | } else if let Ok(Some(_)) = menu::argument_finder(item, args, "bootloader") { 232 | osprintln!("Rebooting into bootloader..."); 233 | (api.power_control)(bios::PowerMode::Bootloader.make_ffi_safe()); 234 | } else { 235 | osprintln!("Shutting down..."); 236 | (api.power_control)(bios::PowerMode::Off.make_ffi_safe()); 237 | } 238 | } 239 | 240 | /// Called when the "i2c" command is executed. 241 | fn i2c(_menu: &menu::Menu, item: &menu::Item, args: &[&str], _ctx: &mut Ctx) { 242 | let bus_idx = menu::argument_finder(item, args, "bus_idx").unwrap(); 243 | let dev_addr = menu::argument_finder(item, args, "dev_addr").unwrap(); 244 | let tx_bytes = menu::argument_finder(item, args, "tx_bytes").unwrap(); 245 | let rx_count = menu::argument_finder(item, args, "rx_count").unwrap(); 246 | 247 | let (Some(bus_idx), Some(dev_addr), Some(tx_bytes), Some(rx_count)) = 248 | (bus_idx, dev_addr, tx_bytes, rx_count) 249 | else { 250 | osprintln!("Missing arguments."); 251 | return; 252 | }; 253 | 254 | let mut tx_buffer: heapless::Vec = heapless::Vec::new(); 255 | 256 | for hex_pair in tx_bytes.as_bytes().chunks(2) { 257 | let Some(top) = hex_digit(hex_pair[0]) else { 258 | osprintln!("Bad hex."); 259 | return; 260 | }; 261 | let Some(bottom) = hex_digit(hex_pair[1]) else { 262 | osprintln!("Bad hex."); 263 | return; 264 | }; 265 | let byte = top << 4 | bottom; 266 | let Ok(_) = tx_buffer.push(byte) else { 267 | osprintln!("Too much hex."); 268 | return; 269 | }; 270 | } 271 | 272 | let Ok(bus_idx) = parse_u8(bus_idx) else { 273 | osprintln!("Bad bus_idx"); 274 | return; 275 | }; 276 | 277 | let Ok(dev_addr) = parse_u8(dev_addr) else { 278 | osprintln!("Bad dev_addr"); 279 | return; 280 | }; 281 | 282 | let Ok(rx_count) = parse_usize(rx_count) else { 283 | osprintln!("Bad rx count."); 284 | return; 285 | }; 286 | 287 | let mut rx_buf = [0u8; 16]; 288 | 289 | let Some(rx_buf) = rx_buf.get_mut(0..rx_count) else { 290 | osprintln!("Too much rx."); 291 | return; 292 | }; 293 | 294 | let api = API.get(); 295 | 296 | match (api.i2c_write_read)( 297 | bus_idx, 298 | dev_addr, 299 | tx_buffer.as_slice().into(), 300 | bios::FfiByteSlice::empty(), 301 | rx_buf.into(), 302 | ) { 303 | bios::FfiResult::Ok(_) => { 304 | osprintln!("Ok, got {:x?}", rx_buf); 305 | } 306 | bios::FfiResult::Err(e) => { 307 | osprintln!("Failed: {:?}", e); 308 | } 309 | } 310 | } 311 | 312 | /// Convert an ASCII hex digit into a number 313 | fn hex_digit(input: u8) -> Option { 314 | match input { 315 | b'0' => Some(0), 316 | b'1' => Some(1), 317 | b'2' => Some(2), 318 | b'3' => Some(3), 319 | b'4' => Some(4), 320 | b'5' => Some(5), 321 | b'6' => Some(6), 322 | b'7' => Some(7), 323 | b'8' => Some(8), 324 | b'9' => Some(9), 325 | b'a' | b'A' => Some(10), 326 | b'b' | b'B' => Some(11), 327 | b'c' | b'C' => Some(12), 328 | b'd' | b'D' => Some(13), 329 | b'e' | b'E' => Some(14), 330 | b'f' | b'F' => Some(15), 331 | _ => None, 332 | } 333 | } 334 | 335 | // End of file 336 | -------------------------------------------------------------------------------- /utilities/neoplay/src/player.rs: -------------------------------------------------------------------------------- 1 | //! Plays a MOD file. 2 | 3 | #[derive(Debug, Default)] 4 | struct Channel { 5 | sample_data: Option<*const u8>, 6 | sample_loops: bool, 7 | sample_length: usize, 8 | repeat_length: usize, 9 | repeat_point: usize, 10 | volume: u8, 11 | note_period: u16, 12 | sample_position: neotracker::Fractional, 13 | note_step: neotracker::Fractional, 14 | effect: Option, 15 | } 16 | 17 | pub struct Player<'a> { 18 | modfile: neotracker::ProTrackerModule<'a>, 19 | /// How many samples left in this tick 20 | samples_left: u32, 21 | /// How many ticks left in this line 22 | ticks_left: u32, 23 | ticks_per_line: u32, 24 | third_ticks_per_line: u32, 25 | samples_per_tick: u32, 26 | clock_ticks_per_device_sample: neotracker::Fractional, 27 | position: u8, 28 | line: u8, 29 | finished: bool, 30 | /// This is set when we get a Pattern Break (0xDxx) effect. It causes 31 | /// us to jump to a specific row in the next pattern. 32 | pattern_break: Option, 33 | channels: [Channel; 4], 34 | } 35 | 36 | /// This code is based on https://www.codeslow.com/2019/02/in-this-post-we-will-finally-have-some.html?m=1 37 | impl<'a> Player<'a> { 38 | /// Make a new player, at the given sample rate. 39 | pub fn new(data: &'static [u8], sample_rate: u32) -> Result, neotracker::Error> { 40 | // We need a 'static reference to this data, and we're not going to free it. 41 | // So just leak it. 42 | let modfile = neotracker::ProTrackerModule::new(data)?; 43 | Ok(Player { 44 | modfile, 45 | samples_left: 0, 46 | ticks_left: 0, 47 | ticks_per_line: 6, 48 | third_ticks_per_line: 2, 49 | samples_per_tick: sample_rate / 50, 50 | position: 0, 51 | line: 0, 52 | finished: false, 53 | clock_ticks_per_device_sample: neotracker::Fractional::new_from_sample_rate( 54 | sample_rate, 55 | ), 56 | pattern_break: None, 57 | channels: [ 58 | Channel::default(), 59 | Channel::default(), 60 | Channel::default(), 61 | Channel::default(), 62 | ], 63 | }) 64 | } 65 | 66 | /// Are we finished playing? 67 | pub fn is_finished(&self) -> bool { 68 | self.finished 69 | } 70 | 71 | /// Return a stereo sample pair 72 | pub fn next_sample(&mut self, out: &mut T) -> (i16, i16) 73 | where 74 | T: core::fmt::Write, 75 | { 76 | if self.ticks_left == 0 && self.samples_left == 0 { 77 | // It is time for a new line 78 | 79 | // Did we have a pattern break? Jump straight there. 80 | if let Some(line) = self.pattern_break { 81 | self.pattern_break = None; 82 | self.position += 1; 83 | self.line = line; 84 | } 85 | 86 | // Find which line we play next. It might be the next line in this 87 | // pattern, or it might be the first line in the next pattern. 88 | let line = loop { 89 | // Work out which pattern we're playing 90 | let Some(pattern_idx) = self.modfile.song_position(self.position) else { 91 | self.finished = true; 92 | return (0, 0); 93 | }; 94 | // Grab the pattern 95 | let pattern = self.modfile.pattern(pattern_idx).expect("Get pattern"); 96 | // Get the line from the pattern 97 | let Some(line) = pattern.line(self.line) else { 98 | // Go to start of next pattern 99 | self.line = 0; 100 | self.position += 1; 101 | continue; 102 | }; 103 | // There was no need to go the next pattern, so produce this 104 | // line from the loop. 105 | break line; 106 | }; 107 | 108 | // Load four channels with new line data 109 | let _ = write!(out, "{:03} {:06}: ", self.position, self.line); 110 | for (channel_num, ch) in self.channels.iter_mut().enumerate() { 111 | let note = &line.channel[channel_num]; 112 | // Do we have a new sample to play? 113 | if note.is_empty() { 114 | let _ = write!(out, "--- -----|"); 115 | } else { 116 | if let Some(sample) = self.modfile.sample(note.sample_no()) { 117 | // if the period is zero, keep playing the old note 118 | if note.period() != 0 { 119 | ch.note_period = note.period(); 120 | ch.note_step = self 121 | .clock_ticks_per_device_sample 122 | .apply_period(ch.note_period); 123 | } 124 | ch.volume = sample.volume(); 125 | ch.sample_data = Some(sample.raw_sample_bytes().as_ptr()); 126 | ch.sample_loops = sample.loops(); 127 | ch.sample_length = sample.sample_length_bytes(); 128 | ch.repeat_length = sample.repeat_length_bytes(); 129 | ch.repeat_point = sample.repeat_point_bytes(); 130 | ch.sample_position = neotracker::Fractional::default(); 131 | } 132 | let _ = write!( 133 | out, 134 | "{:3x} {:02}{:03x}|", 135 | note.period(), 136 | note.sample_no(), 137 | note.effect_u16() 138 | ); 139 | } 140 | ch.effect = None; 141 | match note.effect() { 142 | e @ Some( 143 | neotracker::Effect::Arpeggio(_) 144 | | neotracker::Effect::SlideUp(_) 145 | | neotracker::Effect::SlideDown(_) 146 | | neotracker::Effect::VolumeSlide(_), 147 | ) => { 148 | // we'll need this for later 149 | ch.effect = e; 150 | } 151 | Some(neotracker::Effect::SetVolume(value)) => { 152 | ch.volume = value; 153 | } 154 | Some(neotracker::Effect::SetSpeed(value)) => { 155 | if value <= 31 { 156 | self.ticks_per_line = u32::from(value); 157 | self.third_ticks_per_line = u32::from(value / 3); 158 | } else { 159 | // They are trying to set speed in beats per minute 160 | } 161 | } 162 | Some(neotracker::Effect::SampleOffset(n)) => { 163 | let offset = u32::from(n) * 256; 164 | ch.sample_position = neotracker::Fractional::new(offset); 165 | } 166 | Some(neotracker::Effect::PatternBreak(row)) => { 167 | // Start the next pattern early, at the given row 168 | self.pattern_break = Some(row); 169 | } 170 | Some(_e) => { 171 | // eprintln!("Unhandled effect {:02x?}", e); 172 | } 173 | None => { 174 | // Do nothing 175 | } 176 | } 177 | } 178 | let _ = writeln!(out); 179 | 180 | self.line += 1; 181 | self.samples_left = self.samples_per_tick - 1; 182 | self.ticks_left = self.ticks_per_line - 1; 183 | } else if self.samples_left == 0 { 184 | // end of a tick 185 | self.samples_left = self.samples_per_tick - 1; 186 | self.ticks_left -= 1; 187 | let lower_third = self.third_ticks_per_line; 188 | let upper_third = lower_third * 2; 189 | for ch in self.channels.iter_mut() { 190 | match ch.effect { 191 | Some(neotracker::Effect::Arpeggio(n)) => { 192 | if self.ticks_left == upper_third { 193 | let half_steps = n >> 4; 194 | if let Some(new_period) = 195 | neotracker::shift_period(ch.note_period, half_steps) 196 | { 197 | ch.note_period = new_period; 198 | ch.note_step = self 199 | .clock_ticks_per_device_sample 200 | .apply_period(ch.note_period); 201 | } 202 | } else if self.ticks_left == lower_third { 203 | let first_half_steps = n >> 4; 204 | let second_half_steps = n & 0x0F; 205 | if let Some(new_period) = neotracker::shift_period( 206 | ch.note_period, 207 | second_half_steps - first_half_steps, 208 | ) { 209 | ch.note_period = new_period; 210 | ch.note_step = self 211 | .clock_ticks_per_device_sample 212 | .apply_period(ch.note_period); 213 | } 214 | } 215 | } 216 | Some(neotracker::Effect::SlideUp(n)) => { 217 | ch.note_period -= u16::from(n); 218 | ch.note_step = self 219 | .clock_ticks_per_device_sample 220 | .apply_period(ch.note_period); 221 | } 222 | Some(neotracker::Effect::SlideDown(n)) => { 223 | ch.note_period += u16::from(n); 224 | ch.note_step = self 225 | .clock_ticks_per_device_sample 226 | .apply_period(ch.note_period); 227 | } 228 | Some(neotracker::Effect::VolumeSlide(n)) => { 229 | let new_volume = (ch.volume as i8) + n; 230 | if (0..=63).contains(&new_volume) { 231 | ch.volume = new_volume as u8; 232 | } 233 | } 234 | _ => { 235 | // do nothing 236 | } 237 | } 238 | } 239 | } else { 240 | // just another sample 241 | self.samples_left -= 1; 242 | } 243 | 244 | // Pump existing channels 245 | let mut left_sample = 0; 246 | let mut right_sample = 0; 247 | for (ch_idx, ch) in self.channels.iter_mut().enumerate() { 248 | if ch.note_period == 0 || ch.sample_length == 0 { 249 | continue; 250 | } 251 | let Some(sample_data) = ch.sample_data else { 252 | continue; 253 | }; 254 | let integer_pos = ch.sample_position.as_index(); 255 | let sample_byte = unsafe { sample_data.add(integer_pos).read() } as i8; 256 | let mut channel_value = (sample_byte as i8) as i32; 257 | // max channel vol (64), sample range [-128,127] scaled to [-32768, 32767] 258 | channel_value *= 256; 259 | channel_value *= i32::from(ch.volume); 260 | channel_value /= 64; 261 | // move the sample index by a non-integer amount 262 | ch.sample_position += ch.note_step; 263 | // loop sample if required 264 | if ch.sample_loops { 265 | if ch.sample_position.as_index() >= (ch.repeat_point + ch.repeat_length) { 266 | ch.sample_position = neotracker::Fractional::new(ch.repeat_point as u32); 267 | } 268 | } else if ch.sample_position.as_index() >= ch.sample_length { 269 | // stop playing sample 270 | ch.note_period = 0; 271 | } 272 | 273 | if ch_idx == 0 || ch_idx == 3 { 274 | left_sample += channel_value; 275 | } else { 276 | right_sample += channel_value; 277 | } 278 | } 279 | 280 | ( 281 | left_sample.clamp(-32768, 32767) as i16, 282 | right_sample.clamp(-32768, 32767) as i16, 283 | ) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /neotron-os/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # The Neotron Operating System 2 | //! 3 | //! This OS is intended to be loaded by a Neotron BIOS. 4 | //! 5 | //! Copyright (c) The Neotron Developers, 2022 6 | //! 7 | //! Licence: GPL v3 or higher (see ../LICENCE.md) 8 | 9 | #![cfg_attr(not(test), no_std)] 10 | 11 | // =========================================================================== 12 | // Modules and Imports 13 | // =========================================================================== 14 | 15 | use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; 16 | 17 | use neotron_common_bios as bios; 18 | 19 | mod commands; 20 | mod config; 21 | mod fs; 22 | mod program; 23 | mod refcell; 24 | mod vgaconsole; 25 | 26 | pub use config::Config as OsConfig; 27 | use refcell::CsRefCell; 28 | 29 | // =========================================================================== 30 | // Global Variables 31 | // =========================================================================== 32 | 33 | /// The OS version string 34 | const OS_VERSION: &str = concat!("Neotron OS, v", env!("OS_VERSION")); 35 | 36 | /// Used to convert between POSIX epoch (for `chrono`) and Neotron epoch (for BIOS APIs). 37 | const SECONDS_BETWEEN_UNIX_AND_NEOTRON_EPOCH: i64 = 946684800; 38 | 39 | /// We store the API object supplied by the BIOS here 40 | static API: Api = Api::new(); 41 | 42 | /// We store our VGA console here. 43 | static VGA_CONSOLE: CsRefCell> = CsRefCell::new(None); 44 | 45 | /// We store our serial console here. 46 | static SERIAL_CONSOLE: CsRefCell> = CsRefCell::new(None); 47 | 48 | /// Our overall text output console. 49 | /// 50 | /// Writes to the VGA console and/or the serial console (depending on which is 51 | /// configured). 52 | static CONSOLE: Console = Console; 53 | 54 | /// Note if we are panicking right now. 55 | /// 56 | /// If so, don't panic if a serial write fails. 57 | static IS_PANIC: AtomicBool = AtomicBool::new(false); 58 | 59 | /// Our keyboard controller 60 | static STD_INPUT: CsRefCell = CsRefCell::new(StdInput::new()); 61 | 62 | static FILESYSTEM: fs::Filesystem = fs::Filesystem::new(); 63 | 64 | #[cfg(romfs_enabled = "yes")] 65 | static ROMFS: &[u8] = include_bytes!(env!("ROMFS_PATH")); 66 | 67 | #[cfg(not(romfs_enabled = "yes"))] 68 | static ROMFS: &[u8] = &[]; 69 | 70 | // =========================================================================== 71 | // Macros 72 | // =========================================================================== 73 | 74 | /// Prints to the screen 75 | #[macro_export] 76 | macro_rules! osprint { 77 | ($($arg:tt)*) => { { 78 | #[allow(unused)] 79 | use core::fmt::Write as _; 80 | let _ = write!(&$crate::CONSOLE, $($arg)*); 81 | } } 82 | } 83 | 84 | /// Prints to the screen and puts a new-line on the end 85 | #[macro_export] 86 | macro_rules! osprintln { 87 | () => ($crate::osprint!("\n")); 88 | ($($arg:tt)*) => { 89 | $crate::osprint!($($arg)*); 90 | $crate::osprint!("\n"); 91 | }; 92 | } 93 | 94 | // =========================================================================== 95 | // Local types 96 | // =========================================================================== 97 | 98 | /// Represents the API supplied by the BIOS 99 | struct Api { 100 | bios: AtomicPtr, 101 | } 102 | 103 | impl Api { 104 | /// Create a new object with a null pointer for the BIOS API. 105 | const fn new() -> Api { 106 | Api { 107 | bios: AtomicPtr::new(core::ptr::null_mut()), 108 | } 109 | } 110 | 111 | /// Change the stored BIOS API pointer. 112 | /// 113 | /// The pointed-at object must have static lifetime. 114 | unsafe fn store(&self, api: *const bios::Api) { 115 | self.bios.store(api as *mut bios::Api, Ordering::SeqCst) 116 | } 117 | 118 | /// Get the BIOS API as a reference. 119 | /// 120 | /// Will panic if the stored pointer is null. 121 | fn get(&self) -> &'static bios::Api { 122 | let ptr = self.bios.load(Ordering::SeqCst) as *const bios::Api; 123 | let api_ref = unsafe { ptr.as_ref() }.expect("BIOS API should be non-null"); 124 | api_ref 125 | } 126 | 127 | /// Get the current time 128 | fn get_time(&self) -> chrono::NaiveDateTime { 129 | let api = self.get(); 130 | let bios_time = (api.time_clock_get)(); 131 | let secs = i64::from(bios_time.secs) + SECONDS_BETWEEN_UNIX_AND_NEOTRON_EPOCH; 132 | let nsecs = bios_time.nsecs; 133 | chrono::DateTime::from_timestamp(secs, nsecs) 134 | .unwrap() 135 | .naive_utc() 136 | } 137 | 138 | /// Set the current time 139 | fn set_time(&self, timestamp: chrono::NaiveDateTime) { 140 | let api = self.get(); 141 | let seconds = timestamp.and_utc().timestamp(); 142 | let nanos = timestamp.and_utc().timestamp_subsec_nanos(); 143 | let bios_time = bios::Time { 144 | secs: (seconds - SECONDS_BETWEEN_UNIX_AND_NEOTRON_EPOCH) as u32, 145 | nsecs: nanos, 146 | }; 147 | (api.time_clock_set)(bios_time); 148 | } 149 | } 150 | 151 | /// Represents the serial port we can use as a text input/output device. 152 | struct SerialConsole(u8); 153 | 154 | impl SerialConsole { 155 | /// Write some bytes to the serial console 156 | fn write_bstr(&mut self, mut data: &[u8]) -> Result<(), bios::Error> { 157 | let api = API.get(); 158 | while !data.is_empty() { 159 | let res: Result = (api.serial_write)( 160 | // Which port 161 | self.0, 162 | // Data 163 | bios::FfiByteSlice::new(data), 164 | // No timeout 165 | bios::FfiOption::None, 166 | ) 167 | .into(); 168 | let count = match res { 169 | Ok(n) => n, 170 | Err(_e) => { 171 | // If we can't write to the serial port, let's not break any 172 | // other consoles we might have configured. Instead, just 173 | // quit now and pretend we wrote it all. 174 | return Ok(()); 175 | } 176 | }; 177 | data = &data[count..]; 178 | } 179 | Ok(()) 180 | } 181 | 182 | /// Try and get as many bytes as we can from the serial console. 183 | fn read_data(&mut self, buffer: &mut [u8]) -> Result { 184 | let api = API.get(); 185 | let ffi_buffer = bios::FfiBuffer::new(buffer); 186 | let res = (api.serial_read)( 187 | self.0, 188 | ffi_buffer, 189 | bios::FfiOption::Some(bios::Timeout::new_ms(0)), 190 | ); 191 | res.into() 192 | } 193 | } 194 | 195 | impl core::fmt::Write for SerialConsole { 196 | fn write_str(&mut self, data: &str) -> core::fmt::Result { 197 | self.write_bstr(data.as_bytes()) 198 | .map_err(|_e| core::fmt::Error) 199 | } 200 | } 201 | 202 | /// Represents either or both of the VGA console and the serial console. 203 | struct Console; 204 | 205 | impl core::fmt::Write for &Console { 206 | fn write_str(&mut self, s: &str) -> core::fmt::Result { 207 | if let Ok(mut guard) = VGA_CONSOLE.try_lock() { 208 | if let Some(vga_console) = guard.as_mut() { 209 | vga_console.write_str(s)?; 210 | } 211 | } 212 | 213 | if let Ok(mut guard) = SERIAL_CONSOLE.try_lock() { 214 | if let Some(serial_console) = guard.as_mut() { 215 | serial_console.write_str(s)?; 216 | } 217 | } 218 | 219 | Ok(()) 220 | } 221 | } 222 | 223 | /// Represents the standard input of our console 224 | struct StdInput { 225 | keyboard: pc_keyboard::EventDecoder, 226 | buffer: heapless::spsc::Queue, 227 | } 228 | 229 | impl StdInput { 230 | const fn new() -> StdInput { 231 | StdInput { 232 | keyboard: pc_keyboard::EventDecoder::new( 233 | pc_keyboard::layouts::AnyLayout::Uk105Key(pc_keyboard::layouts::Uk105Key), 234 | pc_keyboard::HandleControl::MapLettersToUnicode, 235 | ), 236 | buffer: heapless::spsc::Queue::new(), 237 | } 238 | } 239 | 240 | fn get_buffered_data(&mut self, buffer: &mut [u8]) -> usize { 241 | // If there is some data, get it. 242 | let mut count = 0; 243 | for slot in buffer.iter_mut() { 244 | if let Some(n) = self.buffer.dequeue() { 245 | *slot = n; 246 | count += 1; 247 | } 248 | } 249 | count 250 | } 251 | 252 | /// Gets a raw event from the keyboard 253 | fn get_raw(&mut self) -> Option { 254 | let api = API.get(); 255 | match (api.hid_get_event)() { 256 | bios::ApiResult::Ok(bios::FfiOption::Some(bios::hid::HidEvent::KeyPress(code))) => { 257 | let pckb_ev = pc_keyboard::KeyEvent { 258 | code, 259 | state: pc_keyboard::KeyState::Down, 260 | }; 261 | self.keyboard.process_keyevent(pckb_ev) 262 | } 263 | bios::ApiResult::Ok(bios::FfiOption::Some(bios::hid::HidEvent::KeyRelease(code))) => { 264 | let pckb_ev = pc_keyboard::KeyEvent { 265 | code, 266 | state: pc_keyboard::KeyState::Up, 267 | }; 268 | self.keyboard.process_keyevent(pckb_ev) 269 | } 270 | bios::ApiResult::Ok(bios::FfiOption::Some(bios::hid::HidEvent::MouseInput( 271 | _ignore, 272 | ))) => None, 273 | bios::ApiResult::Ok(bios::FfiOption::None) => { 274 | // Do nothing 275 | None 276 | } 277 | bios::ApiResult::Err(_e) => None, 278 | } 279 | } 280 | 281 | /// Gets some input bytes, as UTF-8. 282 | /// 283 | /// The data you get might be cut in the middle of a UTF-8 character. 284 | fn get_data(&mut self, buffer: &mut [u8]) -> usize { 285 | let count = self.get_buffered_data(buffer); 286 | if buffer.is_empty() || count > 0 { 287 | return count; 288 | } 289 | 290 | // Nothing buffered - ask the keyboard for something 291 | let decoded_key = self.get_raw(); 292 | 293 | match decoded_key { 294 | Some(pc_keyboard::DecodedKey::Unicode(mut ch)) => { 295 | if ch == '\n' { 296 | ch = '\r'; 297 | } 298 | let mut buffer = [0u8; 6]; 299 | let s = ch.encode_utf8(&mut buffer); 300 | for b in s.as_bytes() { 301 | // This will always fit 302 | self.buffer.enqueue(*b).unwrap(); 303 | } 304 | } 305 | Some(pc_keyboard::DecodedKey::RawKey(pc_keyboard::KeyCode::ArrowRight)) => { 306 | // Load the ANSI sequence for a right arrow 307 | for b in b"\x1b[0;77b" { 308 | // This will always fit 309 | self.buffer.enqueue(*b).unwrap(); 310 | } 311 | } 312 | _ => { 313 | // Drop anything else 314 | } 315 | } 316 | 317 | if let Some(console) = SERIAL_CONSOLE.lock().as_mut() { 318 | while !self.buffer.is_full() { 319 | let mut buffer = [0u8]; 320 | if let Ok(1) = console.read_data(&mut buffer) { 321 | self.buffer.enqueue(buffer[0]).unwrap(); 322 | } else { 323 | break; 324 | } 325 | } 326 | } 327 | 328 | self.get_buffered_data(buffer) 329 | } 330 | } 331 | 332 | /// Local context used by the main menu. 333 | /// 334 | /// Stuff goes here in preference, but we take it out of here and make it a 335 | /// global if we have to. 336 | pub struct Ctx { 337 | config: config::Config, 338 | tpa: program::TransientProgramArea, 339 | /// This flag is set if the "run" command is entered. It tells us 340 | /// to take our input bytes from the TPA. 341 | exec_tpa: Option, 342 | } 343 | 344 | impl core::fmt::Write for Ctx { 345 | fn write_str(&mut self, data: &str) -> core::fmt::Result { 346 | osprint!("{}", data); 347 | Ok(()) 348 | } 349 | } 350 | 351 | // =========================================================================== 352 | // Private functions 353 | // =========================================================================== 354 | 355 | /// Initialise our global variables - the BIOS will not have done this for us 356 | /// (as it doesn't know where they are). 357 | #[cfg(all(target_os = "none", not(feature = "lib-mode")))] 358 | unsafe fn start_up_init() { 359 | use core::ptr::{addr_of, addr_of_mut}; 360 | 361 | extern "C" { 362 | 363 | // These symbols come from `link.x` 364 | static mut __sbss: u32; 365 | static mut __ebss: u32; 366 | 367 | static mut __sdata: u32; 368 | static mut __edata: u32; 369 | static __sidata: u32; 370 | } 371 | 372 | r0::zero_bss(addr_of_mut!(__sbss), addr_of_mut!(__ebss)); 373 | r0::init_data( 374 | addr_of_mut!(__sdata), 375 | addr_of_mut!(__edata), 376 | addr_of!(__sidata), 377 | ); 378 | } 379 | 380 | #[cfg(any(not(target_os = "none"), feature = "lib-mode"))] 381 | unsafe fn start_up_init() { 382 | // Nothing to do 383 | } 384 | 385 | // =========================================================================== 386 | // Public functions / impl for public types 387 | // =========================================================================== 388 | 389 | /// This is the function the BIOS calls. This is because we store the address 390 | /// of this function in the ENTRY_POINT_ADDR variable. 391 | #[no_mangle] 392 | pub extern "C" fn os_main(api: &bios::Api) -> ! { 393 | unsafe { 394 | start_up_init(); 395 | API.store(api); 396 | } 397 | 398 | let api = API.get(); 399 | if (api.api_version_get)() != bios::API_VERSION { 400 | panic!("API mismatch!"); 401 | } 402 | 403 | let config = config::Config::load().unwrap_or_default(); 404 | 405 | if let Some(mut mode) = config.get_vga_console() { 406 | // Set the configured mode 407 | if let bios::FfiResult::Err(_e) = 408 | unsafe { (api.video_set_mode)(mode, core::ptr::null_mut()) } 409 | { 410 | // Failed to change mode - check what mode we're in 411 | mode = (api.video_get_mode)(); 412 | }; 413 | // Work with whatever we get 414 | let (width, height) = (mode.text_width(), mode.text_height()); 415 | 416 | if let (Some(width), Some(height)) = (width, height) { 417 | let mut vga = vgaconsole::VgaConsole::new( 418 | (api.video_get_framebuffer)(), 419 | width as isize, 420 | height as isize, 421 | ); 422 | vga.clear(); 423 | let mut guard = VGA_CONSOLE.lock(); 424 | *guard = Some(vga); 425 | // Drop the lock before trying to grab it again to print something! 426 | drop(guard); 427 | osprintln!("\u{001b}[0mConfigured VGA console {}x{}", width, height); 428 | } 429 | } 430 | 431 | if let Some((idx, serial_config)) = config.get_serial_console() { 432 | let _ignored = (api.serial_configure)(idx, serial_config); 433 | let mut guard = SERIAL_CONSOLE.lock(); 434 | *guard = Some(SerialConsole(idx)); 435 | // Drop the lock before trying to grab it again to print something! 436 | drop(guard); 437 | osprintln!("Configured Serial console on Serial {}", idx); 438 | } 439 | 440 | // Now we can call osprintln! 441 | osprintln!("\u{001b}[44;33;1m{}\u{001b}[0m", OS_VERSION); 442 | osprintln!("\u{001b}[41;37;1mCopyright © Jonathan 'theJPster' Pallant and the Neotron Developers, 2022\u{001b}[0m"); 443 | 444 | let (tpa_start, tpa_size) = match (api.memory_get_region)(0) { 445 | bios::FfiOption::None => { 446 | panic!("No TPA offered by BIOS!"); 447 | } 448 | bios::FfiOption::Some(tpa) => { 449 | if tpa.length < 256 { 450 | panic!("TPA not large enough"); 451 | } 452 | let offset = tpa.start.align_offset(4); 453 | ( 454 | unsafe { tpa.start.add(offset) as *mut u32 }, 455 | tpa.length - offset, 456 | ) 457 | } 458 | }; 459 | 460 | let mut ctx = Ctx { 461 | config, 462 | tpa: unsafe { 463 | // We have to trust the values given to us by the BIOS. If it lies, we will crash. 464 | program::TransientProgramArea::new(tpa_start, tpa_size) 465 | }, 466 | exec_tpa: None, 467 | }; 468 | 469 | osprintln!( 470 | "\u{001b}[7mTPA: {} bytes @ {:p}\u{001b}[0m", 471 | ctx.tpa.as_slice_u8().len(), 472 | ctx.tpa.as_slice_u8().as_ptr() 473 | ); 474 | 475 | // Show the cursor 476 | osprint!("\u{001b}[?25h"); 477 | 478 | let mut buffer = [0u8; 256]; 479 | let mut menu = menu::Runner::new(&commands::OS_MENU, &mut buffer, ctx); 480 | 481 | loop { 482 | let mut buffer = [0u8; 16]; 483 | let count = { STD_INPUT.lock().get_data(&mut buffer) }; 484 | for b in &buffer[0..count] { 485 | menu.input_byte(*b); 486 | } 487 | // TODO: Consider recursively executing scripts, so that scripts can 488 | // call scripts. 489 | if let Some(n) = menu.context.exec_tpa { 490 | menu.context.exec_tpa = None; 491 | let ptr = menu.context.tpa.steal_top(n); 492 | osprintln!("\rExecuting TPA..."); 493 | let mut has_chars = false; 494 | let slice = unsafe { core::slice::from_raw_parts(ptr, n) }; 495 | // TODO: Give the user some way to break out of the loop. 496 | for b in slice { 497 | // Files contain `\n` or `\r\n` line endings. 498 | // menu wants `\r` line endings. 499 | if *b == b'\n' { 500 | if has_chars { 501 | // Execute this line 502 | menu.input_byte(b'\r'); 503 | has_chars = false; 504 | } 505 | } else if *b == b'\r' { 506 | // Drop carriage returns 507 | } else { 508 | menu.input_byte(*b); 509 | has_chars = true; 510 | } 511 | } 512 | unsafe { 513 | menu.context.tpa.restore_top(n); 514 | } 515 | } 516 | (api.power_idle)(); 517 | } 518 | } 519 | 520 | /// Called when we have a panic. 521 | #[inline(never)] 522 | #[panic_handler] 523 | #[cfg(not(any(feature = "lib-mode", test)))] 524 | fn panic(info: &core::panic::PanicInfo) -> ! { 525 | IS_PANIC.store(true, Ordering::Relaxed); 526 | osprintln!("PANIC!\n{:#?}", info); 527 | let api = API.get(); 528 | loop { 529 | (api.power_idle)(); 530 | } 531 | } 532 | 533 | // =========================================================================== 534 | // End of file 535 | // =========================================================================== 536 | -------------------------------------------------------------------------------- /utilities/snake/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Game logic for Snake 2 | 3 | #![no_std] 4 | #![deny(missing_docs)] 5 | #![deny(unsafe_code)] 6 | 7 | use core::fmt::Write; 8 | 9 | use neotron_sdk::console; 10 | 11 | /// Represents the Snake application 12 | /// 13 | /// An application can play multiple games. 14 | pub struct App { 15 | game: Game, 16 | width: u8, 17 | height: u8, 18 | stdout: neotron_sdk::File, 19 | stdin: neotron_sdk::File, 20 | } 21 | 22 | impl App { 23 | /// Make a new snake application. 24 | /// 25 | /// You can give the screen size in characters. There will be a border and 26 | /// the board will be two units smaller in each axis. 27 | pub const fn new(width: u8, height: u8) -> App { 28 | App { 29 | game: Game::new(width - 2, height - 2, console::Position { row: 1, col: 1 }), 30 | width, 31 | height, 32 | stdout: neotron_sdk::stdout(), 33 | stdin: neotron_sdk::stdin(), 34 | } 35 | } 36 | 37 | /// Play multiple games of snake. 38 | /// 39 | /// Loops playing games and printing scores. 40 | pub fn play(&mut self) { 41 | console::cursor_off(&mut self.stdout); 42 | self.clear_screen(); 43 | self.title_screen(); 44 | 45 | let mut seed: u16 = 0x4f34; 46 | 47 | 'outer: loop { 48 | 'inner: loop { 49 | let key = self.wait_for_key(); 50 | seed = seed.wrapping_add(1); 51 | if key == b'q' || key == b'Q' { 52 | break 'outer; 53 | } 54 | if key == b'p' || key == b'P' { 55 | break 'inner; 56 | } 57 | } 58 | 59 | self.clear_screen(); 60 | 61 | neotron_sdk::srand(seed); 62 | 63 | let score = self.game.play(&mut self.stdin, &mut self.stdout); 64 | 65 | self.winning_message(score); 66 | } 67 | 68 | // show cursor 69 | console::cursor_on(&mut self.stdout); 70 | self.clear_screen(); 71 | } 72 | 73 | /// Clear the screen and draw the board. 74 | fn clear_screen(&mut self) { 75 | console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); 76 | console::clear_screen(&mut self.stdout); 77 | console::set_sgr( 78 | &mut self.stdout, 79 | [ 80 | console::SgrParam::Bold, 81 | console::SgrParam::FgYellow, 82 | console::SgrParam::BgBlack, 83 | ], 84 | ); 85 | console::move_cursor(&mut self.stdout, console::Position::origin()); 86 | let _ = self.stdout.write_char('╔'); 87 | for _ in 1..self.width - 1 { 88 | let _ = self.stdout.write_char('═'); 89 | } 90 | let _ = self.stdout.write_char('╗'); 91 | console::move_cursor( 92 | &mut self.stdout, 93 | console::Position { 94 | row: self.height - 1, 95 | col: 0, 96 | }, 97 | ); 98 | let _ = self.stdout.write_char('╚'); 99 | for _ in 1..self.width - 1 { 100 | let _ = self.stdout.write_char('═'); 101 | } 102 | let _ = self.stdout.write_char('╝'); 103 | for row in 1..self.height - 1 { 104 | console::move_cursor(&mut self.stdout, console::Position { row, col: 0 }); 105 | let _ = self.stdout.write_char('║'); 106 | console::move_cursor( 107 | &mut self.stdout, 108 | console::Position { 109 | row, 110 | col: self.width - 1, 111 | }, 112 | ); 113 | let _ = self.stdout.write_char('║'); 114 | } 115 | console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); 116 | } 117 | 118 | /// Show the title screen 119 | fn title_screen(&mut self) { 120 | console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); 121 | let message = "Neotron Snake by theJPster"; 122 | let pos = console::Position { 123 | row: self.height / 2, 124 | col: (self.width - message.chars().count() as u8) / 2, 125 | }; 126 | console::move_cursor(&mut self.stdout, pos); 127 | let _ = self.stdout.write_str(message); 128 | let message = "Q to Quit | 'P' to Play"; 129 | let pos = console::Position { 130 | row: pos.row + 1, 131 | col: (self.width - message.chars().count() as u8) / 2, 132 | }; 133 | console::move_cursor(&mut self.stdout, pos); 134 | let _ = self.stdout.write_str(message); 135 | } 136 | 137 | /// Spin until a key is pressed 138 | fn wait_for_key(&mut self) -> u8 { 139 | loop { 140 | let mut buffer = [0u8; 1]; 141 | if let Ok(1) = self.stdin.read(&mut buffer) { 142 | return buffer[0]; 143 | } 144 | neotron_sdk::delay(core::time::Duration::from_millis(10)); 145 | } 146 | } 147 | 148 | /// Print the game over message with the given score 149 | fn winning_message(&mut self, score: u32) { 150 | console::set_sgr(&mut self.stdout, [console::SgrParam::Reset]); 151 | let pos = console::Position { 152 | row: self.height / 2, 153 | col: (self.width - 13u8) / 2, 154 | }; 155 | console::move_cursor(&mut self.stdout, pos); 156 | let _ = writeln!(self.stdout, "Score: {:06}", score); 157 | let message = "Q to Quit | 'P' to Play"; 158 | let pos = console::Position { 159 | row: pos.row + 1, 160 | col: (self.width - message.chars().count() as u8) / 2, 161 | }; 162 | console::move_cursor(&mut self.stdout, pos); 163 | let _ = self.stdout.write_str(message); 164 | } 165 | } 166 | 167 | /// Something we can send to the ANSI console 168 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 169 | enum Piece { 170 | Head, 171 | Food, 172 | Body, 173 | } 174 | 175 | impl Piece { 176 | /// Get the Unicode char for this piece 177 | fn get_char(self) -> char { 178 | match self { 179 | Piece::Body => '▓', 180 | Piece::Head => '█', 181 | Piece::Food => '▲', 182 | } 183 | } 184 | 185 | /// Get the ANSI colour for this piece 186 | fn get_colour(self) -> console::SgrParam { 187 | match self { 188 | Piece::Body => console::SgrParam::FgMagenta, 189 | Piece::Head => console::SgrParam::FgYellow, 190 | Piece::Food => console::SgrParam::FgGreen, 191 | } 192 | } 193 | } 194 | 195 | /// Represents one game of Snake 196 | struct Game { 197 | board: Board<{ Self::MAX_WIDTH }, { Self::MAX_HEIGHT }>, 198 | width: u8, 199 | height: u8, 200 | offset: console::Position, 201 | head: console::Position, 202 | tail: console::Position, 203 | direction: Direction, 204 | score: u32, 205 | digesting: u32, 206 | tick_interval_ms: u16, 207 | } 208 | 209 | impl Game { 210 | /// The maximum width board we can handle 211 | pub const MAX_WIDTH: usize = 78; 212 | /// The maximum height board we can handle 213 | pub const MAX_HEIGHT: usize = 23; 214 | /// How many ms per tick do we start at? 215 | const STARTING_TICK: u16 = 100; 216 | 217 | /// Make a new game. 218 | /// 219 | /// Give the width and the height of the game board, and where on the screen 220 | /// the board should be located. 221 | const fn new(width: u8, height: u8, offset: console::Position) -> Game { 222 | Game { 223 | board: Board::new(), 224 | width, 225 | height, 226 | offset, 227 | head: console::Position { row: 0, col: 0 }, 228 | tail: console::Position { row: 0, col: 0 }, 229 | direction: Direction::Up, 230 | score: 0, 231 | digesting: 3, 232 | tick_interval_ms: Self::STARTING_TICK, 233 | } 234 | } 235 | 236 | /// Play a game 237 | fn play(&mut self, stdin: &mut neotron_sdk::File, stdout: &mut neotron_sdk::File) -> u32 { 238 | // Reset score and speed, and start with a bit of snake 239 | self.score = 0; 240 | self.tick_interval_ms = Self::STARTING_TICK; 241 | self.digesting = 2; 242 | // Wipe board 243 | self.board.reset(); 244 | // Add offset snake 245 | self.head = console::Position { 246 | row: self.height / 4, 247 | col: self.width / 4, 248 | }; 249 | self.tail = self.head; 250 | self.board.store_body(self.head, self.direction); 251 | self.write_at(stdout, self.head, Some(Piece::Head)); 252 | // Add random food 253 | let pos = self.random_empty_position(); 254 | self.board.store_food(pos); 255 | self.write_at(stdout, pos, Some(Piece::Food)); 256 | 257 | 'game: loop { 258 | // Wait for frame tick 259 | neotron_sdk::delay(core::time::Duration::from_millis( 260 | self.tick_interval_ms as u64, 261 | )); 262 | 263 | // 1 point for not being dead 264 | self.score += 1; 265 | 266 | // Read input 267 | 'input: loop { 268 | let mut buffer = [0u8; 1]; 269 | if let Ok(1) = stdin.read(&mut buffer) { 270 | match buffer[0] { 271 | b'w' | b'W' => { 272 | // Going up 273 | if self.direction.is_horizontal() { 274 | self.direction = Direction::Up; 275 | } 276 | } 277 | b's' | b'S' => { 278 | // Going down 279 | if self.direction.is_horizontal() { 280 | self.direction = Direction::Down; 281 | } 282 | } 283 | b'a' | b'A' => { 284 | // Going left 285 | if self.direction.is_vertical() { 286 | self.direction = Direction::Left; 287 | } 288 | } 289 | b'd' | b'D' => { 290 | // Going right 291 | if self.direction.is_vertical() { 292 | self.direction = Direction::Right; 293 | } 294 | } 295 | b'q' | b'Q' => { 296 | // Quit game 297 | break 'game; 298 | } 299 | _ => { 300 | // ignore 301 | } 302 | } 303 | } else { 304 | break 'input; 305 | } 306 | } 307 | 308 | // Mark which way we're going in the old head position 309 | self.board.store_body(self.head, self.direction); 310 | self.write_at(stdout, self.head, Some(Piece::Body)); 311 | 312 | // Update head position 313 | match self.direction { 314 | Direction::Up => { 315 | if self.head.row == 0 { 316 | break 'game; 317 | } 318 | self.head.row -= 1; 319 | } 320 | Direction::Down => { 321 | if self.head.row == self.height - 1 { 322 | break 'game; 323 | } 324 | self.head.row += 1; 325 | } 326 | Direction::Left => { 327 | if self.head.col == 0 { 328 | break 'game; 329 | } 330 | self.head.col -= 1; 331 | } 332 | Direction::Right => { 333 | if self.head.col == self.width - 1 { 334 | break 'game; 335 | } 336 | self.head.col += 1; 337 | } 338 | } 339 | 340 | // Check what we just ate 341 | // - Food => get longer 342 | // - Ourselves => die 343 | if self.board.is_food(self.head) { 344 | // yum 345 | self.score += 10; 346 | self.digesting = 2; 347 | // Drop 10% on the tick interval 348 | self.tick_interval_ms *= 9; 349 | self.tick_interval_ms /= 10; 350 | if self.tick_interval_ms < 5 { 351 | // Maximum speed 352 | self.tick_interval_ms = 5; 353 | } 354 | // Add random food 355 | let pos = self.random_empty_position(); 356 | self.board.store_food(pos); 357 | self.write_at(stdout, pos, Some(Piece::Food)); 358 | } else if self.board.is_body(self.head) { 359 | // oh no 360 | break 'game; 361 | } 362 | 363 | // Write the new head 364 | self.board.store_body(self.head, self.direction); 365 | self.write_at(stdout, self.head, Some(Piece::Head)); 366 | 367 | if self.digesting == 0 { 368 | let old_tail = self.tail; 369 | match self.board.remove_piece(self.tail) { 370 | Some(Direction::Up) => { 371 | self.tail.row -= 1; 372 | } 373 | Some(Direction::Down) => { 374 | self.tail.row += 1; 375 | } 376 | Some(Direction::Left) => { 377 | self.tail.col -= 1; 378 | } 379 | Some(Direction::Right) => { 380 | self.tail.col += 1; 381 | } 382 | None => { 383 | panic!("Bad game state"); 384 | } 385 | } 386 | self.write_at(stdout, old_tail, None); 387 | } else { 388 | self.digesting -= 1; 389 | } 390 | } 391 | 392 | self.score 393 | } 394 | 395 | /// Draw a piece on the ANSI console at the given location 396 | fn write_at( 397 | &self, 398 | console: &mut neotron_sdk::File, 399 | position: console::Position, 400 | piece: Option, 401 | ) { 402 | let adjusted_position = console::Position { 403 | row: position.row + self.offset.row, 404 | col: position.col + self.offset.col, 405 | }; 406 | console::move_cursor(console, adjusted_position); 407 | if let Some(piece) = piece { 408 | let colour = piece.get_colour(); 409 | let ch = piece.get_char(); 410 | console::set_sgr(console, [colour]); 411 | let _ = console.write_char(ch); 412 | } else { 413 | let _ = console.write_char(' '); 414 | } 415 | } 416 | 417 | /// Find a spot on the board that is empty 418 | fn random_empty_position(&mut self) -> console::Position { 419 | loop { 420 | // This isn't equally distributed. I don't really care. 421 | let pos = console::Position { 422 | row: (neotron_sdk::rand() % self.height as u16) as u8, 423 | col: (neotron_sdk::rand() % self.width as u16) as u8, 424 | }; 425 | if self.board.is_empty(pos) { 426 | return pos; 427 | } 428 | } 429 | } 430 | } 431 | 432 | /// A direction in which a body piece can face 433 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 434 | enum Direction { 435 | /// Facing up 436 | Up, 437 | /// Facing down 438 | Down, 439 | /// Facing left 440 | Left, 441 | /// Facing right 442 | Right, 443 | } 444 | 445 | impl Direction { 446 | /// Is this left/right? 447 | fn is_horizontal(self) -> bool { 448 | self == Direction::Left || self == Direction::Right 449 | } 450 | 451 | /// Is this up/down? 452 | fn is_vertical(self) -> bool { 453 | self == Direction::Up || self == Direction::Down 454 | } 455 | } 456 | 457 | /// Something we can put on a board. 458 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 459 | #[repr(u8)] 460 | enum BoardPiece { 461 | /// Nothing here 462 | Empty, 463 | /// A body, and the next piece is up 464 | Up, 465 | /// A body, and the next piece is down 466 | Down, 467 | /// A body, and the next piece is left 468 | Left, 469 | /// A body, and the next piece is right 470 | Right, 471 | /// A piece of food 472 | Food, 473 | } 474 | 475 | /// Tracks where the snake is in 2D space. 476 | /// 477 | /// We do this rather than maintain a Vec of body positions and a Vec of food 478 | /// positions because it's fixed size and faster to see if a space is empty, or 479 | /// body, or food. 480 | struct Board { 481 | cells: [[BoardPiece; WIDTH]; HEIGHT], 482 | } 483 | 484 | impl Board { 485 | /// Make a new empty board 486 | const fn new() -> Board { 487 | Board { 488 | cells: [[BoardPiece::Empty; WIDTH]; HEIGHT], 489 | } 490 | } 491 | 492 | /// Clean up the board so everything is empty. 493 | fn reset(&mut self) { 494 | for y in 0..HEIGHT { 495 | for x in 0..WIDTH { 496 | self.cells[y][x] = BoardPiece::Empty; 497 | } 498 | } 499 | } 500 | 501 | /// Store a body piece on the board, based on which way it is facing 502 | fn store_body(&mut self, position: console::Position, direction: Direction) { 503 | self.cells[usize::from(position.row)][usize::from(position.col)] = match direction { 504 | Direction::Up => BoardPiece::Up, 505 | Direction::Down => BoardPiece::Down, 506 | Direction::Left => BoardPiece::Left, 507 | Direction::Right => BoardPiece::Right, 508 | } 509 | } 510 | 511 | /// Put some food on the board 512 | fn store_food(&mut self, position: console::Position) { 513 | self.cells[usize::from(position.row)][usize::from(position.col)] = BoardPiece::Food; 514 | } 515 | 516 | /// Is there food on the board here? 517 | fn is_food(&mut self, position: console::Position) -> bool { 518 | self.cells[usize::from(position.row)][usize::from(position.col)] == BoardPiece::Food 519 | } 520 | 521 | /// Is there body on the board here? 522 | fn is_body(&mut self, position: console::Position) -> bool { 523 | let cell = self.cells[usize::from(position.row)][usize::from(position.col)]; 524 | cell == BoardPiece::Up 525 | || cell == BoardPiece::Down 526 | || cell == BoardPiece::Left 527 | || cell == BoardPiece::Right 528 | } 529 | 530 | /// Is this position empty? 531 | fn is_empty(&mut self, position: console::Position) -> bool { 532 | self.cells[usize::from(position.row)][usize::from(position.col)] == BoardPiece::Empty 533 | } 534 | 535 | /// Remove a piece from the board 536 | fn remove_piece(&mut self, position: console::Position) -> Option { 537 | let old = match self.cells[usize::from(position.row)][usize::from(position.col)] { 538 | BoardPiece::Up => Some(Direction::Up), 539 | BoardPiece::Down => Some(Direction::Down), 540 | BoardPiece::Left => Some(Direction::Left), 541 | BoardPiece::Right => Some(Direction::Right), 542 | _ => None, 543 | }; 544 | self.cells[usize::from(position.row)][usize::from(position.col)] = BoardPiece::Empty; 545 | old 546 | } 547 | } 548 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "arrayvec" 7 | version = "0.7.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 10 | 11 | [[package]] 12 | name = "atomic-polyfill" 13 | version = "1.0.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" 16 | dependencies = [ 17 | "critical-section", 18 | ] 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.3.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 25 | 26 | [[package]] 27 | name = "bitflags" 28 | version = "1.3.2" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "2.5.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 37 | 38 | [[package]] 39 | name = "byteorder" 40 | version = "1.5.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "chrono" 52 | version = "0.4.38" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 55 | dependencies = [ 56 | "num-traits", 57 | ] 58 | 59 | [[package]] 60 | name = "cobs" 61 | version = "0.2.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" 64 | 65 | [[package]] 66 | name = "critical-section" 67 | version = "1.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" 70 | 71 | [[package]] 72 | name = "crossterm" 73 | version = "0.26.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" 76 | dependencies = [ 77 | "bitflags 1.3.2", 78 | "crossterm_winapi", 79 | "libc", 80 | "mio", 81 | "parking_lot", 82 | "signal-hook", 83 | "signal-hook-mio", 84 | "winapi", 85 | ] 86 | 87 | [[package]] 88 | name = "crossterm_winapi" 89 | version = "0.9.1" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 92 | dependencies = [ 93 | "winapi", 94 | ] 95 | 96 | [[package]] 97 | name = "embedded-hal" 98 | version = "1.0.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" 101 | 102 | [[package]] 103 | name = "embedded-sdmmc" 104 | version = "0.7.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "da528dbf3f1c1f0b321552bc334d04799bb17c1936de55bccfb643a4f39300d8" 107 | dependencies = [ 108 | "byteorder", 109 | "embedded-hal", 110 | "heapless", 111 | ] 112 | 113 | [[package]] 114 | name = "flames" 115 | version = "0.1.0" 116 | dependencies = [ 117 | "neotron-sdk", 118 | ] 119 | 120 | [[package]] 121 | name = "grounded" 122 | version = "0.2.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "917d82402c7eb9755fdd87d52117701dae9e413a6abb309fac2a13af693b6080" 125 | dependencies = [ 126 | "portable-atomic", 127 | ] 128 | 129 | [[package]] 130 | name = "hash32" 131 | version = "0.2.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 134 | dependencies = [ 135 | "byteorder", 136 | ] 137 | 138 | [[package]] 139 | name = "heapless" 140 | version = "0.7.17" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 143 | dependencies = [ 144 | "atomic-polyfill", 145 | "hash32", 146 | "rustc_version", 147 | "serde", 148 | "spin", 149 | "stable_deref_trait", 150 | ] 151 | 152 | [[package]] 153 | name = "libc" 154 | version = "0.2.155" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 157 | 158 | [[package]] 159 | name = "lock_api" 160 | version = "0.4.12" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 163 | dependencies = [ 164 | "autocfg", 165 | "scopeguard", 166 | ] 167 | 168 | [[package]] 169 | name = "log" 170 | version = "0.4.21" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 173 | 174 | [[package]] 175 | name = "menu" 176 | version = "0.3.2" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "b03d7f798bfe97329ad6df937951142eec93886b37d87010502dd25e8cc75fd5" 179 | 180 | [[package]] 181 | name = "mio" 182 | version = "0.8.11" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 185 | dependencies = [ 186 | "libc", 187 | "log", 188 | "wasi", 189 | "windows-sys", 190 | ] 191 | 192 | [[package]] 193 | name = "neoplay" 194 | version = "0.1.0" 195 | dependencies = [ 196 | "grounded", 197 | "neotracker", 198 | "neotron-sdk", 199 | ] 200 | 201 | [[package]] 202 | name = "neotracker" 203 | version = "0.1.0" 204 | source = "git+https://github.com/thejpster/neotracker.git?rev=2ee7a85006a9461b876bdf47e45b6105437a38f6#2ee7a85006a9461b876bdf47e45b6105437a38f6" 205 | 206 | [[package]] 207 | name = "neotron-api" 208 | version = "0.1.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "82c00f842e7006421002e67a53866b90ddd6f7d86137b52d2147f5e38b35e82f" 211 | dependencies = [ 212 | "bitflags 2.5.0", 213 | "neotron-ffi", 214 | ] 215 | 216 | [[package]] 217 | name = "neotron-api" 218 | version = "0.2.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "67d6c96706b6f3ec069abfb042cadfd2d701980fa4940f407c0bc28ee1e1c493" 221 | dependencies = [ 222 | "bitflags 2.5.0", 223 | "neotron-ffi", 224 | ] 225 | 226 | [[package]] 227 | name = "neotron-common-bios" 228 | version = "0.12.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "24fdb319a4bdecd68d9917e5cc026a2b50d32572649880febb1f993a0bf9ad00" 231 | dependencies = [ 232 | "chrono", 233 | "neotron-ffi", 234 | "pc-keyboard", 235 | ] 236 | 237 | [[package]] 238 | name = "neotron-ffi" 239 | version = "0.1.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "d37886e73d87732421aaf5da617eead9d69a7daf6b0d059780f76157d9ce5372" 242 | 243 | [[package]] 244 | name = "neotron-loader" 245 | version = "0.1.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "b9b8634a088b9d5b338a96b3f6ef45a3bc0b9c0f0d562c7d00e498265fd96e8f" 248 | 249 | [[package]] 250 | name = "neotron-os" 251 | version = "0.8.1" 252 | dependencies = [ 253 | "chrono", 254 | "embedded-sdmmc", 255 | "heapless", 256 | "menu", 257 | "neotron-api 0.2.0", 258 | "neotron-common-bios", 259 | "neotron-loader", 260 | "neotron-romfs", 261 | "pc-keyboard", 262 | "postcard", 263 | "r0", 264 | "serde", 265 | "vte", 266 | ] 267 | 268 | [[package]] 269 | name = "neotron-romfs" 270 | version = "1.0.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "5758cc14dd6e15def81e3a12d36a35b2779ac568e30b96501cdaba2571c4b1e2" 273 | dependencies = [ 274 | "neotron-api 0.1.0", 275 | ] 276 | 277 | [[package]] 278 | name = "neotron-sdk" 279 | version = "0.2.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "2043d6202942b6ebecbf02c95a0d1498bade2ffb287355af02fee6a5ab5de331" 282 | dependencies = [ 283 | "crossterm", 284 | "neotron-api 0.2.0", 285 | "neotron-ffi", 286 | ] 287 | 288 | [[package]] 289 | name = "num-traits" 290 | version = "0.2.19" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 293 | dependencies = [ 294 | "autocfg", 295 | ] 296 | 297 | [[package]] 298 | name = "parking_lot" 299 | version = "0.12.3" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 302 | dependencies = [ 303 | "lock_api", 304 | "parking_lot_core", 305 | ] 306 | 307 | [[package]] 308 | name = "parking_lot_core" 309 | version = "0.9.10" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 312 | dependencies = [ 313 | "cfg-if", 314 | "libc", 315 | "redox_syscall", 316 | "smallvec", 317 | "windows-targets 0.52.5", 318 | ] 319 | 320 | [[package]] 321 | name = "pc-keyboard" 322 | version = "0.7.0" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "ed089a1fbffe3337a1a345501c981f1eb1e47e69de5a40e852433e12953c3174" 325 | 326 | [[package]] 327 | name = "portable-atomic" 328 | version = "1.10.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" 331 | dependencies = [ 332 | "critical-section", 333 | ] 334 | 335 | [[package]] 336 | name = "postcard" 337 | version = "1.0.8" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" 340 | dependencies = [ 341 | "cobs", 342 | "heapless", 343 | "serde", 344 | ] 345 | 346 | [[package]] 347 | name = "proc-macro2" 348 | version = "1.0.85" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" 351 | dependencies = [ 352 | "unicode-ident", 353 | ] 354 | 355 | [[package]] 356 | name = "quote" 357 | version = "1.0.36" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 360 | dependencies = [ 361 | "proc-macro2", 362 | ] 363 | 364 | [[package]] 365 | name = "r0" 366 | version = "1.0.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "bd7a31eed1591dcbc95d92ad7161908e72f4677f8fabf2a32ca49b4237cbf211" 369 | 370 | [[package]] 371 | name = "redox_syscall" 372 | version = "0.5.1" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 375 | dependencies = [ 376 | "bitflags 2.5.0", 377 | ] 378 | 379 | [[package]] 380 | name = "rustc_version" 381 | version = "0.4.0" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 384 | dependencies = [ 385 | "semver", 386 | ] 387 | 388 | [[package]] 389 | name = "scopeguard" 390 | version = "1.2.0" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 393 | 394 | [[package]] 395 | name = "semver" 396 | version = "1.0.23" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 399 | 400 | [[package]] 401 | name = "serde" 402 | version = "1.0.203" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" 405 | dependencies = [ 406 | "serde_derive", 407 | ] 408 | 409 | [[package]] 410 | name = "serde_derive" 411 | version = "1.0.203" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" 414 | dependencies = [ 415 | "proc-macro2", 416 | "quote", 417 | "syn", 418 | ] 419 | 420 | [[package]] 421 | name = "signal-hook" 422 | version = "0.3.17" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 425 | dependencies = [ 426 | "libc", 427 | "signal-hook-registry", 428 | ] 429 | 430 | [[package]] 431 | name = "signal-hook-mio" 432 | version = "0.2.3" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 435 | dependencies = [ 436 | "libc", 437 | "mio", 438 | "signal-hook", 439 | ] 440 | 441 | [[package]] 442 | name = "signal-hook-registry" 443 | version = "1.4.2" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 446 | dependencies = [ 447 | "libc", 448 | ] 449 | 450 | [[package]] 451 | name = "smallvec" 452 | version = "1.13.2" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 455 | 456 | [[package]] 457 | name = "snake" 458 | version = "0.1.0" 459 | dependencies = [ 460 | "neotron-sdk", 461 | ] 462 | 463 | [[package]] 464 | name = "spin" 465 | version = "0.9.8" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 468 | dependencies = [ 469 | "lock_api", 470 | ] 471 | 472 | [[package]] 473 | name = "stable_deref_trait" 474 | version = "1.2.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 477 | 478 | [[package]] 479 | name = "syn" 480 | version = "2.0.66" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" 483 | dependencies = [ 484 | "proc-macro2", 485 | "quote", 486 | "unicode-ident", 487 | ] 488 | 489 | [[package]] 490 | name = "unicode-ident" 491 | version = "1.0.12" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 494 | 495 | [[package]] 496 | name = "utf8parse" 497 | version = "0.2.1" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 500 | 501 | [[package]] 502 | name = "vte" 503 | version = "0.12.1" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "98b0a06c0f086f7abe70cf308967153479e223b6a9809f7dcc6c47b045574bc9" 506 | dependencies = [ 507 | "arrayvec", 508 | "utf8parse", 509 | "vte_generate_state_changes", 510 | ] 511 | 512 | [[package]] 513 | name = "vte_generate_state_changes" 514 | version = "0.1.1" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" 517 | dependencies = [ 518 | "proc-macro2", 519 | "quote", 520 | ] 521 | 522 | [[package]] 523 | name = "wasi" 524 | version = "0.11.0+wasi-snapshot-preview1" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 527 | 528 | [[package]] 529 | name = "winapi" 530 | version = "0.3.9" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 533 | dependencies = [ 534 | "winapi-i686-pc-windows-gnu", 535 | "winapi-x86_64-pc-windows-gnu", 536 | ] 537 | 538 | [[package]] 539 | name = "winapi-i686-pc-windows-gnu" 540 | version = "0.4.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 543 | 544 | [[package]] 545 | name = "winapi-x86_64-pc-windows-gnu" 546 | version = "0.4.0" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 549 | 550 | [[package]] 551 | name = "windows-sys" 552 | version = "0.48.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 555 | dependencies = [ 556 | "windows-targets 0.48.5", 557 | ] 558 | 559 | [[package]] 560 | name = "windows-targets" 561 | version = "0.48.5" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 564 | dependencies = [ 565 | "windows_aarch64_gnullvm 0.48.5", 566 | "windows_aarch64_msvc 0.48.5", 567 | "windows_i686_gnu 0.48.5", 568 | "windows_i686_msvc 0.48.5", 569 | "windows_x86_64_gnu 0.48.5", 570 | "windows_x86_64_gnullvm 0.48.5", 571 | "windows_x86_64_msvc 0.48.5", 572 | ] 573 | 574 | [[package]] 575 | name = "windows-targets" 576 | version = "0.52.5" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 579 | dependencies = [ 580 | "windows_aarch64_gnullvm 0.52.5", 581 | "windows_aarch64_msvc 0.52.5", 582 | "windows_i686_gnu 0.52.5", 583 | "windows_i686_gnullvm", 584 | "windows_i686_msvc 0.52.5", 585 | "windows_x86_64_gnu 0.52.5", 586 | "windows_x86_64_gnullvm 0.52.5", 587 | "windows_x86_64_msvc 0.52.5", 588 | ] 589 | 590 | [[package]] 591 | name = "windows_aarch64_gnullvm" 592 | version = "0.48.5" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 595 | 596 | [[package]] 597 | name = "windows_aarch64_gnullvm" 598 | version = "0.52.5" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 601 | 602 | [[package]] 603 | name = "windows_aarch64_msvc" 604 | version = "0.48.5" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 607 | 608 | [[package]] 609 | name = "windows_aarch64_msvc" 610 | version = "0.52.5" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 613 | 614 | [[package]] 615 | name = "windows_i686_gnu" 616 | version = "0.48.5" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 619 | 620 | [[package]] 621 | name = "windows_i686_gnu" 622 | version = "0.52.5" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 625 | 626 | [[package]] 627 | name = "windows_i686_gnullvm" 628 | version = "0.52.5" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 631 | 632 | [[package]] 633 | name = "windows_i686_msvc" 634 | version = "0.48.5" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 637 | 638 | [[package]] 639 | name = "windows_i686_msvc" 640 | version = "0.52.5" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 643 | 644 | [[package]] 645 | name = "windows_x86_64_gnu" 646 | version = "0.48.5" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 649 | 650 | [[package]] 651 | name = "windows_x86_64_gnu" 652 | version = "0.52.5" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 655 | 656 | [[package]] 657 | name = "windows_x86_64_gnullvm" 658 | version = "0.48.5" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 661 | 662 | [[package]] 663 | name = "windows_x86_64_gnullvm" 664 | version = "0.52.5" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 667 | 668 | [[package]] 669 | name = "windows_x86_64_msvc" 670 | version = "0.48.5" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 673 | 674 | [[package]] 675 | name = "windows_x86_64_msvc" 676 | version = "0.52.5" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 679 | --------------------------------------------------------------------------------