Redstone device
This commit is contained in:
57
README.md
57
README.md
@@ -21,7 +21,7 @@ the console is switched into raw mode before use.
|
||||
Add the crate to your workspace and interact with it directly:
|
||||
|
||||
```rust
|
||||
use oc2r_rust::{DeviceBus, JsonValue, Result, DEFAULT_DEVICE_PATH};
|
||||
use oc2r_rust::{DeviceBus, Result, DEFAULT_DEVICE_PATH};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut bus = DeviceBus::connect(DEFAULT_DEVICE_PATH)?;
|
||||
@@ -33,7 +33,7 @@ fn main() -> Result<()> {
|
||||
|
||||
// Invoke a method on the first device as an example.
|
||||
if let Some(mut device) = bus.device("some-device-id")? {
|
||||
let response = device.invoke("help", &[JsonValue::Null])?;
|
||||
let response = device.call("help", ())?;
|
||||
println!("help(): {response:?}");
|
||||
}
|
||||
|
||||
@@ -41,22 +41,63 @@ fn main() -> Result<()> {
|
||||
}
|
||||
```
|
||||
|
||||
### Running the examples
|
||||
### Dynamic method calls
|
||||
|
||||
`Device::call` (and the matching `DeviceBus::call`) accept anything that
|
||||
implements [`IntoJsonArgs`](https://docs.rs/oc2r-rust/latest/oc2r_rust/trait.IntoJsonArgs.html),
|
||||
so you can stay close to the Lua ergonomics:
|
||||
|
||||
```rust
|
||||
device.call("setRedstoneOutput", ("east", 15_u8))?;
|
||||
device.call("getRedstoneInput", ("north",))?;
|
||||
```
|
||||
|
||||
### Typed helpers
|
||||
|
||||
For common peripherals you can rely on typed wrappers. The redstone helper
|
||||
mirrors the behaviour of `redstone.lua` and performs basic clamping/parsing:
|
||||
|
||||
```rust
|
||||
use oc2r_rust::devices::redstone::{Redstone, Side};
|
||||
use oc2r_rust::{DeviceBus, Result, DEFAULT_DEVICE_PATH};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut bus = DeviceBus::connect(DEFAULT_DEVICE_PATH)?;
|
||||
let mut redstone = Redstone::attach(&mut bus)?
|
||||
.expect("no redstone interface attached");
|
||||
|
||||
redstone.set_output_state(Side::East, true)?;
|
||||
println!("in: {}", redstone.input(Side::East)?);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Building the examples
|
||||
|
||||
Two runnable examples live in `examples/`:
|
||||
|
||||
- `list-devices` – enumerate devices and pretty-print their metadata
|
||||
- `invoke-method` – list available methods for the first device and invoke one
|
||||
- `redstone` – toggle/query a redstone interface, mirroring `redstone.lua`
|
||||
|
||||
Run them with:
|
||||
The OC2R VM does not ship a Rust toolchain, so build the binaries on your host
|
||||
and copy them into the VM:
|
||||
|
||||
```bash
|
||||
cargo run --example list-devices
|
||||
cargo run --example invoke-method
|
||||
# once
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
# build release binaries
|
||||
cross build --release --example list-devices
|
||||
cross build --release --example invoke-method
|
||||
cross build --release --example redstone
|
||||
```
|
||||
|
||||
Both examples expect to run inside the OC2R VM or a compatible environment
|
||||
where `/dev/hvc0` is exposed.
|
||||
The resulting executables live under
|
||||
`target/<triple>/release/examples/`. Copy them into Minux using your preferred
|
||||
workflow—an Import/Export card in Minecraft works well if you drop the binary
|
||||
onto the card outside the VM. Once the file is on disk (e.g. `/usr/bin/`), you
|
||||
can execute it from the shell.
|
||||
|
||||
## Architecture overview
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Build with: cross build --release --example invoke-method
|
||||
|
||||
use oc2r_rust::{DEFAULT_DEVICE_PATH, DeviceBus, Result};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
@@ -22,20 +24,14 @@ fn main() -> Result<()> {
|
||||
|
||||
if let Some(method) = methods.iter().find(|m| m.parameters.is_empty()) {
|
||||
println!("Invoking {}()", method.name);
|
||||
let result = device.invoke(&method.name, &[])?;
|
||||
let result = device.call(&method.name, ())?;
|
||||
println!("Result: {result:?}");
|
||||
} else {
|
||||
println!("No zero-argument methods available to invoke.");
|
||||
}
|
||||
|
||||
// Example for passing parameters:
|
||||
// device.invoke(
|
||||
// "setOutput",
|
||||
// &[
|
||||
// oc2r_rust::JsonValue::Number(1.into()),
|
||||
// oc2r_rust::JsonValue::Bool(true),
|
||||
// ],
|
||||
// )?;
|
||||
// device.call("setRedstoneOutput", ("east", 15_u8))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Build with: cross build --release --example list-devices
|
||||
|
||||
use oc2r_rust::{DEFAULT_DEVICE_PATH, DeviceBus, Result};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
|
||||
62
examples/redstone.rs
Normal file
62
examples/redstone.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
// Build with: cross build --release --example redstone
|
||||
|
||||
use oc2r_rust::devices::redstone::{Redstone, Side};
|
||||
use oc2r_rust::{DEFAULT_DEVICE_PATH, DeviceBus, Result};
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = env::args().skip(1);
|
||||
let Some(side_arg) = args.next() else {
|
||||
print_usage();
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut bus = DeviceBus::connect(DEFAULT_DEVICE_PATH)?;
|
||||
let mut redstone = match Redstone::attach(&mut bus)? {
|
||||
Some(device) => device,
|
||||
None => {
|
||||
eprintln!("No redstone interface found on this bus.");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let side = match Side::from_str(&side_arg) {
|
||||
Ok(side) => side,
|
||||
Err(_) => {
|
||||
eprintln!("Invalid side: {side_arg}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(value) = args.next() {
|
||||
let level = parse_level(&value);
|
||||
redstone.set_output_level(side, level)?;
|
||||
}
|
||||
|
||||
let input = redstone.input(side)?;
|
||||
let output = redstone.output(side)?;
|
||||
println!("in: {}", input.ceil());
|
||||
println!("out: {}", output.ceil());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_level(value: &str) -> u8 {
|
||||
let trimmed = value.trim();
|
||||
if let Ok(num) = trimmed.parse::<u8>() {
|
||||
return num.min(15);
|
||||
}
|
||||
|
||||
match trimmed.to_ascii_lowercase().as_str() {
|
||||
"true" | "on" | "yes" => 15,
|
||||
"false" | "off" | "no" => 0,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!("Usage: redstone <side> [value]");
|
||||
eprintln!(" side : up|down|north|south|west|east|front|back|left|right");
|
||||
eprintln!(" value : 0-15, true/on/yes, false/off/no");
|
||||
}
|
||||
27
src/bus.rs
27
src/bus.rs
@@ -1,6 +1,7 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::rpc::{DeviceEntry, JsonValue, MethodEntry, RpcRequest, RpcResponse};
|
||||
use crate::transport::{flush_fd, read_message, set_raw, write_message};
|
||||
use crate::value::IntoJsonArgs;
|
||||
use std::fs::OpenOptions;
|
||||
use std::os::fd::{AsRawFd, RawFd};
|
||||
use std::path::Path;
|
||||
@@ -65,6 +66,24 @@ impl DeviceBus {
|
||||
device_id: &str,
|
||||
name: &str,
|
||||
parameters: &[JsonValue],
|
||||
) -> Result<JsonValue> {
|
||||
self.invoke_raw(device_id, name, parameters)
|
||||
}
|
||||
|
||||
/// Invoke a method with arguments that implement [`IntoJsonArgs`].
|
||||
pub fn call<P>(&mut self, device_id: &str, name: &str, params: P) -> Result<JsonValue>
|
||||
where
|
||||
P: IntoJsonArgs,
|
||||
{
|
||||
let args = params.into_args();
|
||||
self.invoke_raw(device_id, name, &args)
|
||||
}
|
||||
|
||||
fn invoke_raw(
|
||||
&mut self,
|
||||
device_id: &str,
|
||||
name: &str,
|
||||
parameters: &[JsonValue],
|
||||
) -> Result<JsonValue> {
|
||||
self.flush()?;
|
||||
let request = RpcRequest::Invoke {
|
||||
@@ -145,4 +164,12 @@ impl<'bus> Device<'bus> {
|
||||
pub fn invoke(&mut self, name: &str, parameters: &[JsonValue]) -> Result<JsonValue> {
|
||||
self.bus.invoke(&self.entry.device_id, name, parameters)
|
||||
}
|
||||
|
||||
/// Invoke `name` with arguments converted via [`IntoJsonArgs`].
|
||||
pub fn call<P>(&mut self, name: &str, params: P) -> Result<JsonValue>
|
||||
where
|
||||
P: IntoJsonArgs,
|
||||
{
|
||||
self.bus.call(&self.entry.device_id, name, params)
|
||||
}
|
||||
}
|
||||
|
||||
1
src/devices/mod.rs
Normal file
1
src/devices/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod redstone;
|
||||
144
src/devices/redstone.rs
Normal file
144
src/devices/redstone.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use crate::bus::Device;
|
||||
use crate::{DeviceBus, DeviceEntry, Error, JsonValue, Result};
|
||||
use miniserde::json::Number;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Typed wrapper around the HLAPI redstone interface.
|
||||
pub struct Redstone<'bus> {
|
||||
device: Device<'bus>,
|
||||
}
|
||||
|
||||
impl<'bus> Redstone<'bus> {
|
||||
pub const TYPE_NAME: &'static str = "redstone";
|
||||
|
||||
/// Attempt to find and wrap the first redstone-compatible device on the bus.
|
||||
pub fn attach(bus: &'bus mut DeviceBus) -> Result<Option<Self>> {
|
||||
let Some(entry) = bus.find(Self::TYPE_NAME)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(bus.device(&entry.device_id)?.map(|device| Self { device }))
|
||||
}
|
||||
|
||||
/// Create a typed wrapper from an existing [`Device`].
|
||||
pub fn from_device(device: Device<'bus>) -> Self {
|
||||
Self { device }
|
||||
}
|
||||
|
||||
/// Access the underlying device entry metadata.
|
||||
pub fn info(&self) -> &DeviceEntry {
|
||||
self.device.info()
|
||||
}
|
||||
|
||||
/// Borrow the underlying dynamic device handle.
|
||||
pub fn as_device(&mut self) -> &mut Device<'bus> {
|
||||
&mut self.device
|
||||
}
|
||||
|
||||
/// Read the redstone input level for `side`.
|
||||
pub fn input(&mut self, side: Side) -> Result<f64> {
|
||||
let value = self.device.call("getRedstoneInput", (side.as_str(),))?;
|
||||
expect_number(value, "getRedstoneInput")
|
||||
}
|
||||
|
||||
/// Read the currently configured output level for `side`.
|
||||
pub fn output(&mut self, side: Side) -> Result<f64> {
|
||||
let value = self.device.call("getRedstoneOutput", (side.as_str(),))?;
|
||||
expect_number(value, "getRedstoneOutput")
|
||||
}
|
||||
|
||||
/// Set the output strength on `side` (0-15).
|
||||
pub fn set_output_level(&mut self, side: Side, level: u8) -> Result<()> {
|
||||
let clamped = level.min(15) as u64;
|
||||
let _ = self
|
||||
.device
|
||||
.call("setRedstoneOutput", (side.as_str(), clamped))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convenience for enabling (`true`) or disabling (`false`) an output.
|
||||
pub fn set_output_state(&mut self, side: Side, active: bool) -> Result<()> {
|
||||
self.set_output_level(side, if active { 15 } else { 0 })
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_number(value: JsonValue, context: &str) -> Result<f64> {
|
||||
match value {
|
||||
JsonValue::Number(Number::F64(v)) => Ok(v),
|
||||
JsonValue::Number(Number::I64(v)) => Ok(v as f64),
|
||||
JsonValue::Number(Number::U64(v)) => Ok(v as f64),
|
||||
other => Err(Error::Protocol(format!(
|
||||
"expected numeric response from {context}, got {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Redstone interface sides used by OC2R.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Side {
|
||||
Up,
|
||||
Down,
|
||||
North,
|
||||
South,
|
||||
West,
|
||||
East,
|
||||
Front,
|
||||
Back,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Side {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Side::Up => "up",
|
||||
Side::Down => "down",
|
||||
Side::North => "north",
|
||||
Side::South => "south",
|
||||
Side::West => "west",
|
||||
Side::East => "east",
|
||||
Side::Front => "front",
|
||||
Side::Back => "back",
|
||||
Side::Left => "left",
|
||||
Side::Right => "right",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Side {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Side {
|
||||
type Err = ParseSideError;
|
||||
|
||||
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"up" => Ok(Side::Up),
|
||||
"down" => Ok(Side::Down),
|
||||
"north" => Ok(Side::North),
|
||||
"south" => Ok(Side::South),
|
||||
"west" => Ok(Side::West),
|
||||
"east" => Ok(Side::East),
|
||||
"front" => Ok(Side::Front),
|
||||
"back" => Ok(Side::Back),
|
||||
"left" => Ok(Side::Left),
|
||||
"right" => Ok(Side::Right),
|
||||
_ => Err(ParseSideError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned when parsing a [`Side`] fails.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ParseSideError;
|
||||
|
||||
impl fmt::Display for ParseSideError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("invalid side")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseSideError {}
|
||||
14
src/lib.rs
14
src/lib.rs
@@ -7,28 +7,34 @@
|
||||
//!
|
||||
//! # Quick start
|
||||
//! ```no_run
|
||||
//! use oc2r_rust::{DeviceBus, JsonValue, Result, DEFAULT_DEVICE_PATH};
|
||||
//! use oc2r_rust::{DeviceBus, Result, DEFAULT_DEVICE_PATH};
|
||||
//!
|
||||
//! fn main() -> Result<()> {
|
||||
//! let mut bus = DeviceBus::connect(DEFAULT_DEVICE_PATH)?;
|
||||
//! let devices = bus.list()?;
|
||||
//! println!("Found {} device(s)", devices.len());
|
||||
//!
|
||||
//! if let Some(device) = bus.device(&devices[0].device_id)? {
|
||||
//! let response = device.invoke("help", &[JsonValue::Null])?;
|
||||
//! println!("help(): {response:?}");
|
||||
//! if let Some(entry) = devices.first() {
|
||||
//! if let Some(mut device) = bus.device(&entry.device_id)? {
|
||||
//! let response = device.call("help", ())?;
|
||||
//! println!("help(): {response:?}");
|
||||
//! }
|
||||
//! }
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub mod bus;
|
||||
pub mod devices;
|
||||
pub mod error;
|
||||
pub mod rpc;
|
||||
mod transport;
|
||||
mod value;
|
||||
|
||||
pub use bus::{Device, DeviceBus};
|
||||
pub use devices::redstone::{ParseSideError, Redstone, Side};
|
||||
pub use error::{Error, Result};
|
||||
pub use rpc::{DeviceEntry, JsonValue, MethodEntry, MethodParameter};
|
||||
pub use value::{IntoJsonArgs, IntoJsonValue};
|
||||
|
||||
pub const DEFAULT_DEVICE_PATH: &str = "/dev/hvc0";
|
||||
|
||||
127
src/value.rs
Normal file
127
src/value.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use crate::rpc::JsonValue;
|
||||
use miniserde::json::Number;
|
||||
|
||||
/// Convert common Rust types into `JsonValue`.
|
||||
pub trait IntoJsonValue {
|
||||
fn into_json(self) -> JsonValue;
|
||||
}
|
||||
|
||||
impl IntoJsonValue for JsonValue {
|
||||
fn into_json(self) -> JsonValue {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoJsonValue for &JsonValue {
|
||||
fn into_json(self) -> JsonValue {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoJsonValue for bool {
|
||||
fn into_json(self) -> JsonValue {
|
||||
JsonValue::Bool(self)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_int {
|
||||
($($ty:ty => $variant:ident),+ $(,)?) => {
|
||||
$(
|
||||
impl IntoJsonValue for $ty {
|
||||
fn into_json(self) -> JsonValue {
|
||||
JsonValue::Number(Number::$variant(self as _))
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
impl_int!(i8 => I64, i16 => I64, i32 => I64, i64 => I64, isize => I64);
|
||||
impl_int!(u8 => U64, u16 => U64, u32 => U64, u64 => U64, usize => U64);
|
||||
|
||||
impl IntoJsonValue for f32 {
|
||||
fn into_json(self) -> JsonValue {
|
||||
JsonValue::Number(Number::F64(self as f64))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoJsonValue for f64 {
|
||||
fn into_json(self) -> JsonValue {
|
||||
JsonValue::Number(Number::F64(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoJsonValue for String {
|
||||
fn into_json(self) -> JsonValue {
|
||||
JsonValue::String(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoJsonValue for &str {
|
||||
fn into_json(self) -> JsonValue {
|
||||
JsonValue::String(self.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoJsonValue for &String {
|
||||
fn into_json(self) -> JsonValue {
|
||||
JsonValue::String(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoJsonValue> IntoJsonValue for Option<T> {
|
||||
fn into_json(self) -> JsonValue {
|
||||
match self {
|
||||
Some(value) => value.into_json(),
|
||||
None => JsonValue::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert heterogeneous argument lists into JSON values.
|
||||
pub trait IntoJsonArgs {
|
||||
fn into_args(self) -> Vec<JsonValue>;
|
||||
}
|
||||
|
||||
impl IntoJsonArgs for () {
|
||||
fn into_args(self) -> Vec<JsonValue> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! tuple_args {
|
||||
($($name:ident),+) => {
|
||||
#[allow(non_snake_case, non_camel_case_types)]
|
||||
impl<$($name),+> IntoJsonArgs for ($($name,)+)
|
||||
where
|
||||
$($name: IntoJsonValue,)+
|
||||
{
|
||||
fn into_args(self) -> Vec<JsonValue> {
|
||||
let ($($name,)+) = self;
|
||||
vec![$($name.into_json(),)+]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
tuple_args!(a);
|
||||
tuple_args!(a, b);
|
||||
tuple_args!(a, b, c);
|
||||
tuple_args!(a, b, c, d);
|
||||
tuple_args!(a, b, c, d, e);
|
||||
tuple_args!(a, b, c, d, e, f);
|
||||
|
||||
impl<T: IntoJsonValue> IntoJsonArgs for Vec<T> {
|
||||
fn into_args(self) -> Vec<JsonValue> {
|
||||
self.into_iter().map(IntoJsonValue::into_json).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoJsonArgs for &[T]
|
||||
where
|
||||
T: IntoJsonValue + Clone,
|
||||
{
|
||||
fn into_args(self) -> Vec<JsonValue> {
|
||||
self.iter().cloned().map(IntoJsonValue::into_json).collect()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user