Skip to content

Commit

Permalink
Merge pull request #1 from sammypanda/dev
Browse files Browse the repository at this point in the history
lUSB-cli 1.0.0
  • Loading branch information
sammypanda authored Aug 27, 2023
2 parents 0abe1cd + 3bd74f0 commit ac1a36d
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk


# Added by cargo

/target
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "lusb-cli"
description = "linUxSwitchBoard (command-line interface): Virtual switchboard for your USB devices"
authors = ["sammypanda"]
version = "0.1.0"
edition = "2021"

[dependencies]
clap = "4.3.10"
rusb = "0.9"
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
# lUSB-cli
linUxSwitchBoard (command-line interface): Virtual switchboard for your USB devices
<h1 align="center">lUSB-cli</h1>
<p align="center">linUxSwitchBoard (command-line interface): Virtual switchboard for your USB devices</p>
<br>
<h2>Usage</h2>

```shell
lUSB-cli [verb] [device ids]
```
```
verbs:
- list
- disable
- enable
```
9 changes: 9 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
with (import <nixpkgs> {});

mkShell {
buildInputs = [
pkg-config
libusb
];
}

96 changes: 96 additions & 0 deletions src/cli/cli_devices.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
extern crate rusb;
use super::cli_devices_list;
use std::io::{Error, ErrorKind};
use rusb::{Context, UsbContext, DeviceHandle};

pub struct Device {
index: u8
}

impl Device {
pub fn new(index: u8) -> Self {
Self {
index
}
}

pub fn get_index(&self) -> Result<u8, Error> {
Ok(self.index)
}

pub fn get_device_handle(&self) -> Result<DeviceHandle<Context>, Error> {
let index = self.index;
let context = Context::new().unwrap();
let devices = context.devices().unwrap();

let device = match devices.iter().nth(index as usize) { // handle <Option>
Some(device) => device, // pass on the variable
None => { // error case
return Err(Error::new(
ErrorKind::NotFound,
format!("USB with the '{}' identifier was not found", index)
));
}
};

let device_handle = match device.open() { // handle <Result>
Ok(device_handle) => device_handle, // pass on the variable
Err(_) => { // error case
return Err(Error::new(
ErrorKind::Unsupported,
format!("USB device not openable: identifier '{}'", index)
));
}
};

// return device_handle as Result
Ok(device_handle)
}
}

pub fn list() {
cli_devices_list::demo();
}

pub fn handle_verb(verb: &str, device: &Device) {
match device.get_device_handle() {
Ok(mut device_handle) => {
println!("Device: {:?}", device.get_index().unwrap());
println!(
"device_handle: {}",
device_handle
.device()
.device_descriptor()
.unwrap()
.product_id()
);

match verb {
"enable" => {
interface_loop(&mut device_handle, true)
}
"disable" => {
interface_loop(&mut device_handle, false)
}
_ => {
panic!("Invalid verb, use disable or enable");
}
}
},
Err(error) => {
eprintln!("{error}");
}
};
}

fn interface_loop<T: UsbContext>(device_handle: &mut DeviceHandle<T>, enable: bool) {
device_handle.device().active_config_descriptor().unwrap().interfaces().enumerate().for_each(|(index, interface)| {
let index = index as u8;

if enable {
device_handle.attach_kernel_driver(index).unwrap_or_else(|error| println!("{error}"));
} else {
device_handle.detach_kernel_driver(index).unwrap_or_else(|error| println!("{error}"));
}
});
}
57 changes: 57 additions & 0 deletions src/cli/cli_devices_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
extern crate rusb;
use std::time::Duration;

const ID_ENGLISH: u16 = 1033; // the language code for US English

//
// translate devices from <Result> and iterate over
//
pub fn demo() {
let mut index = 0;

for device in rusb::devices().unwrap().iter() {
let device_desc = device.device_descriptor().unwrap(); // translate the device description from <Result>
let device_handle; // allows us to access extra deets
let device_string;

// open device for handle with consideration that it might be empty
match device.open() {
Ok(result) => device_handle = result,
Err(error) => {
println!("\u{274C} - Bus {}, Device {}: {}", // 2A2F is a ⨯ (cross product) unicode symbol
device.bus_number(),
device.address(),
error
);
continue;
}
}

let device_languages = device_handle.read_languages( // dependency for reading string descriptors
Duration::new(30, 0) // timeout after 30 seconds, 0 nanoseconds
).unwrap();

device_string = device_languages.iter() // convert Vec to Iter to check if our language exists in
.find(|language| language.lang_id() == ID_ENGLISH) // pass `language` parameter to represent `Some` value and get the `Language` id
.and_then(|language| { // operation to be performed on `Some` value returned by `find()`
device_handle.read_product_string(language.clone(), &device_desc, Duration::new(30, 0)) // get the product string
.map_err(|error| error.to_string()) // if there is an error
.ok() // convert `Result` into `Option<String>`
})
.unwrap_or_else(|| { // unwrapping `Option<String>` but with an `else` for...
// handling the case when the desired language is not found
String::from("Language not found")
});

println!("{} - Bus {} Device {} ID {}:{} ({})",
index,
device.bus_number(),
device.address(),
device_desc.vendor_id(),
device_desc.product_id(),
device_string
);

index += 1;
}
}
4 changes: 4 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod cli_devices;
pub mod cli_devices_list;

// super keyword is used in this codebase to refer up a layer
53 changes: 53 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use clap::{Command, Arg};

mod cli;
use cli::cli_devices; // should basically be the proxy for every other *_devices_* module

// responsible for parsing command inputs and delegating tasks
fn main() {
let identifiers_arg: Arg = Arg::new("identifiers")
.value_name("IDENTIFIERS")
.required(true)
.value_delimiter(',')
.value_parser(clap::value_parser!(u8))
.help("Comma-separated list of identifiers");

let cmd = Command::new(env!("CARGO_PKG_NAME"))
.arg_required_else_help(true)
.version(env!("CARGO_PKG_VERSION", "Version not set"))
.author(env!("CARGO_PKG_AUTHORS"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.subcommand(
Command::new("list")
.about("List all recognised USB devices"))
.subcommand(
Command::new("disable")
.about("Disable the specified USB devices")
.arg(
&identifiers_arg
)
)
.subcommand(
Command::new("enable")
.about("Enable the specified USB devices")
.arg(
&identifiers_arg
)
)
.get_matches();

match cmd.subcommand() {
Some(("list", _)) => {
cli_devices::list();
},
Some((verb, sub_m)) => {
let identifiers = sub_m.get_many::<u8>("identifiers") // we can be sure it exists since `clap` handles parsing
.unwrap_or_else(|| panic!("Comma-separted identifiers not found")); // ..but just in case

for device in identifiers {
cli_devices::handle_verb(verb, &cli::cli_devices::Device::new(*device))
};
},
_ => {} // required by 'match'
};
}

0 comments on commit ac1a36d

Please sign in to comment.