oc2_hlapi_v2 0.1.0

A client for the new version of OpenComputers II's HLAPI
Documentation
#![deny(missing_docs)]
#![warn(missing_debug_implementations)]

//! # OC2 HLAPI interface crate
//!
//! This crate provides a client for [OpenComputers II](https://github.com/fnuecke/oc2)'s HLAPI,
//! used to interface with high-level peripheral devices like redstone interfaces, robot movement
//! APIs, and transposers, among many others.
//!
//! <div class="warning">
//!
//! *This crate is specifically for the new 1.21.1 version of base OC2!* The original
//! OC2 for 1.18.2 and the community 1.20.1 port, OC2R, use an earlier version of the HLAPI that
//! is incompatible with this crate. Support for their version of the HLAPI will very likely not
//! be added, as crates to interface with both are already available, see
//! [miku-rpc](https://crates.io/crates/miku-rpc) and [oc2-hlapi](https://crates.io/crates/oc2-hlapi).
//!
//! </div>
//!
//! The implemented HLAPI is that of OC2 v0.10.1, the latest version at the time this version of the
//! crate was made.
//!
//! This crate is divided into three different layers of abstraction.
//! From lowest to highest level, they are:
//! * [`Transport`]s: Transports provide the raw byte streams over which HLAPI messages are sent, and
//!   may even adapt other transports, like the client for the HLAPI bus daemon.
//! * [`Codec`]s: Codecs handle message framing, (de)serialization and connection management, allowing
//!   for the sending and receiving of parsed Rust data structures instead of raw bytes. Only one is
//!   provided with this crate for now, as only one main codec exists.
//! * The [`HlapiClient`] + [device wrappers](crate::devices): [`HlapiClient`] handles listing devices,
//!   obtaining device wrappers, and reading device events. Wrappers are Rust structures that provide
//!   a typed API for HLAPI devices, instead of the dynamic typing that the HLAPI natively exposes.
//!   Devices without an available wrapper can be used either via the [`GenericDevice`] wrapper, which
//!   exposes the raw method call API, or a custom wrapper type can be written (this is the recommended
//!   approach).
//!
//! ## Cross-platform usage
//!
//! Most of this crate is platform-independent, and should work on any platform with `std`, though
//! it has only been tested on Linux, with a cross-build for Windows confirmed successful.
//! The [`LinuxVirtio`] and [`BusdUnixSocket`] transports are Linux-only, a custom transport is
//! required to use the client on other platforms.
//!
//! ## Basic usage
//!
//! The following is a basic usage example of this crate. It opens a connection (via the bus daemon
//! if available, otherwise via the virtio ports),
//! gets a wrapper for a redstone device, and sets the left output on.
//!
//! ```
//! use oc2_hlapi_v2::{
//!     client::HlapiClient,
//!     devices::{Side, redstone::Redstone},
//! };
//! use std::error::Error;
//!
//! # fn example() -> Result<(), Box<dyn Error>> {
//! let client = HlapiClient::connect()?;
//! let redstone = client.find_device_by_type::<Redstone>()?.ok_or_else(||
//!     Box::<dyn Error>::from("No redstone device found".to_owned())
//! )?;
//! redstone.set_redstone_output(Side::Left, 15)?;
//! # Ok(())
//! # }
//! ```
//!
//! ## Device events
//!
//! Device events in this crate are handled somewhat differently than the Lua library. The biggest change
//! is that waiting on an event for one device will not automatically discard events for other devices.
//!
//! To get events from a device, obtain an [`EventStream`] for the device by calling
//! [`Device::event_stream`]. Events are dropped for a device if no [`EventStream`]s exist for it,
//! so the stream will start out empty. Call [`EventStream::get_queued_event`] to obtain events waiting to be
//! processed, or [`EventStream::get_event`] to wait for an event from the device.
//!
//! [`EventStreamGroup`] allows waiting for events from any one of a group of devices all at once.
//! All devices in a given [`EventStreamGroup`] *must* be on the same client. Add streams to the
//! group with [`EventStreamGroup::add`] and wait for an event on any stream with
//! [`EventStreamGroup::wait_for_event`].
//!
//! [`EventStream::get_event`] and [`EventStreamGroup::wait_for_event`] both block the client from
//! being accessed from other threads while they are running, so if multiple threads are being used,
//! threads that wait for device events should have their own connection to the HLAPI bus.
//!
//! ## Writing a custom device wrapper
//!
//! Writing a custom device wrapper requires implementing the [`DeviceBase`] trait. You should have
//! at least two fields in your struct, one for a client and one for a device UUID. `new()` should
//! fill them in with the passed parameters and give default values for the other fields,
//! `uuid_base()` should get the UUID, and `client()` should get the client. A simple example is below.
//!
//! ```
//! use oc2_hlapi_v2::{
//!     client::{DeviceBase, HlapiClient, HlapiClientError},
//!     uuid::Uuid,
//! };
//!
//! use std::convert::Infallible;
//!
//! #[derive(Debug, Clone)]
//! pub struct RedstoneDevice {
//!     client: HlapiClient,
//!     uuid: Uuid,
//! }
//!
//! impl RedstoneDevice {
//!     // Note: for simplicity side is a string here, in the real wrapper it is an enum for type
//!     // safety. The value of strength is also not validated, unlike in the real wrapper.
//!     pub fn set_redstone_output(&self, side: &str, strength: u8) -> Result<(), HlapiClientError> {
//!         self.call::<()>("setRedstoneOutput", &[&side as _, &strength as _], &[])?;
//!         Ok(())
//!     }
//! }
//!
//! impl DeviceBase for RedstoneDevice {
//!     const TYPE: &'static str = "redstone";
//!
//!     type Event = Infallible;
//!
//!     fn new(client: HlapiClient, uuid: Uuid) -> Result<Self, HlapiClientError> {
//!         Ok(Self { client, uuid })
//!     }
//!
//!     fn uuid_base(&self) -> Uuid {
//!         self.uuid
//!     }
//!
//!     fn client(&self) -> &HlapiClient {
//!         &self.client
//!     }
//! }
//! ```
//!
//! ## AI usage disclaimer
//!
//! **TL;DR: AI-assisted, not vibecoded.** AI was primarily a helper checking that the code matched
//! the specs and providing bugfixes, and I wrote the vast majority of the code. The only pure AI bit is the test suite.
//! The docs are 100% human-written.
//!
//! Library code (`src/`) is 95% human-written, with some chunks of code from Claude that
//! have been reviewed and understood by me before using. Every feature started from a base of
//! fully human-written code. The test suite (`tests/`) is entirely AI-generated, as I am incredibly
//! bad at writing tests, and something is better than nothing. It appears to cover everything quite
//! well, and Claude was *not* allowed to make changes that broke the library to make tests pass.
//!
//! The documentation is 100% human written. If any bit of the docs feel AI-generated, well, I
//! happen to be autistic and the writing of us autistic folks very often gets falsely flagged
//! as AI by detectors...
//!
//! Claude was also used to do a, quite considerable, code review of the crate before it was published,
//! which is where the AI-generated chunks of code and test suite came from. Any and all design changes
//! (not many) as a result I agreed with, Claude did not make any changes without my OK.
//!
//! [`LinuxVirtio`]: crate::transports::LinuxVirtio
//! [`BusdUnixSocket`]: crate::transports::BusdUnixSocket
//! [`HlapiClient`]: crate::client::HlapiClient
//! [`GenericDevice`]: crate::devices::GenericDevice
//! [`DeviceBase`]: crate::client::DeviceBase
//! [`EventStream`]: crate::client::EventStream
//! [`Device::event_stream`]: crate::client::Device::event_stream
//! [`EventStream::get_queued_event`]: crate::client::EventStream::get_queued_event
//! [`EventStream::get_event`]: crate::client::EventStream::get_event
//! [`EventStreamGroup`]: crate::client::EventStreamGroup
//! [`EventStreamGroup::add`]: crate::client::EventStreamGroup::add
//! [`EventStreamGroup::wait_for_event`]: crate::client::EventStreamGroup::wait_for_event

pub mod client;
pub mod codecs;
pub mod devices;
pub mod messages;
pub mod transports;

pub use erased_serde;
pub use ndarray;
pub use rgb;
pub use serde_json;
pub use uuid;

#[cfg(target_os = "linux")]
use std::os::unix::net::UnixStream;
use std::{
    error::Error,
    io::{self, Read, Write},
    net::TcpStream,
    time::Duration,
};

use crate::messages::{BlobDescriptor, Event, Request, Response};

/// Transport trait
///
/// `Transport` provides a simple interface to allow plugging different base transports into
/// message codecs, allowing HLAPI messages to be sent to (or from) a VM host by arbitrary means.
///
/// Streams returned by a transport should implement [`Stream`] if possible
pub trait Transport {
    /// The stream container type provided by this transport
    type Streams;

    /// The error type of this transport
    type Error: Error + Send + Sync + 'static;

    /// Open the HLAPI connection stream(s)
    fn open_streams(&mut self) -> Result<Self::Streams, Self::Error>;
}

/// Stream trait
///
/// Abstracts over stream types to provide a generic API for non-blocking mode and read timeouts.
/// Necessary for event read timeouts and the like.
pub trait Stream: Read + Write {
    /// Sets non-blocking mode on the stream
    fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()>;

    /// Sets the read timeout on the stream. A zero timeout is invalid and will result in an error
    /// being returned. A timeout of [`None`] means no timeout, and [`read`] calls will block forever.
    ///
    /// [`read`]: Read::read
    fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()>;
}

impl Stream for TcpStream {
    fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
        self.set_nonblocking(nonblocking)
    }

    fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
        self.set_read_timeout(timeout)
    }
}

#[cfg(target_os = "linux")]
impl Stream for UnixStream {
    fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
        self.set_nonblocking(nonblocking)
    }

    fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
        self.set_read_timeout(timeout)
    }
}

/// Message codec trait
///
/// A `Codec` is responsible for the low-level sending and receiving of HLAPI messages.
/// It translates between the raw stream(s) of bytes and the Rust representation, handling framing,
/// (de)serialization, and connection management.
///
/// Codecs should take a [`Transport`] as an argument to allow using arbitrary byte transports for
/// HLAPI messages.
///
/// Using a codec directly requires upholding several rules about message order. Familiarity with
/// the [HLAPI docs](https://github.com/fnuecke/oc2/blob/1.21.1/docs/hlapi.md) is strongly
/// suggested, especially the [`Request Flow`](https://github.com/fnuecke/oc2/blob/1.21.1/docs/hlapi.md#request-flow)
/// and [`Binary Payloads`](https://github.com/fnuecke/oc2/blob/1.21.1/docs/hlapi.md#binary-payloads) sections.
pub trait Codec {
    /// The error type of the codec
    type Error: Error + Send + Sync + 'static;
    /// Send an HLAPI request, as well as the given blobs.
    fn send_request(&mut self, request: Request, blobs: &[&[u8]]) -> Result<(), Self::Error>;
    /// Reads an HLAPI response, with an optional timeout. A zero timeout is a non-blocking read.
    ///
    /// Returns `Ok(Some(_))` if a response was read, `Ok(None)` if the timeout was hit, or `Err(_)` if
    /// there was an error
    fn read_response(&mut self, timeout: Option<Duration>)
    -> Result<Option<Response>, Self::Error>;
    /// Reads an HLAPI event, with an optional timeout. A zero timeout is a non-blocking read.
    ///
    /// Returns `Ok(Some(_))` if an event was read, `Ok(None)` if the timeout was hit, or `Err(_)` if
    /// there was an error
    fn read_event(&mut self, timeout: Option<Duration>) -> Result<Option<Event>, Self::Error>;
    /// Reads an HLAPI blob with the given descriptor. Errors if the checksum is invalid.
    fn read_blob(&mut self, descriptor: BlobDescriptor) -> Result<Vec<u8>, Self::Error>;
    /// Clear the response stream of any stray data
    fn clear_response_stream(&mut self) -> Result<(), Self::Error>;
    /// Clear the blob stream of any stray data
    fn clear_blob_stream(&mut self) -> Result<(), Self::Error>;
}