Expand description
Idiomatic Rust client for the Mastervolt MasterBus CAN-bus protocol.
MasterBus is the CAN network used by Mastervolt marine power equipment — inverter/chargers, lithium and lead batteries, alternator regulators, solar/charge controllers, switch panels and displays. This crate speaks the protocol directly: it tracks which devices are present, discovers their schema (the groups and fields each device exposes), reads and writes those fields, and streams live values — without the vendor’s closed library.
§Quick start
The navigator API is a small tree of cheap, clonable, 'static handles:
MasterBus → Device → Group → Field.
use masterbus::{Config, MasterBus, Menu, Value};
// One-call connect using the per-host config file (auto-created on first
// run; see [`FileConfig`]). Or call [`MasterBus::socketcan`] /
// [`MasterBus::usb`] directly to bypass the file.
let bus = MasterBus::auto(Config::default())?;
// Browse the monitoring tab of every device currently on the bus.
for device in bus.devices() {
println!("{} (id {})", device.name()?, device.id());
for group in device.tab(Menu::Monitoring)? {
for field in group.fields()? {
println!(" {:<24} {:?}", field.name()?, field.value()?);
}
}
}
// Write a value. The supplied `Value` is checked against the field's schema
// type, and the value observed after the write is returned.
let device = bus.device(0x188EE2);
let applied = device.field(23).set(Value::Float(6.0))?;
println!("AC input limit is now {applied:?}");§Two API surfaces over one engine
-
Navigator (above): blocking and ergonomic, for scripts and one-shot tools.
Field::valuereturns a cached value when it is fresh enough (seeConfig::max_age) and otherwise polls the bus;Field::setwrites and confirms by observing the resulting value, so a rejected write is reported truthfully. -
Channel / event:
MasterBus::device_eventsstreams device presence (DeviceEvent), andMasterBus::subscribereturns aSubscriptionthat deliversValueUpdates at a chosen rate. Both arecrossbeam-channelreceivers, so a TUI or daemon canselect!bus events alongside its own (terminal input, timers, …).use std::time::Duration; use masterbus::{Config, DeviceEvent, MasterBus, Menu}; let bus = MasterBus::auto(Config::default())?; let events = bus.device_events(); // React to every device that comes online: subscribe to its // Monitoring tab and print value updates as they arrive. while let Ok(ev) = events.recv() { if let DeviceEvent::Alive(id) = ev { let device = bus.device(id); let mut fields = Vec::new(); for g in device.tab(Menu::Monitoring)? { for f in g.fields()? { fields.push(f.index()); } } let sub = bus.subscribe(id, fields, Duration::from_secs(1), true); std::thread::spawn(move || { while let Some(u) = sub.recv() { println!("0x{:06X} fid 0x{:03X} = {:?}", u.device, u.field, u.value); } }); } }Dropping the
Subscriptionunsubscribes from the engine; see thewatchexample in the repo for a fuller program that also tears down per-device subscriptions onDeviceEvent::Offline.
§Discovery and caching
Nothing is enumerated until you ask for it:
- Identity (name / article / serial / firmware) is the cheap half of
discovery and is fetched on demand — e.g.
Device::name. - Schema is discovered lazily per menu (
Menu::Monitoring,Menu::Configuration,Menu::Service):Device::tabdiscovers just that menu, whileDevice::groups/Device::schemadiscover all of them. - An optional on-disk cache (
Config::cache_path) persists each device’s schema across runs (keyed per device, by serial), so a long-running daemon discovers a device once and loads it from cache thereafter. Schemas are cached per device rather than shared by article/firmware, since same-model devices can differ (e.g. one battery in a cluster exposes an extra group).
All bus access is serialized on a single scheduler thread and paced to a bus budget, so the crate coexists with the boat’s real Mastervolt masters.
§Transports
The engine runs over any transport::Transport:
MasterBus::socketcan— Linux SocketCAN, built in on Linux.MasterBus::usb— the Mastervolt USB link (cross-platform, HID).MasterBus::with_transport— bring your owntransport::Transport.
§Values
Field values are a single Value enum (float, boolean, list selection,
Date, Time, device reference, …). Writes are expressed as a Value
and validated against the field’s VisualizationType before anything is
sent.
§Protocol reference
The wire protocol — frame classes, the date/time encodings, the
per-field metadata opcodes, writability, and more — is documented in
docs/PROTOCOL.md.
Re-exports§
pub use api::Device;pub use api::Field;pub use api::Group;pub use api::MasterBus;pub use api::Subscription;pub use api::MAX_EDITABLE_TEXT_BYTES;pub use error::Error;pub use error::Result;pub use model::field_id;pub use model::AccessLevel;pub use model::Channel;pub use model::DeviceId;pub use model::DeviceIdentity;pub use model::DeviceSchema;pub use model::DeviceStatus;pub use model::FieldId;pub use model::FieldInfo;pub use model::GroupInfo;pub use model::Menu;pub use protocol::VisualizationType;pub use settings::DeviceType;pub use settings::FileConfig;pub use value::Date;pub use value::Time;pub use value::Value;pub use value::WriteValue;
Modules§
- api
- Public navigator API:
MasterBus→Device→Group→Field, plus rate-based subscriptions. Handles are cheap,Clone,'static(Arc-backed). - error
- Error and result types for the public API.
- model
- Device schema model — the static, per-firmware description of a device that discovery produces and the optional disk cache persists.
- protocol
- MasterBus wire protocol: frame types, class constants, and codec.
- settings
- Permanentish per-host configuration: where the bus is and whether we drive it as master. Lives in a small INI file so tools don’t need transport arguments on every invocation.
- transport
- CAN transport abstraction.
- value
- Field value types.
Structs§
- Config
- Tunable, lib-level behaviour.
- Value
Update - A value update delivered to a subscriber.
Enums§
- Device
Event - Device presence change.