diff --git a/.gitignore b/.gitignore index d01bd1a..e741ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,9 @@ Cargo.lock # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ + + +*.mp3 +*.flac +*.wav \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8a6b8e0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "marlinbox-rs" +version = "0.1.0" +edition = "2021" + +[dependencies] +rand = "0.8.5" +rodio = "0.19.0" +rusb = "0.9.4" +serde = { version = "1.0.210", features = ["derive", "rc"] } +serde_json = "1.0.132" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7dd7844 --- /dev/null +++ b/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1729265718, + "narHash": "sha256-4HQI+6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ccc0c2126893dd20963580b6478d1a10a4512185", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1728538411, + "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1729477859, + "narHash": "sha256-r0VyeJxy4O4CgTB/PNtfQft9fPfN1VuGvnZiCxDArvg=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "ada8266712449c4c0e6ee6fcbc442b3c217c79e1", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a4eb26d --- /dev/null +++ b/flake.nix @@ -0,0 +1,57 @@ +{ + description = "Example Rust development environment for Zero to Nix"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" "clippy" "rustfmt" ]; + targets = [ "x86_64-unknown-linux-gnu" "wasm32-unknown-unknown" ]; + }; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + rustToolchain + trunk + clippy + # tailwindcss + + # dioxus-cli + # wasm-bindgen-cli + + # cargo-shuttle + cargo-edit + cargo-binstall + bacon + + alsa-lib.dev + + openssl + pkg-config + ]; + + shellHook = '' + export PATH=${rustToolchain}/bin:$PATH + export RUSTC_VERSION=$(rustc --version) + export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library" + export OPENSSL_DIR="${pkgs.openssl.dev}" + export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib" + export OPENSSL_INCLUDE_DIR="${pkgs.openssl.dev}/include" + ''; + + packages = pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [ libiconv ]); + }; + } + ); +} diff --git a/music.json b/music.json new file mode 100644 index 0000000..942fdba --- /dev/null +++ b/music.json @@ -0,0 +1,11 @@ +{ + "music": { + "27271E24211E1E26": { + "Play": "02 - Disturbed - Immortalized.mp3" + }, + "27271E242121221F": "Shuffle", + "27271E2420222321": { + "Play": "01-13 33 RPM.flac" + } + } +} \ No newline at end of file diff --git a/src/card.rs b/src/card.rs new file mode 100644 index 0000000..da80ae5 --- /dev/null +++ b/src/card.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum Card { + Play(Arc), + Pause, + Resume, + Next, + Previous, + Shuffle, +} + +impl From<&Card> for Option { + fn from(card: &Card) -> Self { + Some(card.clone()) + } +} + +impl> From for Card { + fn from(music_file: T) -> Self { + Self::Play(Arc::from(music_file.as_ref())) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..97169ed --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod card; +pub mod library; +pub mod portal; diff --git a/src/library.rs b/src/library.rs new file mode 100644 index 0000000..aab6148 --- /dev/null +++ b/src/library.rs @@ -0,0 +1,47 @@ +use std::{collections::HashMap, sync::Arc}; + +use rand::seq::SliceRandom; +use serde::{Deserialize, Serialize}; + +use crate::card::Card; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Library { + music: HashMap, Option>, +} + +impl Library { + #[must_use] + pub fn new() -> Self { + Self { + music: HashMap::new(), + } + } + + pub fn add(&mut self, card_id: &str) { + self.music.insert(card_id.into(), None); + } + + pub fn update(&mut self, card_id: &str, music_file: Option) { + self.music + .insert(card_id.into(), music_file.map(Into::into)); + } + + #[must_use] + pub fn get(&self, card_id: &str) -> Option<&Card> { + self.music.get::(card_id).unwrap_or(&None).as_ref() + } + + #[must_use] + pub fn get_random(&self) -> Option<&Card> { + let play_cards: Vec<&Card> = self.music.values().flatten().collect(); + + play_cards.choose(&mut rand::thread_rng()).copied() + } +} + +impl Default for Library { + fn default() -> Self { + Self::new() + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4dda953 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,111 @@ +use marlinbox_rs::{card::Card, library::Library}; +use rodio::{Decoder, OutputStream, Sink}; +use rusb::{Context, DeviceHandle, UsbContext}; +use std::{fs::File, io::BufReader, time::Duration}; + +const VID: u16 = 0xffff; // Replace with your device's Vendor ID +const PID: u16 = 0x0035; // Replace with your device's Product ID + +fn extract_card_id(buf: &[u8]) -> Option { + let significant_indices = [2, 18, 34, 50, 66, 82, 98, 114]; + let extracted: Vec = significant_indices + .iter() + .filter_map(|&i| buf.get(i).copied()) + .collect(); + + if extracted.len() == 8 && extracted.iter().all(|&b| b != 0) { + Some(extracted.iter().fold(String::new(), |mut acc, &b| { + acc.push_str(&format!("{b:02X}")); + acc + })) + } else { + None + } +} + +fn main() -> Result<(), Box> { + let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + + let sink = Sink::try_new(&stream_handle).unwrap(); + + let music: Library = serde_json::from_reader(File::open("music.json")?)?; + // let mut music = Library::new(); + // music.update( + // "27271E24211E1E26", + // Some(Card::Play("02 - Disturbed - Immortalized.mp3".into())), + // ); + // music.update( + // "27271E242121221F", + // Some(Card::Play( + // "11 - Disturbed - The Sound Of Silence.mp3".into(), + // )), + // ); + // music.update("27271E2420222321", Card::Shuffle.into()); + + // let music_json = serde_json::to_string_pretty(&music)?; + // fs::write("music.json", music_json)?; + + let context = Context::new()?; + let handle: DeviceHandle = context + .open_device_with_vid_pid(VID, PID) + .ok_or("Device not found")?; + + #[cfg(target_os = "linux")] + { + if handle.kernel_driver_active(0)? { + handle.detach_kernel_driver(0)?; + } + } + + handle.claim_interface(0)?; + + println!("Starting to read RFID cards..."); + + let mut buf = [0u8; 120]; + // let mut last_id: Option = None; + + loop { + match handle.read_interrupt(0x81, &mut buf, Duration::from_millis(1000)) { + Ok(len) => { + if len == 120 { + if let Some(card_id) = extract_card_id(&buf) { + // if last_id.as_ref() != Some(&card_id) { + println!("Card ID: {card_id}"); + if let Some(music_file) = music.get(card_id.as_ref()) { + println!("Playing: {music_file:?}"); + + match music_file { + Card::Play(music_file) => { + sink.stop(); + let file = + BufReader::new(File::open(music_file.as_ref()).unwrap()); + let source = Decoder::new(file).unwrap(); + sink.append(source); + } + Card::Pause => sink.pause(), + Card::Resume => sink.play(), + Card::Next | Card::Previous => sink.stop(), + Card::Shuffle => { + if let Some(Card::Play(music_file)) = music.get_random() { + sink.stop(); + let file = BufReader::new( + File::open(music_file.as_ref()).unwrap(), + ); + let source = Decoder::new(file).unwrap(); + sink.append(source); + } + } + } + } else { + println!("No music file found for this card"); + } + } + } + } + Err(rusb::Error::Timeout) => { + // last_id = None; + } + Err(e) => eprintln!("Error: {e:?}"), + } + } +} diff --git a/src/portal/mod.rs b/src/portal/mod.rs new file mode 100644 index 0000000..74f47ad --- /dev/null +++ b/src/portal/mod.rs @@ -0,0 +1 @@ +pub mod server; diff --git a/src/portal/server.rs b/src/portal/server.rs new file mode 100644 index 0000000..e69de29