Digital Out
Hardware
AutomatePro comes with 12x digital outputs for controlling external systems, from small solenoids to LEDs or small DC motors. Two different digital outputs are available. One is set up as a high-side output (DO_H_0x), which PWMs the onboard 12V as an output. The second is a low-side sink (DO_L_0x), which sinks up to 36V. The first four low-side channels are PWM controllable, and the final two are binary on/off switches.
Connector Pinout
- Connector
- Development Breakout Board


Digital Output High-Side (DO_H_x) Specifications
There are 6x high-side digital outputs, all of which are PWM-controllable switches.
| Parameter | Value |
|---|---|
| Output Voltage (On-state) | +12V (from on-board 12V) |
| Drive Current | 500mA |
| PWM Controllable | All (DO_H_01 - DO_H_06) |
| PWM Switching Frequency | 500Hz |
| Number of Outputs | 6 |
Digital Output Low-Side (DO_L_x) Specifications
There are 6x low-side digital outputs. The first four are PWM-controllable, while the final two are binary on/off switches.
| Parameter | Value |
|---|---|
| On-state | Sinks up to 36V |
| Drive Current | 500mA |
| PWM Controllable | DO_L_01 DO_L_02 DO_L_03 DO_L_04 DO_L_05 & DO_L_06 are On/Off switches |
| PWM Switching Frequency | 15 kHz |
| Number of Outputs | 6 |
ROS API
Subscribers
| Topic | Type | Description |
|---|---|---|
/io/digital_out | automatepro_interfaces/msg/DigitalOut | Publish the desired digital output states to this topic. AutomatePro supports 12 digital output pins (6 high-side and 6 low-side). Each output pin is referred to as DIGITAL_OUT_H_XX or DIGITAL_OUT_L_XX, where H/L refers to High Side/Low Side and XX is the pin number, ranging from 01 to 06. d_out_pin_id: Data type: uint8 (Use ENUM constants defined in the message definition. DigitalOut.DIGITAL_OUT_H_XX: 01-06 DigitalOut.DIGITAL_OUT_L_XX: 01-06) duty_cycle_percent: Data type: uint8 Min: 0 in percentage % Max: 100 in percentage % DIGITAL_OUT_H_03 to DIGITAL_OUT_H_06 and DIGITAL_OUT_L_01 to DIGITAL_OUT_L_04 cap at 99; DIGITAL_OUT_L_05 and DIGITAL_OUT_L_06 switch fully on for any value above 0. Response rate: 200 ms |
Example
This example steps the duty cycle of Digital Out 01 through 0%, 50%, 100%, and 50%, one step per second, by publishing to the /io/digital_out topic.
The IO controller keeps the last command it receives, so the example sets Digital Out 01 to 0% when you stop it with Ctrl+C; a node that is killed or crashes leaves the output in its last state.
The tutorial package is available in the AutomatePro tutorials repository, and its README explains how to build and run the examples.
- Python
- C++
Source: digital_out.py
import signal
import rclpy
from rclpy.duration import Duration
from rclpy.node import Node
from rclpy.signals import SignalHandlerOptions
from automatepro_interfaces.msg import DigitalOut
class DigitalOutPublisher(Node):
def __init__(self):
super().__init__('digital_out_publisher')
self.publisher = self.create_publisher(DigitalOut, '/io/digital_out', 10)
self.timer = self.create_timer(1.0, self.timer_callback)
self.duty_cycle_sequence = [0, 50, 100, 50]
self.sequence_index = 0
def timer_callback(self):
msg = DigitalOut()
msg.d_out_pin_id = DigitalOut.DIGITAL_OUT_H_01
msg.duty_cycle_percent = self.duty_cycle_sequence[self.sequence_index]
self.publisher.publish(msg)
self.get_logger().info('Publishing: "%s"' % msg)
self.sequence_index = (self.sequence_index + 1) % len(self.duty_cycle_sequence)
def switch_off(self):
msg = DigitalOut()
msg.d_out_pin_id = DigitalOut.DIGITAL_OUT_H_01
msg.duty_cycle_percent = 0
self.publisher.publish(msg)
if self.publisher.get_subscription_count() == 0:
self.get_logger().warn(
'DIGITAL_OUT_H_01 off command not delivered: no subscriber on %s'
% self.publisher.topic_name)
return
if self.publisher.wait_for_all_acked(Duration(seconds=4)):
self.get_logger().info('Switched DIGITAL_OUT_H_01 off')
else:
self.get_logger().warn('DIGITAL_OUT_H_01 off command not acknowledged')
def main(args=None):
# The IO controller keeps the last command, so switch the output off before exiting.
rclpy.init(args=args, signal_handler_options=SignalHandlerOptions.NO)
signal.signal(signal.SIGTERM, signal.default_int_handler)
node = DigitalOutPublisher()
try:
rclpy.spin(node)
except KeyboardInterrupt:
signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGTERM, signal.SIG_IGN)
node.switch_off()
finally:
node.destroy_node()
rclpy.try_shutdown()
if __name__ == '__main__':
main()
Run the node using the following command:
ros2 run automatepro_python_tutorials digital_out_node
Source: digital_out.cpp
#include <chrono>
#include <memory>
#include <mutex>
#include <vector>
#include <rclcpp/rclcpp.hpp>
#include <automatepro_interfaces/msg/digital_out.hpp>
class DigitalOutPublisher : public rclcpp::Node
{
public:
DigitalOutPublisher()
: Node("digital_out_publisher"),
duty_cycle_sequence_{0, 50, 100, 50},
sequence_index_(0)
{
publisher_ = this->create_publisher<automatepro_interfaces::msg::DigitalOut>(
"/io/digital_out",
10);
timer_ = this->create_wall_timer(
std::chrono::seconds(1),
std::bind(&DigitalOutPublisher::timer_callback, this));
}
void switch_off()
{
std::lock_guard<std::mutex> lock(mutex_);
stopped_ = true;
timer_->cancel();
auto msg = automatepro_interfaces::msg::DigitalOut();
msg.d_out_pin_id = automatepro_interfaces::msg::DigitalOut::DIGITAL_OUT_H_01;
msg.duty_cycle_percent = 0;
publisher_->publish(msg);
if (publisher_->get_subscription_count() == 0) {
RCLCPP_WARN(
this->get_logger(), "DIGITAL_OUT_H_01 off command not delivered: no subscriber on %s",
publisher_->get_topic_name());
return;
}
if (publisher_->wait_for_all_acked(std::chrono::seconds(4))) {
RCLCPP_INFO(this->get_logger(), "Switched DIGITAL_OUT_H_01 off");
} else {
RCLCPP_WARN(this->get_logger(), "DIGITAL_OUT_H_01 off command not acknowledged");
}
}
private:
void timer_callback()
{
std::lock_guard<std::mutex> lock(mutex_);
if (stopped_) {
return;
}
auto msg = automatepro_interfaces::msg::DigitalOut();
msg.d_out_pin_id = automatepro_interfaces::msg::DigitalOut::DIGITAL_OUT_H_01;
msg.duty_cycle_percent = duty_cycle_sequence_[sequence_index_];
publisher_->publish(msg);
RCLCPP_INFO(
this->get_logger(), "Publishing: d_out_pin_id=%d, duty_cycle_percent=%d", msg.d_out_pin_id,
msg.duty_cycle_percent);
sequence_index_ = (sequence_index_ + 1) % duty_cycle_sequence_.size();
}
rclcpp::Publisher<automatepro_interfaces::msg::DigitalOut>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
std::vector<int> duty_cycle_sequence_;
size_t sequence_index_;
std::mutex mutex_;
bool stopped_{false};
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<DigitalOutPublisher>();
// The IO controller keeps the last command, so switch the output off before shutdown.
std::weak_ptr<DigitalOutPublisher> weak_node = node;
rclcpp::contexts::get_global_default_context()->add_pre_shutdown_callback(
[weak_node]() {
if (auto locked_node = weak_node.lock()) {
locked_node->switch_off();
}
});
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
Run the node using the following command:
ros2 run automatepro_cpp_tutorials digital_out_node