Add more devices

This commit is contained in:
2025-10-29 20:35:12 +01:00
parent 8c50eb0f84
commit 9fbe103827
15 changed files with 567 additions and 60 deletions

View File

@@ -5,6 +5,7 @@ rustflags = [
"-Zfmt-debug=none",
"-Zunstable-options",
"-Cpanic=immediate-abort",
"-Zpanic_abort_tests",
]
[unstable]

View File

@@ -58,20 +58,23 @@ 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};
use oc2r_rust::{DeviceBus, Redstone, Result, Side, 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(())
redstone.set_output_state(Side::East, true)?;
println!("in: {}", redstone.input(Side::East)?);
Ok(())
}
```
Available wrappers: `Redstone`, `EnergyStorage`, `FluidHandler`, `ItemHandler`,
`Cpu`, `SoundCard`, `BlockOperations`, `InventoryOperations`, and
`FileImportExport`.
### Building the examples
Two runnable examples live in `examples/`:

View File

@@ -1,7 +1,6 @@
// Build with: cross build --release --example redstone
use oc2r_rust::devices::redstone::{Redstone, Side};
use oc2r_rust::{DEFAULT_DEVICE_PATH, DeviceBus, Result};
use oc2r_rust::{DEFAULT_DEVICE_PATH, DeviceBus, Redstone, Result, Side};
use std::env;
use std::str::FromStr;

View File

@@ -0,0 +1,56 @@
use crate::Result;
use crate::value::JsonValueExt;
crate::devices::define_wrapper!(
BlockOperations,
"block_operations",
"Wrapper for the block operations robot module."
);
impl<'bus> BlockOperations<'bus> {
pub fn excavate(&mut self) -> Result<bool> {
self.device.call("excavate", ())?.into_bool("excavate")
}
pub fn excavate_on(&mut self, side: RobotSide) -> Result<bool> {
self.device
.call("excavate", (side.as_str(),))?
.into_bool("excavate")
}
pub fn place(&mut self) -> Result<bool> {
self.device.call("place", ())?.into_bool("place")
}
pub fn place_on(&mut self, side: RobotSide) -> Result<bool> {
self.device
.call("place", (side.as_str(),))?
.into_bool("place")
}
pub fn durability(&mut self) -> Result<i64> {
self.device.call("durability", ())?.into_i64("durability")
}
pub fn repair(&mut self) -> Result<bool> {
self.device.call("repair", ())?.into_bool("repair")
}
}
/// Robot operation sides recognised by OC2R.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RobotSide {
Front,
Up,
Down,
}
impl RobotSide {
pub const fn as_str(self) -> &'static str {
match self {
RobotSide::Front => "front",
RobotSide::Up => "up",
RobotSide::Down => "down",
}
}
}

12
src/devices/cpu.rs Normal file
View File

@@ -0,0 +1,12 @@
use crate::Result;
use crate::value::JsonValueExt;
crate::devices::define_wrapper!(Cpu, "cpu", "Wrapper exposing CPU metadata.");
impl<'bus> Cpu<'bus> {
pub fn frequency(&mut self) -> Result<i64> {
self.device
.call("getFrequency", ())?
.into_i64("getFrequency")
}
}

View File

@@ -0,0 +1,34 @@
use crate::Result;
use crate::value::JsonValueExt;
crate::devices::define_wrapper!(
EnergyStorage,
"energy_storage",
"Wrapper for OC2R energy storage devices."
);
impl<'bus> EnergyStorage<'bus> {
pub fn energy_stored(&mut self) -> Result<i64> {
self.device
.call("getEnergyStored", ())?
.into_i64("getEnergyStored")
}
pub fn max_energy_stored(&mut self) -> Result<i64> {
self.device
.call("getMaxEnergyStored", ())?
.into_i64("getMaxEnergyStored")
}
pub fn can_extract(&mut self) -> Result<bool> {
self.device
.call("canExtractEnergy", ())?
.into_bool("canExtractEnergy")
}
pub fn can_receive(&mut self) -> Result<bool> {
self.device
.call("canReceiveEnergy", ())?
.into_bool("canReceiveEnergy")
}
}

View File

@@ -0,0 +1,87 @@
use crate::value::JsonValueExt;
use crate::{Error, JsonValue, Result};
use miniserde::json::{Array, Number};
crate::devices::define_wrapper!(
FileImportExport,
"file_import_export",
"Wrapper for the file import/export card."
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportedFileInfo {
pub name: Option<String>,
pub size: u64,
}
impl<'bus> FileImportExport<'bus> {
pub fn reset(&mut self) -> Result<()> {
self.device.call("reset", ()).map(|_| ())
}
pub fn begin_export(&mut self, name: &str) -> Result<()> {
self.device.call("beginExportFile", (name,)).map(|_| ())
}
pub fn write_export_chunk(&mut self, chunk: &[u8]) -> Result<()> {
let payload = JsonValue::Array(
chunk
.iter()
.copied()
.map(|byte| JsonValue::Number(Number::U64(byte as u64)))
.collect::<Array>(),
);
self.device.call("writeExportFile", (payload,)).map(|_| ())
}
pub fn finish_export(&mut self) -> Result<()> {
self.device.call("finishExportFile", ()).map(|_| ())
}
pub fn request_import(&mut self) -> Result<bool> {
self.device
.call("requestImportFile", ())?
.into_bool("requestImportFile")
}
pub fn begin_import(&mut self) -> Result<Option<ImportedFileInfo>> {
match self.device.call("beginImportFile", ())? {
JsonValue::Null => Ok(None),
value => {
let mut object = value.into_object("beginImportFile")?;
let name = match object.remove("name") {
Some(JsonValue::Null) | None => None,
Some(other) => Some(other.into_string("beginImportFile.name")?),
};
let size_value = object
.remove("size")
.ok_or_else(|| Error::Protocol("beginImportFile missing size".into()))?;
let size = size_value.into_u64("beginImportFile.size")?;
Ok(Some(ImportedFileInfo { name, size }))
}
}
}
pub fn read_import_chunk(&mut self) -> Result<Option<Vec<u8>>> {
match self.device.call("readImportFile", ())? {
JsonValue::Null => Ok(None),
value => {
let bytes = value
.into_array("readImportFile")?
.into_iter()
.map(|item| {
let value = item.into_u64("readImportFile byte")?;
if value > 255 {
Err(Error::Protocol(format!(
"readImportFile returned byte value {value} > 255"
)))
} else {
Ok(value as u8)
}
})
.collect::<Result<Vec<u8>>>()?;
Ok(Some(bytes))
}
}
}
}

View File

@@ -0,0 +1,29 @@
use crate::value::JsonValueExt;
use crate::{JsonValue, Result};
crate::devices::define_wrapper!(
FluidHandler,
"fluid_handler",
"Wrapper for Forge fluid handler devices."
);
impl<'bus> FluidHandler<'bus> {
pub fn tank_count(&mut self) -> Result<i64> {
self.device
.call("getFluidTanks", ())?
.into_i64("getFluidTanks")
}
pub fn fluid_in_tank(&mut self, tank: usize) -> Result<Option<JsonValue>> {
match self.device.call("getFluidInTank", (tank,))? {
JsonValue::Null => Ok(None),
other => Ok(Some(other)),
}
}
pub fn tank_capacity(&mut self, tank: usize) -> Result<i64> {
self.device
.call("getFluidTankCapacity", (tank,))?
.into_i64("getFluidTankCapacity")
}
}

View File

@@ -0,0 +1,100 @@
use crate::Result;
use crate::devices::RobotSide;
use crate::value::JsonValueExt;
crate::devices::define_wrapper!(
InventoryOperations,
"inventory_operations",
"Wrapper for the inventory operations robot module."
);
impl<'bus> InventoryOperations<'bus> {
pub fn move_items(&mut self, from_slot: usize, into_slot: usize, count: u32) -> Result<()> {
self.device
.call("move", (from_slot, into_slot, count))
.map(|_| ())
}
pub fn drop(&mut self, count: u32) -> Result<i64> {
self.drop_internal(count, None)
}
pub fn drop_on(&mut self, count: u32, side: RobotSide) -> Result<i64> {
self.drop_internal(count, Some(side))
}
pub fn drop_into(&mut self, into_slot: usize, count: u32) -> Result<i64> {
self.drop_into_internal(into_slot, count, None)
}
pub fn drop_into_on(&mut self, into_slot: usize, count: u32, side: RobotSide) -> Result<i64> {
self.drop_into_internal(into_slot, count, Some(side))
}
pub fn take(&mut self, count: u32) -> Result<i64> {
self.take_internal(count, None)
}
pub fn take_on(&mut self, count: u32, side: RobotSide) -> Result<i64> {
self.take_internal(count, Some(side))
}
pub fn take_from_slot(&mut self, from_slot: usize, count: u32) -> Result<i64> {
self.take_from_internal(from_slot, count, None)
}
pub fn take_from_slot_on(
&mut self,
from_slot: usize,
count: u32,
side: RobotSide,
) -> Result<i64> {
self.take_from_internal(from_slot, count, Some(side))
}
fn drop_internal(&mut self, count: u32, side: Option<RobotSide>) -> Result<i64> {
let value = match side {
Some(side) => self.device.call("drop", (count, side.as_str()))?,
None => self.device.call("drop", (count,))?,
};
value.into_i64("drop")
}
fn drop_into_internal(
&mut self,
into_slot: usize,
count: u32,
side: Option<RobotSide>,
) -> Result<i64> {
let value = match side {
Some(side) => self
.device
.call("dropInto", (into_slot, count, side.as_str()))?,
None => self.device.call("dropInto", (into_slot, count))?,
};
value.into_i64("dropInto")
}
fn take_internal(&mut self, count: u32, side: Option<RobotSide>) -> Result<i64> {
let value = match side {
Some(side) => self.device.call("take", (count, side.as_str()))?,
None => self.device.call("take", (count,))?,
};
value.into_i64("take")
}
fn take_from_internal(
&mut self,
from_slot: usize,
count: u32,
side: Option<RobotSide>,
) -> Result<i64> {
let value = match side {
Some(side) => self
.device
.call("takeFrom", (from_slot, count, side.as_str()))?,
None => self.device.call("takeFrom", (from_slot, count))?,
};
value.into_i64("takeFrom")
}
}

View File

@@ -0,0 +1,29 @@
use crate::value::JsonValueExt;
use crate::{JsonValue, Result};
crate::devices::define_wrapper!(
ItemHandler,
"item_handler",
"Wrapper for Forge item handler devices."
);
impl<'bus> ItemHandler<'bus> {
pub fn slot_count(&mut self) -> Result<i64> {
self.device
.call("getItemSlotCount", ())?
.into_i64("getItemSlotCount")
}
pub fn stack_in_slot(&mut self, slot: usize) -> Result<Option<JsonValue>> {
match self.device.call("getItemStackInSlot", (slot,))? {
JsonValue::Null => Ok(None),
other => Ok(Some(other)),
}
}
pub fn slot_limit(&mut self, slot: usize) -> Result<i64> {
self.device
.call("getItemSlotLimit", (slot,))?
.into_i64("getItemSlotLimit")
}
}

View File

@@ -1 +1,65 @@
macro_rules! define_wrapper {
($name:ident, $type_name:literal, $doc:literal) => {
#[doc = $doc]
pub struct $name<'bus> {
pub(crate) device: crate::Device<'bus>,
}
impl<'bus> $name<'bus> {
pub const TYPE_NAME: &'static str = $type_name;
pub fn attach(bus: &'bus mut crate::DeviceBus) -> crate::Result<Option<Self>> {
crate::devices::attach_device(bus, Self::TYPE_NAME)
.map(|opt| opt.map(Self::from_device))
}
pub fn from_device(device: crate::Device<'bus>) -> Self {
Self { device }
}
pub fn info(&self) -> &crate::DeviceEntry {
self.device.info()
}
pub fn as_device(&mut self) -> &mut crate::Device<'bus> {
&mut self.device
}
pub fn into_device(self) -> crate::Device<'bus> {
self.device
}
}
};
}
pub(crate) use define_wrapper;
pub(crate) fn attach_device<'bus>(
bus: &'bus mut crate::DeviceBus,
type_name: &str,
) -> crate::Result<Option<crate::Device<'bus>>> {
match bus.find(type_name)? {
Some(entry) => bus.device(&entry.device_id),
None => Ok(None),
}
}
pub mod block_operations;
pub mod cpu;
pub mod energy_storage;
pub mod file_import_export;
pub mod fluid_handler;
pub mod inventory_operations;
pub mod item_handler;
pub mod redstone;
pub mod sound;
pub use block_operations::{BlockOperations, RobotSide};
pub use cpu::Cpu;
pub use energy_storage::EnergyStorage;
pub use file_import_export::{FileImportExport, ImportedFileInfo};
pub use fluid_handler::FluidHandler;
pub use inventory_operations::InventoryOperations;
pub use item_handler::ItemHandler;
pub use redstone::{ParseSideError, Redstone, Side};
pub use sound::SoundCard;

View File

@@ -1,59 +1,35 @@
use crate::bus::Device;
use crate::{DeviceBus, DeviceEntry, Error, JsonValue, Result};
use miniserde::json::Number;
use crate::Result;
use crate::value::JsonValueExt;
use std::fmt;
use std::str::FromStr;
/// Typed wrapper around the HLAPI redstone interface.
pub struct Redstone<'bus> {
device: Device<'bus>,
}
crate::devices::define_wrapper!(
Redstone,
"redstone",
"Typed wrapper around the HLAPI redstone interface."
);
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")
self.device
.call("getRedstoneInput", (side.as_str(),))?
.into_f64("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")
self.device
.call("getRedstoneOutput", (side.as_str(),))?
.into_f64("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(())
let clamped = level.min(15);
self.device
.call("setRedstoneOutput", (side.as_str(), clamped))
.map(|_| ())
}
/// Convenience for enabling (`true`) or disabling (`false`) an output.
@@ -62,17 +38,6 @@ impl<'bus> Redstone<'bus> {
}
}
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 {

38
src/devices/sound.rs Normal file
View File

@@ -0,0 +1,38 @@
use crate::Result;
use crate::value::JsonValueExt;
crate::devices::define_wrapper!(
SoundCard,
"sound",
"Wrapper around the OC2R sound card HLAPI."
);
impl<'bus> SoundCard<'bus> {
pub fn play(&mut self, name: &str) -> Result<()> {
self.device.call("playSound", (name,)).map(|_| ())
}
pub fn play_with_volume(&mut self, name: &str, volume: f32) -> Result<()> {
self.device.call("playSound", (name, volume)).map(|_| ())
}
pub fn play_with_volume_and_pitch(
&mut self,
name: &str,
volume: f32,
pitch: f32,
) -> Result<()> {
self.device
.call("playSound", (name, volume, pitch))
.map(|_| ())
}
pub fn find(&mut self, query: &str) -> Result<Vec<String>> {
self.device
.call("findSound", (query,))?
.into_array("findSound")?
.into_iter()
.map(|value| value.into_string("findSound result"))
.collect::<Result<Vec<String>>>()
}
}

View File

@@ -32,7 +32,10 @@ mod transport;
mod value;
pub use bus::{Device, DeviceBus};
pub use devices::redstone::{ParseSideError, Redstone, Side};
pub use devices::{
BlockOperations, Cpu, EnergyStorage, FileImportExport, FluidHandler, ImportedFileInfo,
InventoryOperations, ItemHandler, ParseSideError, Redstone, RobotSide, Side, SoundCard,
};
pub use error::{Error, Result};
pub use rpc::{DeviceEntry, JsonValue, MethodEntry, MethodParameter};
pub use value::{IntoJsonArgs, IntoJsonValue};

View File

@@ -1,5 +1,7 @@
use crate::error::{Error, Result};
use crate::rpc::JsonValue;
use miniserde::json::Number;
use std::collections::BTreeMap;
/// Convert common Rust types into `JsonValue`.
pub trait IntoJsonValue {
@@ -125,3 +127,88 @@ where
self.iter().cloned().map(IntoJsonValue::into_json).collect()
}
}
/// Convenience conversions from raw JSON values returned by the bus.
pub trait JsonValueExt {
fn into_bool(self, context: &str) -> Result<bool>;
fn into_i64(self, context: &str) -> Result<i64>;
fn into_u64(self, context: &str) -> Result<u64>;
fn into_f64(self, context: &str) -> Result<f64>;
fn into_string(self, context: &str) -> Result<String>;
fn into_array(self, context: &str) -> Result<Vec<JsonValue>>;
fn into_object(self, context: &str) -> Result<BTreeMap<String, JsonValue>>;
}
impl JsonValueExt for JsonValue {
fn into_bool(self, context: &str) -> Result<bool> {
match self {
JsonValue::Bool(value) => Ok(value),
other => Err(Error::Protocol(format!(
"expected boolean from {context}, got {other:?}"
))),
}
}
fn into_i64(self, context: &str) -> Result<i64> {
match self {
JsonValue::Number(Number::I64(value)) => Ok(value),
JsonValue::Number(Number::U64(value)) => value.try_into().map_err(|_| {
Error::Protocol(format!("{context} returned value too large for i64"))
}),
JsonValue::Number(Number::F64(value)) => Ok(value as i64),
other => Err(Error::Protocol(format!(
"expected integer from {context}, got {other:?}"
))),
}
}
fn into_u64(self, context: &str) -> Result<u64> {
match self {
JsonValue::Number(Number::U64(value)) => Ok(value),
JsonValue::Number(Number::I64(value)) if value >= 0 => Ok(value as u64),
JsonValue::Number(Number::F64(value)) if value >= 0.0 => Ok(value as u64),
other => Err(Error::Protocol(format!(
"expected unsigned integer from {context}, got {other:?}"
))),
}
}
fn into_f64(self, context: &str) -> Result<f64> {
match self {
JsonValue::Number(Number::F64(value)) => Ok(value),
JsonValue::Number(Number::I64(value)) => Ok(value as f64),
JsonValue::Number(Number::U64(value)) => Ok(value as f64),
other => Err(Error::Protocol(format!(
"expected floating point number from {context}, got {other:?}"
))),
}
}
fn into_string(self, context: &str) -> Result<String> {
match self {
JsonValue::String(value) => Ok(value),
other => Err(Error::Protocol(format!(
"expected string from {context}, got {other:?}"
))),
}
}
fn into_array(self, context: &str) -> Result<Vec<JsonValue>> {
match self {
JsonValue::Array(array) => Ok(array.into_iter().collect()),
JsonValue::Null => Ok(Vec::new()),
other => Err(Error::Protocol(format!(
"expected array from {context}, got {other:?}"
))),
}
}
fn into_object(self, context: &str) -> Result<BTreeMap<String, JsonValue>> {
match self {
JsonValue::Object(object) => Ok(object.into_iter().collect()),
other => Err(Error::Protocol(format!(
"expected object from {context}, got {other:?}"
))),
}
}
}