Sending Commands from the Control Program
Overview
The CommandClient (provided by autocore-std) lets your control program send requests to external modules (Modbus, EtherCAT, camera, etc.) and receive responses — all without blocking the scan cycle.
Key characteristics:
- Non-blocking:
send()queues a message immediately; responses are collected later. - Transaction-based: Each request gets a unique ID so you can match responses.
- Multi-consumer: Multiple subsystems can share one
CommandClient, each tracking its own requests.
Control Program autocore-server External Module
│ │ │
│ send("labelit.inspect", {}) │ │
│ ──────────────────────────────►│ route to labelit via TCP │
│ │ ────────────────────────────►│
│ │ │
│ (scan cycles continue) │ │
│ │ Response (transaction_id) │
│ │ ◄────────────────────────────│
│ take_response(tid) │ │
│ ◄───────────────────────────── │ │
Sending a Request
Call send() with a topic and JSON payload. It returns a transaction_id:
#![allow(unused)]
fn main() {
use serde_json::json;
let tid = ctx.client.send("labelit.inspect_full", json!({
"exposure_ms": 50,
"threshold": 0.8
}));
// tid is a u32 you can use to match the response later
}
The topic format is module_name.command:
| Topic | Module | Command |
|---|---|---|
labelit.status | labelit | status |
modbus.read_holding | modbus | read_holding |
python.run_script | python | run_script |
Polling for Responses
The framework calls poll() before each process_tick, so responses are already buffered. Use take_response(tid) to retrieve yours:
#![allow(unused)]
fn main() {
if let Some(response) = ctx.client.take_response(my_tid) {
if response.success {
log::info!("Result: {}", response.data);
} else {
log::error!("Failed: {}", response.error_message);
}
}
}
Handling Timeouts
Clean up requests that have been pending too long:
#![allow(unused)]
fn main() {
use std::time::Duration;
let stale = ctx.client.drain_stale(Duration::from_secs(10));
for tid in &stale {
log::warn!("Request {} timed out", tid);
}
}
Full Example: Calling an External Vision Module
This example sends an inspect_full command to a camera module when a trigger fires, then uses the result to position a robot:
#![allow(unused)]
fn main() {
use autocore_std::{ControlProgram, TickContext};
use autocore_std::fb::RTrig;
use serde_json::json;
use std::time::Duration;
use crate::gm::GlobalMemory;
pub struct MyControlProgram {
trigger: RTrig,
inspect_tid: Option<u32>,
}
impl MyControlProgram {
pub fn new() -> Self {
Self {
trigger: RTrig::new(),
inspect_tid: None,
}
}
}
impl ControlProgram for MyControlProgram {
type Memory = GlobalMemory;
fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
// 1. On rising edge of the inspect trigger, send the command
if self.trigger.call(ctx.gm.start_inspect) && self.inspect_tid.is_none() {
let tid = ctx.client.send("labelit.inspect_full", json!({}));
self.inspect_tid = Some(tid);
log::info!("Sent inspect command (tid={})", tid);
}
// 2. Check for our response
if let Some(tid) = self.inspect_tid {
if let Some(response) = ctx.client.take_response(tid) {
self.inspect_tid = None;
if response.success {
let placement = &response.data["placement"];
ctx.gm.placement_x = placement["robot_x"].as_f64().unwrap_or(0.0) as f32;
ctx.gm.placement_y = placement["robot_y"].as_f64().unwrap_or(0.0) as f32;
ctx.gm.placement_c = placement["robot_c"].as_f64().unwrap_or(0.0) as f32;
ctx.gm.placement_valid = true;
log::info!("Placement received: ({:.2}, {:.2}, {:.1} deg)",
ctx.gm.placement_x, ctx.gm.placement_y, ctx.gm.placement_c);
} else {
log::error!("Inspect failed: {}", response.error_message);
ctx.gm.placement_valid = false;
}
}
}
// 3. Clean up stale requests
let stale = ctx.client.drain_stale(Duration::from_secs(10));
for tid in stale {
if Some(tid) == self.inspect_tid {
log::warn!("Inspect request timed out");
self.inspect_tid = None;
ctx.gm.placement_valid = false;
}
}
}
}
}