Meow API, feedback and shared PDOs
This guide covers the GUI's hex-motor 0.3.2 interface and Meow wire encoding. See the quick start for wiring and first motion, and the protocol reference for object addresses and errors.
Rust example
This Linux / SocketCAN example runs node 0x01 at 0.05 Rev/s for about one second, then sends zero velocity and disables the motor. Configure can0 using the quick start, secure the motor and keep clearance and a physical power cutoff available. Close the GUI and other programs controlling this channel.
Cargo.toml:
[package]
name = "meow-pv-example"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
can-transport = { version = "=0.1.4", features = ["socketcan"] }
hex-motor = "=0.3.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
src/main.rs:
use std::{sync::Arc, time::Duration};
use can_transport::{socketcan::SocketCanBus, CanBus};
use hex_motor::{
cia402::PdoProfile,
meow_motor::{
MeowMotorManager, MeowMotorManagerOptions, MeowMotorTarget, MeowProfileLimits,
},
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let bus: Arc<dyn CanBus> = Arc::new(SocketCanBus::open("can0")?);
let manager = MeowMotorManager::new(
bus,
MeowMotorManagerOptions {
heartbeat_node_id: 0x0A,
auto_identify: false,
..Default::default()
},
)?;
let node = 0x01;
let model = manager.identify(node).await?;
println!("{}", model.display_name());
manager
.initialize(node, PdoProfile::from_event_timer_ms(10)?)
.await?;
let motion = async {
manager.set_max_torque(node, 50).await?;
manager
.set_profile_limits(
node,
MeowProfileLimits {
velocity_rev_per_s: 0.2,
acceleration_rev_per_s2: 0.2,
deceleration_rev_per_s2: 0.2,
},
)
.await?;
manager
.set_mode_sdo(
node,
MeowMotorTarget::ProfileVelocity {
velocity_rev_per_s: 0.05,
},
)
.await?;
tokio::time::sleep(Duration::from_secs(1)).await;
let state = manager.status(node)?;
println!("{:?}", state.measurements);
manager
.set_target_sdo(
node,
MeowMotorTarget::ProfileVelocity {
velocity_rev_per_s: 0.0,
},
)
.await?;
tokio::time::sleep(Duration::from_secs(1)).await;
Ok::<(), hex_motor::Error>(())
}
.await;
// Attempt Disable on both success and ordinary error paths.
let disabled = manager.disable(node).await;
motion?;
disabled?;
Ok(())
}
Adjust the interface and Node-ID, then run cargo run. PdoProfile supplies PDO timing and MeowMotorManager controls the motor. The example's 50 ‰ cap is relative to raw peak torque, not a fixed N·m value, and may not move your load.
Initialization and lifecycle
Use this order:
identify(nid): reread full identity, match an exact Meow model and read0x2001.initialize(nid, profile): enter Pre-Operational, clear existing errors and confirm Disabled, configure both TPDOs and consumer heartbeat, read peak torque and MIT scale factor, enter Operational, and wait for fresh feedback from both PDOs while still Disabled.- Set torque and profile limits, then send a target matching the desired mode.
- Explicitly call
disable(nid)and check its result when finished.
| Lifecycle | Meaning / action |
|---|---|
Unknown |
Not yet identified |
UnsupportedIdentity |
Not a supported Meow identity |
Identified |
Identity and CAN configuration read; ready for initialization |
Initializing |
Initialization transaction running |
Initialized |
Initialization complete; also check online, NMT and feedback freshness |
NeedsReinit |
Left Operational or telemetry went stale; restore and initialize |
NeedsRestart |
Communication settings saved; power-cycle and identify |
Default host heartbeat is 50 ms, with a 250 ms consumer timeout. Use one bus-level heartbeat / discovery / SDO owner. When integrating into an existing owner, set broadcast_heartbeat = false and auto_identify = false; the owner must maintain heartbeat, coordinate SDO transactions and explicitly identify devices. Do not disable broadcasting without an external heartbeat while leaving the consumer watchdog enabled.
Targets and mode changes
| Rust API | Behavior |
|---|---|
set_mode_sdo(nid, target) |
Writes all target objects, writes 0x4401, then waits for fresh TPDO2 mode confirmation; supports online mode changes |
set_target_sdo(nid, target) |
Updates only the current mode's target; does not write a mode command; types must match |
set_max_torque(nid, permille) |
Writes 0x4572, range 0–1000 |
set_profile_limits(nid, limits) |
Writes 0x4581/0x4583/0x4584; values must be finite and positive |
disable(nid) |
Writes 0x4401 = 0 and confirms Disabled |
clear_error(nid) |
Clears errors and confirms Disabled, without restoring the old mode; handles rearming on retry |
status(nid) |
Returns the latest cached snapshot without an SDO sample |
subscribe_status(nid) |
Returns a watch receiver of latest state |
subscribe_status_stream(nid) |
Subscribes to the publication stream; handle overflow / dropped-sample notices |
Construct PP targets with SignedQ8_24::from_revolutions(value)?; do not write a Float32 bit pattern to 0x457A. PV uses MeowMotorTarget::ProfileVelocity { velocity_rev_per_s }, and Torque uses Torque { torque_permille }. The GUI's Send Target uses set_mode_sdo, so it also activates the selected mode.
MIT units and scale
Uncompressed MIT uses MeowMotorTarget::Mit(MeowMitTarget { ... }). Position and velocity are Rev / Rev/s, torque_nm is N·m in the raw command domain, kp / kd are raw U16 values and kp_kd_limit_permille is 0–1000. Read F = 0x4102:07 and require a finite positive value:
position_rev = position_rad / (2π)
velocity_rev_per_s = velocity_rad_per_s / (2π)
kp_raw = round(kp_Nm_per_rad × 2π / F)
kd_raw = round(kd_Nm_s_per_rad × 2π / F)
Encoded gains must fit 0–65535. 0x4102:06 limits the torque calculated by each Kp / Kd term, not the gains themselves. The GUI performs this conversion; direct library callers must do it themselves.
Factory torque factor
read_factory_calibration(nid) reads 0x4001. The driver decodes calibration without applying it. The GUI additionally applies its torque factor k:
raw_torque_command = physical_torque_command × k
physical_torque_feedback = (feedback_permille / 1000 × peak_torque_Nm) / k
Convert torque commands to raw permille using the peak torque at 0x4576 and enforce ±1000. The GUI uses k = 1 for an uncalibrated motor and rejects target requests if calibration could not be read. It applies the factor to Torque targets and MIT feedforward torque, without rescaling position, velocity or Kp / Kd, and does not automatically apply friction compensation.
Maximum torque 0x4572 remains permille of raw peak and is not multiplied by k. Its approximate physical limit is permille / 1000 × peak / k. Do not apply calibration again to values already converted by the GUI.
TPDO feedback layout
Initialization configures these default little-endian layouts. The IDs shown belong to this recipe; PDO COB-IDs are configurable.
| PDO | COB-ID | Length | Period |
|---|---|---|---|
| Device TPDO1 | 0x180 + nid |
8 bytes | 1–100 ms, selected through PdoProfile |
| Device TPDO2 | 0x280 + nid |
7 bytes, without padding to 8 | Fixed 20 ms (50 Hz) |
| TPDO1 bytes | Object | Type / meaning |
|---|---|---|
| 0–3 | 0x4564:00 |
I32 Q8.24 position, Rev |
| 4–5 | 0x4713:00 |
U16 timestamp, µs |
| 6–7 | 0x4577:00 |
I16 torque, ‰ |
| TPDO2 bytes | Object | Type / meaning |
|---|---|---|
| 0 | 0x4402:00 |
U8 mode / error state |
| 1–2 | 0x453F:00 |
U16 detailed error |
| 3–4 | 0x4104:01 |
I16 driver temperature, 0.1 °C |
| 5–6 | 0x4104:02 |
I16 motor temperature, 0.1 °C |
Follow the BRS rule, not length alone, to select frame type. TPDO1 has no direct velocity field: the driver estimates it from position and timestamps. Read 0x456C for separate SDO velocity feedback.
Use U16 modular timestamp differences and I32 wrapping position differences. Accumulation requires continuous sampling and less than 128 revolutions of actual motion between samples. Check position_accumulation_valid / position_accumulation_segment and do not join trajectories across disconnects or gaps blindly. A publication stream does not guarantee lossless hard real-time acquisition.
Shared PDOs and compressed MIT
Normal GUI initialization configures feedback and SDO targets; it does not create a shared compressed-MIT command loop. Controller integrations can explicitly use:
MeowMotorInitializeOptions::shared_host_pdo = Some(SharedHostPdoConfig { ... }), assigning one slot to each of 1–8 motors.compressed_mit_rpdo1_recipe(slot, total, host_nid): motor RPDO1 receives host TPDO1, with 8 bytes per motor, mapping its0x4103:02/:03and filling other slots with0x3000:03.mode_rpdo2_recipe(slot, total, host_nid): motor RPDO2 receives host TPDO2, with one byte per motor, mapping its0x4401and filling other slots with0x3000:01.CompressedMitMappingfor the five quantization ranges, andCompressedMitTarget/pack_shared_framefor encoding. Match the ranges configured in0x4103; another vendor's MIT frame is not interchangeable.
Host TPDO1 / TPDO2 use 0x180 + host_nid / 0x280 + host_nid, or 0x18A / 0x28A for host 0x0A. Four motors use a 32-byte MIT payload and a 4-byte mode payload. For two motors:
Host TPDO1: [ motor 0: 8 bytes ][ motor 1: 8 bytes ]
Motor 0 RPDO1: 0x4103:02 | 0x4103:03 | filler32 | filler32
Motor 1 RPDO1: filler32 | filler32 | 0x4103:02 | 0x4103:03
Host TPDO2: [ motor 0 mode: 1 byte ][ motor 1 mode: 1 byte ]
Configuring RPDOs does not start a host transmitter. Send safe target lanes continuously before explicitly activating MIT, and maintain heartbeat and stop handling. Shared mode frames follow 0x4401 change-of-value semantics too. See the 0.3.2 implementation for encoding and recipes.
Persistence
apply_can_settings(nid, settings) requires exact identity, online state, NMT Pre-Operational / Stopped and lifecycle Identified / NeedsReinit. After writing and successful readback, it saves with 0x1010:04 = save and enters NeedsRestart. Power-cycle before identifying and initializing again.
Ordinary initialization does not persist PDO mapping. For explicit commissioning, pdo_communication_save_write() provides 0x1010:02 = save. Position preset uses the separate 0x3001 commit described in the GUI guide.