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

Writing External Modules

When to Write a Module

Write an external module when you need to:

  • Interface with hardware that AutoCore doesn’t support natively (cameras, custom sensors, robotic controllers)
  • Run code that should operate independently of the control loop (long-running tasks, blocking SDK calls)
  • Add a service that other components can call by name (e.g., a barcode scanner service)

Module Lifecycle

Server starts
    │
    ├─ Spawns module process with --ipc-address, --module-name, --config
    │
    ├─ Module connects to IPC server
    │
    ├─ Server sends "initialize" command
    │   └─ Module calls on_initialize()
    │
    ├─ Server sends "configure_shm" (if module uses shared memory)
    │   └─ Module calls on_shm_configured()
    │
    ├─ Module handles incoming requests via handle_message()
    │   (continues until shutdown)
    │
    ├─ Server sends "finalize" command
    │   └─ Module calls on_finalize()
    │
    └─ Module process exits

Step-by-Step Module Development

Step 1: Create the Crate

cargo init my-module
cd my-module

Add dependencies to Cargo.toml:

[package]
name = "my-module"
version = "1.0.0"
edition = "2024"

[dependencies]
mechutil = "0.7"
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
serde_json = "1"
anyhow = "1"
log = "0.4"
simplelog = "0.12"

Step 2: Implement the ModuleHandler Trait

#![allow(unused)]
fn main() {
use anyhow::Result;
use async_trait::async_trait;
use mechutil::ipc::{CommandMessage, IpcClient, ModuleArgs, ModuleHandler};
use mechutil::shm::ShmMap;
use simplelog::{Config, LevelFilter, SimpleLogger};

struct MyModule {
    domain: String,
    shm: Option<ShmMap>,
}

impl MyModule {
    fn new(domain: &str) -> Self {
        Self {
            domain: domain.to_string(),
            shm: None,
        }
    }
}

#[async_trait]
impl ModuleHandler for MyModule {
    fn domain(&self) -> &str {
        &self.domain
    }

    async fn on_initialize(&mut self) -> Result<()> {
        log::info!("{} initialized", self.domain);
        Ok(())
    }

    async fn on_finalize(&mut self) -> Result<()> {
        log::info!("{} shutting down", self.domain);
        Ok(())
    }

    async fn handle_message(&mut self, msg: CommandMessage) -> CommandMessage {
        let subtopic = msg.subtopic().to_string();
        match subtopic.as_str() {
            "status" => {
                msg.into_response(serde_json::json!({
                    "ok": true,
                    "shm_connected": self.shm.is_some(),
                }))
            }
            _ => msg.into_error_response(
                &format!("Unknown command: {}", subtopic)
            ),
        }
    }

    fn shm_variable_names(&self) -> Vec<String> {
        vec![] // Return variable names if your module uses shared memory
    }

    async fn on_shm_configured(&mut self, shm_map: ShmMap) -> Result<()> {
        self.shm = Some(shm_map);
        Ok(())
    }
}
}

Step 3: Write main()

#[tokio::main]
async fn main() -> Result<()> {
    SimpleLogger::init(LevelFilter::Info, Config::default())?;

    let args = ModuleArgs::from_env()?;
    log::info!("Starting {} at {}", args.module_name, args.ipc_address);

    let handler = MyModule::new(&args.module_name);
    let client = IpcClient::connect(&args.ipc_address, handler).await?;
    client.run().await?;

    Ok(())
}

Step 4: Register the Module

Add to project.json:

{
  "modules": {
    "my_module": {
      "enabled": true,
      "config": {
        "setting1": "value1"
      }
    }
  }
}

Add to config.ini:

[modules]
my_module = /path/to/my-module/target/release/my-module

Step 5: Test

# Build the module
cargo build --release

# Restart the server (it will spawn the module automatically)
sudo systemctl restart autocore_server

# Verify the module is connected
acctl cmd my_module.status

Real-World Example: Camera Integration

The autocore-labelit module demonstrates a production-quality module pattern. It manages a Basler GigE camera for label inspection:

  • Handle/Worker split: Camera SDK calls are blocking, so they run on a dedicated OS thread. The async ModuleHandler communicates with the camera thread through channels.
  • IPC commands map to subtopics: camera_start, camera_snap, camera_shutdown, status.
  • Timeouts on every operation: Each camera operation is wrapped in tokio::time::timeout so a stuck camera cannot hang the IPC loop.
  • Graceful lifecycle: The camera worker is spawned in on_initialize() and shut down in on_finalize().