Moved devices into their own crate

This commit is contained in:
2025-10-31 22:14:18 +01:00
parent 41129f475d
commit c65634e1b3
22 changed files with 174 additions and 184 deletions

8
Cargo.lock generated
View File

@@ -56,6 +56,14 @@ dependencies = [
"miniserde-enum",
]
[[package]]
name = "opencomputers"
version = "0.1.0"
dependencies = [
"libc",
"oc2r-core",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"

View File

@@ -24,3 +24,4 @@ codegen-units = 1
[workspace.dependencies]
oc2r-core = { path = "crates/oc2r-core" }
opencomputers = { path = "crates/opencomputers" }

View File

@@ -5,7 +5,8 @@ Thin, synchronous access to the [OpenComputers II: Reimagined](https://github.co
This repository is a workspace is divided into multiple crates:
- `crates/oc2r-core`: mirrors the stock Lua helpers (`devices.lua`, `robot.lua`) so you can list devices, invoke methods,
subscribe to events, and work with typed wrappers for common peripherals.
subscribe to events, and work with the low-level RPC primitives.
- `crates/opencomputers`: companion crate that provides strongly typed wrappers for the common HLAPI peripherals.
## Development
@@ -26,4 +27,3 @@ Each crates has some examples of how to use it crate are in the `examples` direc
Copy the resulting binaries from `target/<triple>/release/examples/` into Minux (an Import/Export card works
well), mark them as executable and then execute them inside the VM.

View File

@@ -1,13 +1,66 @@
# OC2R-Core
This crate mirrors the stock Lua helpers (`devices.lua`, `robot.lua`) so you can list devices, invoke methods,
subscribe to events, and work with typed wrappers for common peripherals.
Minimal Rust client for the OC2R high-level API. It mirrors the bundled Lua
helper (`devices.lua`) so you can list devices, invoke methods,
and subscribe to events. For ergonomic wrappers on top of these primitives, add
the [`opencomputers`](../opencomputers) crate.
## Documentation
## Getting Started
See [`docs/index.md`](docs/index.md) for the full guide and
[`docs/wrappers.md`](docs/wrappers.md) for device-by-device examples.
1. Install [cross](https://github.com/cross-rs/cross):
```bash
cargo install cross --git https://github.com/cross-rs/cross
```
2. Compile your binary for Minux:
```bash
cross build --release
```
3. Copy `target/<triple>/release/your-app` onto the Import/Export card and drop
it inside the OC2R VM (for example `/usr/bin/your-app`).
## Quick Start
```rust
use oc2r_core::{DeviceBus, DeviceEvent, Result, DEFAULT_DEVICE_PATH};
fn main() -> Result<()> {
let mut bus = DeviceBus::connect(DEFAULT_DEVICE_PATH)?;
// Enumerate devices.
let devices = bus.list()?;
println!("Found {} device(s)", devices.len());
// Call a method dynamically.
let first = &devices[0].device_id;
let mut device = bus.device(first)?.expect("device disappeared");
let response = device.call("help", ())?;
println!("help(): {response}");
// Subscribe to events and drain one.
device.subscribe()?;
let event = bus.next_event_for(&device_id)?;
println!("{device_id} -> {:#?}", event.payload);
Ok(())
}
```
## Transport & API Surface
- **Framing:** Messages are JSON framed with a leading/trailing NUL, exactly
like the Lua RPC client. `DeviceBus::flush` drains stale responses before each
call.
- **Dynamic calls:** Use `Device::call` or `DeviceBus::call`. Anything that
implements `IntoJsonArgs` (tuples, slices, `Vec<T>`) is accepted and converted
into JSON automatically.
- **Subscriptions:** `Device::subscribe` / `Device::unsubscribe` send the
corresponding RPC requests. `DeviceBus::next_event_for(device_id)` waits for
the next matching event; `Device::next_event()` provides a convenience
wrapper. `try_event*` variants check queues without blocking. For a Lua-style
dispatcher, use [`EventLoop`](src/event.rs) which mirrors `event.pull` and
friends.
## Development
The crate targets rust nightly for some size optimisations and relies on `miniserde` and `libc`.
The crate targets rust nightly for size optimisations and relies on `miniserde`
and `libc`.

View File

@@ -1,84 +0,0 @@
# OC2R Rust Client Guide
This guide walks through everything you can do with the crate: connecting to
the RPC controller, issuing method calls, handling events, and using the typed
wrappers that mirror `devices.lua`.
## Getting Started
1. Install [cross](https://github.com/cross-rs/cross):
```bash
cargo install cross --git https://github.com/cross-rs/cross
```
2. Compile your binary for Minux:
```bash
cross build --release
```
3. Copy `target/<triple>/release/your-app` onto the Import/Export card and drop
it inside the OC2R VM (e.g. `/usr/bin/your-app`).
## Quick Start
```rust
use oc2r_rust::{DeviceBus, DeviceEvent, Result, DEFAULT_DEVICE_PATH};
fn main() -> Result<()> {
let mut bus = DeviceBus::connect(DEFAULT_DEVICE_PATH)?;
// Enumerate devices.
let devices = bus.list()?;
println!("Found {} device(s)", devices.len());
// Call a method dynamically.
let first = &devices[0].device_id;
let mut device = bus.device(first)?.expect("device disappeared");
let response = device.call("help", ())?;
println!("help(): {response}");
// Subscribe to events and drain one.
device.subscribe()?;
let event = bus.next_event_for(&device_id)?;
println!("{device_id} -> {:#?}", event.payload);
Ok(())
}
```
## Transport & API Surface
- **Framing:** Messages are JSON framed with a leading/trailing NUL, exactly like
Luas RPC client. `DeviceBus::flush` drains stale responses before each call.
- **Dynamic calls:** Use `Device::call` or `DeviceBus::call`. Anything that
implements `IntoJsonArgs` (tuples, slices, `Vec<T>`) is accepted and converted
into JSON automatically.
- **Subscriptions:** `Device::subscribe` / `Device::unsubscribe` send the
corresponding RPC requests. `DeviceBus::next_event_for(device_id)` waits for
the next matching event; `Device::next_event()` provides a convenience wrapper.
`try_event*` variants check queues without blocking. For a Lua-style
dispatcher, use [`EventLoop`](../src/event.rs) which mirrors `event.pull` and
friends.
## Typed Wrappers
To avoid raw JSON, every base device has a wrapper with strongly typed methods:
- `RedstoneInterface`
- `EnergyStorage`
- `FluidHandler`
- `ItemHandler`
- `Cpu`
- `SoundCard`
- `BlockOperations`
- `InventoryOperations`
- `FileImportExport`
- `Robot`
Each wrapper exposes Rust-style methods (e.g. `RedstoneInterface::input`, `SoundCard::find`)
built on top of the dynamic API. See [`wrappers.md`](wrappers.md) for detailed
examples and method-by-method coverage.
## Additional Topics
- [Device wrapper reference](wrappers.md)
- [Redstone event example](../examples/redstone-events.rs)
- [Lua-style event loop](../src/event.rs)

View File

@@ -3,11 +3,12 @@
//! This crate mirrors the behaviour of the bundled Lua `devices.lua`
//! helper. It communicates with the OC2R RPC controller over the first
//! VirtIO console device (`/dev/hvc0`) and exposes helpers for listing
//! devices, inspecting available methods and invoking them.
//! devices, inspecting available methods and invoking them. For ergonomic
//! wrappers built on this low level API, add the `opencomputers` crate.
//!
//! # Quick start
//! ```no_run
//! use oc2r_rust::{DeviceBus, Result, DEFAULT_DEVICE_PATH};
//! use oc2r_core::{DeviceBus, Result, DEFAULT_DEVICE_PATH};
//!
//! fn main() -> Result<()> {
//! let mut bus = DeviceBus::connect(DEFAULT_DEVICE_PATH)?;
@@ -25,7 +26,6 @@
//! ```
pub mod bus;
pub mod devices;
pub mod error;
pub mod event;
pub mod rpc;
@@ -33,14 +33,9 @@ mod transport;
mod value;
pub use bus::{Device, DeviceBus, DeviceEvent};
pub use devices::{
BlockOperations, Cpu, EnergyStorage, FileImportExport, FluidHandler, ImportedFileInfo,
InventoryOperations, ItemHandler, ParseSideError, RedstoneInterface, RedstoneSignal, Robot,
RobotDirection, RobotSide, Side, SoundCard,
};
pub use error::{Error, Result};
pub use event::{EventLoop, Signal};
pub use rpc::{DeviceEntry, JsonValue, MethodEntry, MethodParameter};
pub use rpc::{DeviceEntry, JsonArray, JsonNumber, JsonValue, MethodEntry, MethodParameter};
pub use value::{IntoJsonArgs, IntoJsonValue, JsonValueExt};
pub const DEFAULT_DEVICE_PATH: &str = "/dev/hvc0";

View File

@@ -71,3 +71,5 @@ pub struct MethodParameter {
}
pub type JsonValue = json::Value;
pub type JsonArray = json::Array;
pub type JsonNumber = json::Number;

View File

@@ -0,0 +1,10 @@
[package]
name = "opencomputers"
version = "0.1.0"
edition = "2024"
[dependencies]
oc2r-core = { path = "../oc2r-core" }
[dev-dependencies]
libc = "0.2"

View File

@@ -1,26 +1,31 @@
# Device Wrapper Reference
# opencomputers
Each wrapper mirrors the Lua helper for the corresponding OC2R device and builds
on top of `Device::call`. All examples assume:
Typed wrappers for the OC2R high-level API based on `oc2r-core`.
Each module exposes a safe, idiomatic Rust interface for a specific
device class.
## Usage Overview
Every wrapper sits on top of `Device::call`. Typical imports look like:
```rust
use oc2r_rust::{DeviceBus, Result, DEFAULT_DEVICE_PATH};
use oc2r_core::{DeviceBus, Result, DEFAULT_DEVICE_PATH};
use opencomputers::*;
```
Before using any wrapper, attach it:
Attach the device before use:
```rust
let mut bus = DeviceBus::connect(DEFAULT_DEVICE_PATH)?;
let mut redstone = oc2r_rust::RedstoneInterface::attach(&mut bus)?
let mut redstone = RedstoneInterface::attach(&mut bus)?
.expect("no redstone interface attached");
// call unsubscribe() once you are done processing events
redstone.subscribe()?;
redstone.subscribe()?; // unsubscribe() once you are done
```
## Robot
### Robot
```rust
use oc2r_rust::{Robot, RobotDirection};
use opencomputers::{Robot, RobotDirection};
let mut robot = Robot::attach(&mut bus)?.expect("no robot available");
println!("energy {}/{}", robot.energy()?, robot.capacity()?);
@@ -37,10 +42,10 @@ if robot.move_blocking(RobotDirection::Forward)? {
robot.turn(RobotDirection::Left)?;
```
## Redstone
### Redstone Interface
```rust
use oc2r_rust::{RedstoneInterface, RedstoneSignal, Side};
use opencomputers::{RedstoneInterface, RedstoneSignal, Side};
redstone.set_output_state(Side::East, true)?;
let input = redstone.input(Side::East)?;
@@ -52,14 +57,15 @@ if let Some(signal) = redstone.try_event()? {
}
```
`Side` implements `FromStr` so strings like "north" can be parsed safely.
Incoming events surface as `RedstoneSignal { side, level }`; see also the
`examples/redstone-events.rs` program.
`Side` implements `FromStr` so strings like "north" parse safely. Incoming
events surface as `RedstoneSignal { side, level }`; see
[`examples/redstone-events.rs`](examples/redstone-events.rs) for a full event
loop.
## Energy Storage
### Energy Storage
```rust
use oc2r_rust::EnergyStorage;
use opencomputers::EnergyStorage;
let mut energy = EnergyStorage::attach(&mut bus)?.expect("no energy storage");
println!("{}/{}", energy.energy_stored()?, energy.max_energy_stored()?);
@@ -67,10 +73,10 @@ println!("can_extract: {}", energy.can_extract()?);
println!("can_receive: {}", energy.can_receive()?);
```
## Fluid Handler
### Fluid Handler
```rust
use oc2r_rust::FluidHandler;
use opencomputers::FluidHandler;
let mut fluids = FluidHandler::attach(&mut bus)?.expect("no fluid handler");
let tanks = fluids.tank_count()? as usize;
@@ -81,10 +87,10 @@ for tank in 0..tanks {
}
```
## Item Handler
### Item Handler
```rust
use oc2r_rust::ItemHandler;
use opencomputers::ItemHandler;
let mut items = ItemHandler::attach(&mut bus)?.expect("no item handler");
let slots = items.slot_count()? as usize;
@@ -95,19 +101,19 @@ for slot in 0..slots {
}
```
## CPU
### CPU
```rust
use oc2r_rust::Cpu;
use opencomputers::Cpu;
let mut cpu = Cpu::attach(&mut bus)?.expect("no cpu card");
println!("frequency {} MHz", cpu.frequency()?);
```
## Sound Card
### Sound Card
```rust
use oc2r_rust::SoundCard;
use opencomputers::SoundCard;
let mut sound = SoundCard::attach(&mut bus)?.expect("no sound card");
sound.play_with_volume_and_pitch("minecraft:block.note_block.harp", 0.8, 1.2)?;
@@ -115,10 +121,10 @@ let matches = sound.find("note_block")?;
println!("found {} sound(s)", matches.len());
```
## Block Operations Module
### Block Operations Module
```rust
use oc2r_rust::{BlockOperations, RobotSide};
use opencomputers::{BlockOperations, RobotSide};
let mut ops = BlockOperations::attach(&mut bus)?.expect("no block module");
ops.excavate_on(RobotSide::Front)?;
@@ -129,10 +135,10 @@ if ops.repair()? {
}
```
## Inventory Operations Module
### Inventory Operations Module
```rust
use oc2r_rust::{InventoryOperations, RobotSide};
use opencomputers::{InventoryOperations, RobotSide};
let mut inv = InventoryOperations::attach(&mut bus)?.expect("no inventory module");
inv.move_items(0, 1, 32)?;
@@ -141,10 +147,10 @@ let taken = inv.take_from_slot_on(0, 8, RobotSide::Up)?;
println!("taken {taken}");
```
## File Import/Export Card
### File Import/Export Card
```rust
use oc2r_rust::FileImportExport;
use opencomputers::FileImportExport;
let mut fie = FileImportExport::attach(&mut bus)?.expect("no file card");
fie.reset()?;
@@ -154,7 +160,11 @@ fie.finish_export()?;
if fie.request_import()? {
if let Some(info) = fie.begin_import()? {
println!("importing {} ({} bytes)", info.name.unwrap_or_default(), info.size);
println!(
"importing {} ({} bytes)",
info.name.unwrap_or_default(),
info.size
);
while let Some(bytes) = fie.read_import_chunk()? {
// write bytes to disk
println!("chunk {} bytes", bytes.len());
@@ -163,9 +173,12 @@ if fie.request_import()? {
}
```
The raw event payloads for these devices can also be obtained via
`Device::next_event`/`DeviceBus::next_event_for` when you need to handle
custom packets.
Device-specific events remain accessible through `Device::next_event` or
`DeviceBus::next_event_for` when you need raw payloads.
Feel free to extend these wrappers or add new ones; each uses the same
`define_wrapper!` macro found in `src/devices/mod.rs`.
## Examples
- [`examples/redstone.rs`](examples/redstone.rs) — control redstone IO from the
command line.
- [`examples/redstone-events.rs`](examples/redstone-events.rs) — subscribe to
redstone change events.

View File

@@ -1,4 +1,5 @@
use oc2r_core::{DEFAULT_DEVICE_PATH, DeviceBus, EventLoop, JsonValueExt, RedstoneInterface, Result};
use oc2r_core::{DEFAULT_DEVICE_PATH, DeviceBus, EventLoop, JsonValueExt, Result};
use opencomputers::RedstoneInterface;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;

View File

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

View File

@@ -1,7 +1,6 @@
use crate::Result;
use crate::value::JsonValueExt;
use oc2r_core::{JsonValueExt, Result};
crate::devices::define_wrapper!(
crate::define_wrapper!(
BlockOperations,
"block_operations",
"Wrapper for the block operations robot module."

View File

@@ -1,7 +1,6 @@
use crate::Result;
use crate::value::JsonValueExt;
use oc2r_core::{JsonValueExt, Result};
crate::devices::define_wrapper!(Cpu, "cpu", "Wrapper exposing CPU metadata.");
crate::define_wrapper!(Cpu, "cpu", "Wrapper exposing CPU metadata.");
impl<'bus> Cpu<'bus> {
pub fn frequency(&mut self) -> Result<i64> {

View File

@@ -1,7 +1,6 @@
use crate::Result;
use crate::value::JsonValueExt;
use oc2r_core::{JsonValueExt, Result};
crate::devices::define_wrapper!(
crate::define_wrapper!(
EnergyStorage,
"energy_storage",
"Wrapper for OC2R energy storage devices."

View File

@@ -1,8 +1,6 @@
use crate::value::JsonValueExt;
use crate::{Error, JsonValue, Result};
use miniserde::json::{Array, Number};
use oc2r_core::{Error, JsonArray, JsonNumber, JsonValue, JsonValueExt, Result};
crate::devices::define_wrapper!(
crate::define_wrapper!(
FileImportExport,
"file_import_export",
"Wrapper for the file import/export card."
@@ -28,8 +26,8 @@ impl<'bus> FileImportExport<'bus> {
chunk
.iter()
.copied()
.map(|byte| JsonValue::Number(Number::U64(byte as u64)))
.collect::<Array>(),
.map(|byte| JsonValue::Number(JsonNumber::U64(byte as u64)))
.collect::<JsonArray>(),
);
self.device.call("writeExportFile", (payload,)).map(|_| ())
}

View File

@@ -1,7 +1,6 @@
use crate::value::JsonValueExt;
use crate::{JsonValue, Result};
use oc2r_core::{JsonValue, JsonValueExt, Result};
crate::devices::define_wrapper!(
crate::define_wrapper!(
FluidHandler,
"fluid_handler",
"Wrapper for Forge fluid handler devices."

View File

@@ -1,8 +1,7 @@
use crate::Result;
use crate::devices::RobotSide;
use crate::value::JsonValueExt;
use crate::RobotSide;
use oc2r_core::{JsonValueExt, Result};
crate::devices::define_wrapper!(
crate::define_wrapper!(
InventoryOperations,
"inventory_operations",
"Wrapper for the inventory operations robot module."

View File

@@ -1,7 +1,6 @@
use crate::value::JsonValueExt;
use crate::{JsonValue, Result};
use oc2r_core::{JsonValue, JsonValueExt, Result};
crate::devices::define_wrapper!(
crate::define_wrapper!(
ItemHandler,
"item_handler",
"Wrapper for Forge item handler devices."

View File

@@ -1,31 +1,32 @@
//! Typed OC2R device wrappers built on top of `oc2r-core`.
macro_rules! define_wrapper {
($name:ident, $type_name:literal, $doc:literal) => {
#[doc = $doc]
pub struct $name<'bus> {
pub(crate) device: crate::Device<'bus>,
pub(crate) device: oc2r_core::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 attach(bus: &'bus mut oc2r_core::DeviceBus) -> oc2r_core::Result<Option<Self>> {
crate::attach_device(bus, Self::TYPE_NAME).map(|opt| opt.map(Self::from_device))
}
pub fn from_device(device: crate::Device<'bus>) -> Self {
pub fn from_device(device: oc2r_core::Device<'bus>) -> Self {
Self { device }
}
pub fn info(&self) -> &crate::DeviceEntry {
pub fn info(&self) -> &oc2r_core::DeviceEntry {
self.device.info()
}
pub fn as_device(&mut self) -> &mut crate::Device<'bus> {
pub fn as_device(&mut self) -> &mut oc2r_core::Device<'bus> {
&mut self.device
}
pub fn into_device(self) -> crate::Device<'bus> {
pub fn into_device(self) -> oc2r_core::Device<'bus> {
self.device
}
}
@@ -35,9 +36,9 @@ macro_rules! define_wrapper {
pub(crate) use define_wrapper;
pub(crate) fn attach_device<'bus>(
bus: &'bus mut crate::DeviceBus,
bus: &'bus mut oc2r_core::DeviceBus,
type_name: &str,
) -> crate::Result<Option<crate::Device<'bus>>> {
) -> oc2r_core::Result<Option<oc2r_core::Device<'bus>>> {
match bus.find(type_name)? {
Some(entry) => bus.device(&entry.device_id),
None => Ok(None),

View File

@@ -1,9 +1,8 @@
use crate::value::JsonValueExt;
use crate::{DeviceEvent, Error, Result};
use oc2r_core::{DeviceEvent, Error, JsonValueExt, Result};
use std::fmt;
use std::str::FromStr;
crate::devices::define_wrapper!(
crate::define_wrapper!(
RedstoneInterface,
"redstone",
"Typed wrapper around the HLAPI redstone interface."

View File

@@ -1,9 +1,8 @@
use crate::value::JsonValueExt;
use crate::{Error, JsonValue, Result};
use oc2r_core::{Error, JsonValue, JsonValueExt, Result};
use std::thread;
use std::time::Duration;
crate::devices::define_wrapper!(
crate::define_wrapper!(
Robot,
"robot",
"High level wrapper mirroring the Lua robot helper."

View File

@@ -1,7 +1,6 @@
use crate::Result;
use crate::value::JsonValueExt;
use oc2r_core::{JsonValueExt, Result};
crate::devices::define_wrapper!(
crate::define_wrapper!(
SoundCard,
"sound",
"Wrapper around the OC2R sound card HLAPI."