Multi-Device Management

Learn how to manage multiple PoKeys devices simultaneously for complex automation systems

What You'll Learn

  • • Discover and connect to multiple PoKeys devices
  • • Coordinate operations across multiple devices
  • • Handle device-specific configurations and capabilities
  • • Implement robust error handling for multi-device systems

Basic Multi-Device Discovery

use pokeys_lib::*;
use std::collections::HashMap;

fn main() -> Result<()> {
    // Discover all connected devices
    let usb_count = enumerate_usb_devices()?;
    let network_devices = discover_network_devices(5000)?; // 5 second timeout

    println!("Found {} USB devices", usb_count);
    println!("Found {} network devices", network_devices.len());

    // Connect to all USB devices
    let mut devices = HashMap::new();

    for device_id in 0..usb_count {
        match connect_to_device(device_id) {
            Ok(device) => {
                let info = device.get_device_info()?;
                println!("Connected to USB device {}: {}", device_id, info.device_name);
                devices.insert(format!("usb_{}", device_id), device);
            },
            Err(e) => println!("Failed to connect to USB device {}: {}", device_id, e),
        }
    }

    // Connect to network devices
    for (i, net_device) in network_devices.iter().enumerate() {
        match connect_to_network_device(&net_device.ip_address, net_device.port) {
            Ok(device) => {
                let info = device.get_device_info()?;
                println!("Connected to network device {}: {}", net_device.ip_address, info.device_name);
                devices.insert(format!("net_{}", i), device);
            },
            Err(e) => println!("Failed to connect to {}: {}", net_device.ip_address, e),
        }
    }

    println!("Successfully connected to {} devices total", devices.len());

    // Perform operations on all devices
    for (name, device) in &mut devices {
        match configure_device_basics(device) {
            Ok(_) => println!("Configured device: {}", name),
            Err(e) => println!("Failed to configure {}: {}", name, e),
        }
    }

    Ok(())
}

fn configure_device_basics(device: &mut PoKeysDevice) -> Result<()> {
    // Set pin 1 as digital output on all devices
    device.set_pin_function(1, PinFunction::DigitalOutput)?;
    device.set_digital_output(1, false)?;
    Ok(())
}

Advanced: Coordinated Device Control

use pokeys_lib::*;
use std::collections::HashMap;
use std::thread;
use std::time:{Duration, Instant};

struct DeviceManager {
    devices: HashMap<String, PoKeysDevice>,
    device_roles: HashMap<String, DeviceRole>,
}

#[derive(Clone)]
enum DeviceRole {
    InputController,    // Reads sensors and inputs
    OutputController,   // Controls actuators and outputs
    CommunicationHub,   // Handles SPI/I2C communication
}

impl DeviceManager {
    fn new() -> Result<Self> {
        let mut manager = DeviceManager {
            devices: HashMap::new(),
            device_roles: HashMap::new(),
        };

        manager.discover_and_assign_devices()?;
        Ok(manager)
    }

    fn discover_and_assign_devices(&mut self) -> Result<()> {
        let usb_count = enumerate_usb_devices()?;

        for device_id in 0..usb_count {
            let device = connect_to_device(device_id)?;
            let info = device.get_device_info()?;
            let device_name = format!("device_{}", device_id);

            // Assign roles based on device capabilities or serial number
            let role = match device_id {
                0 => DeviceRole::InputController,
                1 => DeviceRole::OutputController,
                _ => DeviceRole::CommunicationHub,
            };

            println!("Assigned {} as {:?}", device_name, role);

            self.devices.insert(device_name.clone(), device);
            self.device_roles.insert(device_name, role);
        }

        Ok(())
    }

    fn configure_all_devices(&mut self) -> Result<()> {
        for (name, device) in &mut self.devices {
            let role = self.device_roles.get(name).unwrap();

            match role {
                DeviceRole::InputController => self.configure_input_device(device)?,
                DeviceRole::OutputController => self.configure_output_device(device)?,
                DeviceRole::CommunicationHub => self.configure_communication_device(device)?,
            }

            println!("Configured {} for role {:?}", name, role);
        }

        Ok(())
    }

    fn configure_input_device(&self, device: &mut PoKeysDevice) -> Result<()> {
        // Configure pins for input sensing
        for pin in 1..=10 {
            device.set_pin_function(pin, PinFunction::DigitalInput)?;
        }

        // Configure analog inputs
        for pin in 41..=47 {
            device.set_pin_function(pin, PinFunction::AnalogInput)?;
        }

        // Configure encoders
        device.configure_encoder(0, 11, 12, EncoderOptions::with_4x_sampling())?;

        Ok(())
    }

    fn configure_output_device(&self, device: &mut PoKeysDevice) -> Result<()> {
        // Configure digital outputs
        for pin in 1..=20 {
            device.set_pin_function(pin, PinFunction::DigitalOutput)?;
            device.set_digital_output(pin, false)?;
        }

        // Configure PWM outputs
        for channel in 0..6 {
            let pin = 21 + channel;
            device.set_pin_function(pin, PinFunction::PwmOutput)?;
            device.set_pwm_duty_cycle(channel as u8, 0.0)?;
        }

        Ok(())
    }

    fn configure_communication_device(&self, device: &mut PoKeysDevice) -> Result<()> {
        // Configure SPI
        let spi_config = SpiConfiguration {
            clock_frequency: 1_000_000,
            mode: SpiMode::Mode0,
            bit_order: SpiBitOrder::MsbFirst,
        };
        device.configure_spi(spi_config)?;

        // Configure I2C
        device.configure_i2c(100000)?; // 100kHz

        // Configure CS pins for SPI devices
        for pin in 10..=15 {
            device.set_pin_function(pin, PinFunction::DigitalOutput)?;
            device.set_digital_output(pin, true)?; // CS idle high
        }

        Ok(())
    }

    fn run_coordinated_operation(&mut self) -> Result<()> {
        println!("Starting coordinated multi-device operation...");

        let start_time = Instant::now();
        let mut cycle_count = 0;

        loop {
            // Read inputs from input controller
            if let Some(input_device) = self.get_device_by_role(DeviceRole::InputController) {
                let sensor_values = self.read_all_sensors(input_device)?;

                // Process sensor data and determine outputs
                let output_commands = self.process_sensor_data(sensor_values);

                // Send commands to output controller
                if let Some(output_device) = self.get_device_by_role(DeviceRole::OutputController) {
                    self.execute_output_commands(output_device, output_commands)?;
                }

                // Handle communication tasks
                if let Some(comm_device) = self.get_device_by_role(DeviceRole::CommunicationHub) {
                    self.handle_communication_tasks(comm_device)?;
                }
            }

            cycle_count += 1;
            if cycle_count % 100 == 0 {
                let elapsed = start_time.elapsed();
                println!("Completed {} cycles in {:.2}s", cycle_count, elapsed.as_secs_f64());
            }

            thread::sleep(Duration::from_millis(10)); // 100Hz update rate
        }
    }

    fn get_device_by_role(&mut self, role: DeviceRole) -> Option<&mut PoKeysDevice> {
        for (name, device_role) in &self.device_roles {
            if matches!(device_role, role) {
                return self.devices.get_mut(name);
            }
        }
        None
    }

    fn read_all_sensors(&mut self, device: &mut PoKeysDevice) -> Result<SensorData> {
        let digital_inputs = device.get_digital_inputs(1, 10)?;
        let analog_values = device.get_analog_inputs(41, 47)?;
        let encoder_position = device.get_encoder_value(0)?;

        Ok(SensorData {
            digital_inputs,
            analog_values,
            encoder_position,
        })
    }

    fn process_sensor_data(&self, data: SensorData) -> OutputCommands {
        // Simple example: control outputs based on inputs
        let mut commands = OutputCommands::default();

        // Mirror digital inputs to first 10 outputs
        commands.digital_outputs = data.digital_inputs.clone();

        // Convert analog input to PWM duty cycle
        if !data.analog_values.is_empty() {
            let duty_cycle = data.analog_values[0] / 4095.0; // Normalize to 0-1
            commands.pwm_values.push((0, duty_cycle));
        }

        commands
    }

    fn execute_output_commands(&mut self, device: &mut PoKeysDevice, commands: OutputCommands) -> Result<()> {
        // Set digital outputs
        for (pin, state) in commands.digital_outputs.iter().enumerate() {
            device.set_digital_output((pin + 1) as u8, *state)?;
        }

        // Set PWM outputs
        for (channel, duty_cycle) in commands.pwm_values {
            device.set_pwm_duty_cycle(channel, duty_cycle)?;
        }

        Ok(())
    }

    fn handle_communication_tasks(&mut self, device: &mut PoKeysDevice) -> Result<()> {
        // Example: Read temperature sensor via I2C
        match device.i2c_read(0x48, 2) { // Common temp sensor address
            Ok(data) => {
                let temp = ((data[0] as u16) << 8 | data[1] as u16) as f32 / 256.0;
                if temp > 50.0 { // Temperature threshold
                    println!("High temperature detected: {:.1}°C", temp);
                }
            },
            Err(_) => {} // Sensor not connected, ignore
        }

        Ok(())
    }
}

#[derive(Default)]
struct SensorData {
    digital_inputs: Vec<bool>,
    analog_values: Vec<f32>,
    encoder_position: i32,
}

#[derive(Default)]
struct OutputCommands {
    digital_outputs: Vec<bool>,
    pwm_values: Vec<(u8, f32)>,
}

fn main() -> Result<()> {
    let mut manager = DeviceManager::new()?;
    manager.configure_all_devices()?;
    manager.run_coordinated_operation()?;

    Ok(())
}

Robust Error Handling

use pokeys_lib::*;
use std::collections::HashMap;
use std::time:{Duration, Instant};

struct RobustDeviceManager {
    devices: HashMap<String, Option<PoKeysDevice>>,
    last_successful_contact: HashMap<String, Instant>,
    reconnect_attempts: HashMap<String, u32>,
}

impl RobustDeviceManager {
    fn new() -> Self {
        RobustDeviceManager {
            devices: HashMap::new(),
            last_successful_contact: HashMap::new(),
            reconnect_attempts: HashMap::new(),
        }
    }

    fn discover_devices(&mut self) -> Result<()> {
        // Try USB devices
        match enumerate_usb_devices() {
            Ok(count) => {
                for device_id in 0..count {
                    let device_name = format!("usb_{}", device_id);
                    match connect_to_device(device_id) {
                        Ok(device) => {
                            self.devices.insert(device_name.clone(), Some(device));
                            self.last_successful_contact.insert(device_name.clone(), Instant::now());
                            self.reconnect_attempts.insert(device_name, 0);
                            println!("Connected to USB device {}", device_id);
                        },
                        Err(e) => {
                            println!("Failed to connect to USB device {}: {}", device_id, e);
                            self.devices.insert(device_name, None);
                        }
                    }
                }
            },
            Err(e) => println!("Failed to enumerate USB devices: {}", e),
        }

        // Try network devices
        match discover_network_devices(3000) {
            Ok(network_devices) => {
                for (i, net_device) in network_devices.iter().enumerate() {
                    let device_name = format!("net_{}", i);
                    match connect_to_network_device(&net_device.ip_address, net_device.port) {
                        Ok(device) => {
                            self.devices.insert(device_name.clone(), Some(device));
                            self.last_successful_contact.insert(device_name.clone(), Instant::now());
                            self.reconnect_attempts.insert(device_name, 0);
                            println!("Connected to network device {}", net_device.ip_address);
                        },
                        Err(e) => {
                            println!("Failed to connect to {}: {}", net_device.ip_address, e);
                            self.devices.insert(device_name, None);
                        }
                    }
                }
            },
            Err(e) => println!("Failed to discover network devices: {}", e),
        }

        Ok(())
    }

    fn execute_with_retry<F, R>(&mut self, device_name: &str, operation: F) -> Option<R>
    where
        F: Fn(&mut PoKeysDevice) -> Result<R>,
    {
        if let Some(device_opt) = self.devices.get_mut(device_name) {
            if let Some(device) = device_opt {
                match operation(device) {
                    Ok(result) => {
                        // Success - update last contact time and reset retry count
                        self.last_successful_contact.insert(device_name.to_string(), Instant::now());
                        self.reconnect_attempts.insert(device_name.to_string(), 0);
                        return Some(result);
                    },
                    Err(e) => {
                        println!("Operation failed on {}: {}", device_name, e);

                        // Mark device as disconnected
                        *device_opt = None;

                        // Attempt reconnection
                        self.attempt_reconnection(device_name);
                    }
                }
            } else {
                // Device is disconnected, try to reconnect
                self.attempt_reconnection(device_name);
            }
        }

        None
    }

    fn attempt_reconnection(&mut self, device_name: &str) {
        let attempts = self.reconnect_attempts.get(device_name).unwrap_or(&0);

        if *attempts < 5 { // Max 5 reconnection attempts
            println!("Attempting to reconnect to {} (attempt {})", device_name, attempts + 1);

            // Try to reconnect based on device type
            let reconnected = if device_name.starts_with("usb_") {
                if let Ok(device_id) = device_name[4..].parse::<u32>() {
                    connect_to_device(device_id).ok()
                } else {
                    None
                }
            } else {
                // For network devices, you'd need to store IP/port info
                None
            };

            if let Some(device) = reconnected {
                println!("Successfully reconnected to {}", device_name);
                self.devices.insert(device_name.to_string(), Some(device));
                self.last_successful_contact.insert(device_name.to_string(), Instant::now());
                self.reconnect_attempts.insert(device_name.to_string(), 0);
            } else {
                self.reconnect_attempts.insert(device_name.to_string(), attempts + 1);
            }
        }
    }

    fn get_system_status(&self) -> SystemStatus {
        let total_devices = self.devices.len();
        let connected_devices = self.devices.values().filter(|d| d.is_some()).count();
        let disconnected_devices = total_devices - connected_devices;

        SystemStatus {
            total_devices,
            connected_devices,
            disconnected_devices,
            devices_with_errors: self.reconnect_attempts.values().filter(|&&attempts| attempts > 0).count(),
        }
    }

    fn run_monitoring_loop(&mut self) -> Result<()> {
        loop {
            // Perform operations on all available devices
            for device_name in self.devices.keys().cloned().collect::<Vec<_>>() {
                // Example: Read pin 1 status
                self.execute_with_retry(&device_name, |device| {
                    device.get_digital_input(1)
                });

                // Example: Toggle pin 2
                self.execute_with_retry(&device_name, |device| {
                    let current = device.get_digital_output(2)?;
                    device.set_digital_output(2, !current)
                });
            }

            // Print system status every 10 seconds
            let status = self.get_system_status();
            println!("System Status: {}/{} devices connected, {} errors",
                     status.connected_devices, status.total_devices, status.devices_with_errors);

            std::thread::sleep(Duration::from_millis(100));
        }
    }
}

struct SystemStatus {
    total_devices: usize,
    connected_devices: usize,
    disconnected_devices: usize,
    devices_with_errors: usize,
}

fn main() -> Result<()> {
    let mut manager = RobustDeviceManager::new();
    manager.discover_devices()?;
    manager.run_monitoring_loop()?;

    Ok(())
}

Best Practices

Device Management

  • • Assign specific roles to each device for clear separation of concerns
  • • Implement robust error handling and automatic reconnection
  • • Monitor device health and connection status continuously
  • • Use device serial numbers or IP addresses for consistent identification

Performance Optimization

  • • Use bulk operations when possible to reduce communication overhead
  • • Implement appropriate update rates for different device functions
  • • Cache device capabilities and configurations to avoid repeated queries
  • • Consider using separate threads for each device to prevent blocking

Safety Considerations

  • • Implement failsafe behavior when devices become unavailable
  • • Validate device capabilities before assigning critical functions
  • • Use watchdog timers to detect communication failures
  • • Implement emergency stop functionality across all devices