Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

TopicModuleCommand
labelit.statuslabelitstatus
modbus.read_holdingmodbusread_holding
python.run_scriptpythonrun_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;
            }
        }
    }
}
}