{"title":"Tutorial 5: Hardware & Real-Time (C++)","description":"Connect real hardware with RT scheduling, CPU pinning, and watchdog protection","slug":"tutorials/05-hardware-rt-cpp","content":"# Tutorial 5: Hardware & Real-Time (C++)\n\nConnect real sensors and actuators to HORUS with proper real-time scheduling. This tutorial builds a motor controller that reads encoder feedback and drives a motor with SCHED_FIFO priority.\n\n## What You'll Learn\n\n- Opening serial ports from a Node\n- RT scheduling: `budget()`, `deadline()`, `pin_core()`, `priority()`\n- Watchdog for detecting frozen hardware\n- `enter_safe_state()` for actuator safety\n- CPU governor and kernel requirements\n\n## Prerequisites\n\n- Completed [Tutorial 2: Motor Controller](/tutorials/02-motor-controller-cpp)\n- A Linux machine (RT features require Linux)\n- Optional: USB serial device for testing\n\n## The Hardware Pattern\n\nEvery hardware driver follows the same pattern:\n\n```\ninit():    open device, configure, verify connection\ntick():    read sensor OR write actuator (never both blocking)\nsafe():    zero actuators, disable outputs\nshutdown(): close device, release resources\n```\n\n## Complete Code: RT Motor Driver\n\n```cpp\n#include <horus/horus.hpp>\n#include <fcntl.h>\n#include <unistd.h>\n#include <termios.h>\n#include <cstdio>\n#include <cstring>\nusing namespace horus::literals;\n\nclass MotorDriver : public horus::Node {\npublic:\n    MotorDriver(const char* port, int baudrate)\n        : Node(\"motor_driver\"), port_(port), baudrate_(baudrate)\n    {\n        cmd_sub_   = subscribe<horus::msg::CmdVel>(\"motor.cmd\");\n        state_pub_ = advertise<horus::msg::CmdVel>(\"motor.state\");\n    }\n\n    void init() override {\n        fd_ = open(port_, O_RDWR | O_NOCTTY | O_NONBLOCK);\n        if (fd_ < 0) {\n            horus::log::error(\"motor\", \"Failed to open serial port\");\n            horus::blackbox::record(\"motor\", \"Serial port open failed\");\n            return;\n        }\n\n        // Configure serial port\n        struct termios tty{};\n        tcgetattr(fd_, &tty);\n        cfsetispeed(&tty, baudrate_);\n        cfsetospeed(&tty, baudrate_);\n        tty.c_cflag |= (CLOCAL | CREAD);\n        tty.c_cflag &= ~PARENB;\n        tty.c_cflag &= ~CSTOPB;\n        tty.c_cflag &= ~CSIZE;\n        tty.c_cflag |= CS8;\n        tcsetattr(fd_, TCSANOW, &tty);\n\n        horus::log::info(\"motor\", \"Serial port opened, motor ready\");\n    }\n\n    void tick() override {\n        if (fd_ < 0) return;\n\n        // Read command\n        auto cmd = cmd_sub_->recv();\n        if (cmd) {\n            float duty = cmd->get()->linear;\n            last_cmd_ = duty;\n\n            // Send PWM command to motor controller\n            // Protocol: \"M<duty>\\n\" where duty is -100 to 100\n            char buf[32];\n            int n = std::snprintf(buf, sizeof(buf), \"M%.0f\\n\", duty * 100.0f);\n            write(fd_, buf, n);\n        }\n\n        // Read encoder feedback (non-blocking)\n        char rbuf[64];\n        int n = read(fd_, rbuf, sizeof(rbuf) - 1);\n        if (n > 0) {\n            rbuf[n] = '\\0';\n            float rpm = 0;\n            if (std::sscanf(rbuf, \"E%f\", &rpm) == 1) {\n                horus::msg::CmdVel state{};\n                state.linear = rpm;\n                state.angular = last_cmd_;\n                state_pub_->send(state);\n                watchdog_fed_ = true;\n            }\n        }\n\n        // Watchdog: if no encoder response for 100 ticks (1s), alarm\n        if (watchdog_fed_) {\n            watchdog_counter_ = 0;\n            watchdog_fed_ = false;\n        } else {\n            watchdog_counter_++;\n            if (watchdog_counter_ > 100 && !watchdog_alarmed_) {\n                horus::log::error(\"motor\", \"Encoder watchdog timeout\");\n                horus::blackbox::record(\"motor\", \"Encoder timeout > 1s\");\n                watchdog_alarmed_ = true;\n                send_zero();\n            }\n        }\n    }\n\n    void enter_safe_state() override {\n        send_zero();\n        horus::blackbox::record(\"motor\", \"Safe state: motor zeroed\");\n    }\n\nprivate:\n    void send_zero() {\n        if (fd_ >= 0) {\n            write(fd_, \"M0\\n\", 3);\n        }\n        horus::msg::CmdVel stop{};\n        state_pub_->send(stop);\n    }\n\n    const char* port_;\n    int baudrate_;\n    int fd_ = -1;\n    float last_cmd_ = 0;\n    int watchdog_counter_ = 0;\n    bool watchdog_fed_ = false;\n    bool watchdog_alarmed_ = false;\n    horus::Subscriber<horus::msg::CmdVel>* cmd_sub_;\n    horus::Publisher<horus::msg::CmdVel>*  state_pub_;\n};\n\nint main() {\n    horus::Scheduler sched;\n    sched.tick_rate(100_hz)\n         .name(\"rt_motor\")\n         .prefer_rt();    // Use SCHED_FIFO if available\n\n    MotorDriver motor(\"/dev/ttyUSB0\", B115200);\n    sched.add(motor)\n        .order(0)                    // highest priority\n        .budget(2_ms)                // must complete in 2ms\n        .deadline(5_ms)              // absolute deadline 5ms\n        .on_miss(horus::Miss::SafeMode)  // stop motor if overrun\n        .pin_core(2)                 // pin to CPU core 2\n        .priority(90)                // SCHED_FIFO priority 90\n        .watchdog(1_s)               // scheduler-level watchdog\n        .build();\n\n    sched.spin();\n}\n```\n\n## RT Configuration Explained\n\n```cpp\nsched.prefer_rt();         // Try SCHED_FIFO; degrade gracefully if unavailable\n// vs\nsched.require_rt();        // FAIL if SCHED_FIFO not available (production systems)\n```\n\n| Setting | Purpose | Typical Value |\n|---------|---------|---------------|\n| `budget(2_ms)` | Max time per tick | 50-80% of period |\n| `deadline(5_ms)` | Absolute tick deadline | 90-95% of period |\n| `pin_core(2)` | CPU affinity | Dedicated core, not core 0 |\n| `priority(90)` | SCHED_FIFO level | 80-99 for critical, 50-79 for normal |\n| `watchdog(1_s)` | Frozen node detection | 5-10x expected tick period |\n\n## RT Kernel Setup\n\nFor full RT guarantees:\n\n```bash\n# Check current kernel\nuname -r  # Look for \"-rt\" suffix\n\n# Set CPU governor to performance\nsudo cpupower frequency-set -g performance\n\n# Grant RT privileges without root\nsudo setcap cap_sys_nice+ep ./my_robot\n\n# Or run with elevated privileges\nsudo nice -n -20 ./my_robot\n```\n\nWithout an RT kernel, HORUS still works — `prefer_rt()` logs warnings but continues with best-effort scheduling.\n\n## Key Takeaways\n\n- `init()` opens hardware, `tick()` reads/writes, `enter_safe_state()` zeros actuators\n- Never block in `tick()` — use non-blocking I/O (`O_NONBLOCK`)\n- Watchdog detects frozen hardware (encoder cable disconnected, motor driver crash)\n- `pin_core()` prevents OS from migrating the thread — critical for latency\n- `budget()` + `SafeMode` = automatic motor shutdown on timing overrun\n- Test without RT kernel first, add RT for production deployment","headings":["Tutorial 5: Hardware & Real-Time (C++)","What You'll Learn","Prerequisites","The Hardware Pattern","Complete Code: RT Motor Driver","RT Configuration Explained","RT Kernel Setup","Check current kernel","Set CPU governor to performance","Grant RT privileges without root","Or run with elevated privileges","Key Takeaways"],"wordCount":745}