17. Microros
micro-ROS คือเวอร์ชันย่อของ ROS 2 (Robot Operating System 2) ออกแบบมาสำหรับ “ไมโครคอนโทรลเลอร์” เช่น ESP32, STM32, Arduino, Teensy, ฯลฯ ที่มีทรัพยากรจำกัด
บทนี้จะสอนการติดตั้งและ build micro-ROS Agent ผ่าน ซึ่งเป็นตัวกลาง (Bridge) เชื่อมการสื่อสารระหว่าง ROS 2 ที่อยู่บน PC ↔ microcontroller (เช่น ESP32) ที่รัน micro-ROS firmware
โดยเราจะติดตั้ง Arduino IDE 2.0 ไว้ใน Windows/Mac host machine ที่ลิ้งค์ https://www.arduino.cc/en/software/
และใน Boards Manager ให้ติดตั้ง esp32 by Espressif Systems version 3.X. ดูวิธีการทำได้จาก https://randomnerdtutorials.com/installing-esp32-arduino-ide-2-0/
และ Add .zip library ใน Arduino IDE จากลิ้งค์ https://github.com/micro-ROS/micro_ros_arduino/releases โดยเลือกเวอร์ชั่น v2.0.8-Jazzy (Source code .zip)
17.1. ขั้นตอนการติดตั้งผ่าน setup_microros.sh และขั้นตอนการเชื่อมต่อบน Ubuntu
เปิด Terminal และใช้คำสั่งสร้างไฟล์ setup_microros.sh ไว้ตำแหน่งหน้า Home
gedit setup_microros.sh
copy เนื้อหาใส่ setup_microros.sh ดังนี้
#!/bin/bash
# setup_microros.sh install and build micro-ROS agent workspace
set -euo pipefail
# ---------- install git (added) ----------
echo "Installing git..."
sudo apt update
sudo apt install -y git
# ---------- helper: retry mechanism ----------
retry() {
local max=3 delay=10 n=1
while true; do
eval "$@"
status=$?
if [[ $status -eq 0 ]]; then
return 0
fi
if [[ $n -lt $max ]]; then
echo "Attempt $n/$max failed (exit $status). Retrying in ${delay}s: $*"
sleep "$delay"
n=$((n+1))
else
echo "Command failed after $max attempts: $*"
return $status
fi
done
}
# ---------- permissions ----------
echo "Adding $USER to dialout (for serial/USB)..."
sudo usermod -a -G dialout "$USER" || true
# ---------- workspace ----------
cd ~
mkdir -p microros_ws/src
cd ~/microros_ws
# Detect ROS distro if not set
if [[ -z "${ROS_DISTRO:-}" ]]; then
if [[ -d /opt/ros ]]; then
ROS_DISTRO="$(ls /opt/ros | head -n1)"
echo "ROS_DISTRO not set. Using detected: $ROS_DISTRO"
else
echo "ROS2 not found. Please install ROS2 and/or export ROS_DISTRO before running."
exit 1
fi
fi
# ---------- clone micro-ROS setup ----------
if [[ ! -d src/micro_ros_setup ]]; then
git clone -b "$ROS_DISTRO" https://github.com/micro-ROS/micro_ros_setup.git src/micro_ros_setup
fi
# ---------- dependencies ----------
sudo apt update
sudo rosdep init 2>/dev/null || true
rosdep update
rosdep install --from-paths src --ignore-src -y
sudo apt-get install -y python3-pip
# ---------- build & source safely ----------
colcon build
if [ -f install/local_setup.bash ]; then
set +u
source install/local_setup.bash
set -u
fi
# ---------- build micro-ROS agent ----------
retry "ros2 run micro_ros_setup create_agent_ws.sh"
retry "ros2 run micro_ros_setup build_agent.sh"
if [ -f install/local_setup.bash ]; then
set +u
source install/local_setup.bash
set -u
fi
# ---------- persist sourcing ----------
if ! grep -Fxq "source ~/microros_ws/install/local_setup.bash" ~/.bashrc; then
echo "source ~/microros_ws/install/local_setup.bash" >> ~/.bashrc
echo "Added workspace sourcing to ~/.bashrc"
else
echo "Workspace sourcing already in ~/.bashrc"
fi
echo
echo "micro-ROS agent setup complete."
echo "Log out or reboot for 'dialout' group membership to take effect."
ใช้คำสั่งเพื่อเปิดสิทธิ์การเข้าถึงไฟล์
chmod +x setup_microros.sh
รันสคริปต์เพื่อติดตั้ง
./setup_microros.sh
รีบูท Ubuntu
sudo reboot
กำหนดสิทธิ์ให้กับพอต
sudo chmod 666 /dev/ttyUSB0
เปิดไฟล์ .bashrc และใส่ # ไว้ข้างหน้า export ROS_DOMAIN_ID=30
gedit ~/.bashrc
source ~/.bashrc ทุกหน้าต่าง หรือเปิด terminal แล้วเปิดใหม่
ต่อ ESP32 ที่อัพโค้ด micro ros แล้วเข้า vm
รัน dmesg | grep tty เพื่อดู port ของ ESP32 (มักจะได้ชื่ออุปกรณ์เป็น /dev/ttyUSB0 หรือ /dev/ttyACM0)
sudo dmesg | grep tty
รัน micro-ROS Agent เพื่อต่อกับ ESP32
ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0
กดปุ่ม BOOT บนบอร์ด ESP32
สังเกตข้อความการเชื่อมต่อ ที่หน้าต่างของ Agent จะขึ้น log การเชื่อมต่อ ดังภาพตัวอย่าง
17.2. ตัวอย่างโค้ด micro-ROS
17.2.1. Control LED
// ESP32 with micROS : Control LED via ROS2 Topics (Improved Version)
#include <micro_ros_arduino.h>
#include <stdio.h>
#include <rcl/rcl.h>
#include <rcl/error_handling.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <std_msgs/msg/bool.h>
#include <std_msgs/msg/string.h>
#define LED_PIN 4 // LED Pin (connected to R 220Ω then to GND)
#define LED_BUILTIN 13 // Built-in LED for some ESP32 boards
#define RCCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){error_loop();}}
#define RCSOFTCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){}}
rcl_subscription_t subscriber_led;
rcl_publisher_t publisher_status;
std_msgs__msg__Bool led_msg;
std_msgs__msg__String status_msg;
rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;
void error_loop(){
while(1){
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
delay(100);
}
}
// Callback for receiving Bool commands (true/false)
void led_callback(const void * msgin)
{
const std_msgs__msg__Bool * msg = (const std_msgs__msg__Bool *)msgin;
bool led_state = msg->data;
digitalWrite(LED_PIN, led_state ? HIGH : LOW);
// Prepare and publish the status update
sprintf(status_msg.data.data, led_state ? "LED is ON" : "LED is OFF");
status_msg.data.size = strlen(status_msg.data.data);
RCSOFTCHECK(rcl_publish(&publisher_status, &status_msg, NULL));
}
void setup() {
Serial.begin(115200);
// Set up WiFi transport for micro-ROS
set_microros_transports();
pinMode(LED_PIN, OUTPUT);
pinMode(LED_BUILTIN, OUTPUT);
delay(2000);
allocator = rcl_get_default_allocator();
// Create init_options
RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));
// Create node
RCCHECK(rclc_node_init_default(&node, "esp32_led_controller", "", &support));
// Create subscriber for Bool messages
RCCHECK(rclc_subscription_init_default(
&subscriber_led,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Bool),
"led_control"));
// Create publisher for status messages
RCCHECK(rclc_publisher_init_default(
&publisher_status,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, String),
"led_status"));
// Prepare status message buffer
status_msg.data.data = (char*) malloc(100 * sizeof(char));
status_msg.data.size = 0;
status_msg.data.capacity = 100;
// Create executor with 1 subscription
RCCHECK(rclc_executor_init(&executor, &support.context, 1, &allocator));
RCCHECK(rclc_executor_add_subscription(&executor, &subscriber_led, &led_msg, &led_callback, ON_NEW_DATA));
// Send an initial status message
sprintf(status_msg.data.data, "ESP32 LED Controller is ready.");
status_msg.data.size = strlen(status_msg.data.data);
RCSOFTCHECK(rcl_publish(&publisher_status, &status_msg, NULL));
}
void loop() {
// REMOVED the delay(100).
// The executor's spin function handles the necessary waiting and message checking.
// This makes the node much more responsive.
RCSOFTCHECK(rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100)));
}
17.2.2. Control LED with Potentiometer + ROS2 Commands
// ESP32 with micROS : Control LED with Potentiometer + ROS2 Commands
#include <micro_ros_arduino.h>
#include <stdio.h>
#include <rcl/rcl.h>
#include <rcl/error_handling.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <std_msgs/msg/bool.h>
#include <std_msgs/msg/int32.h>
#include <std_msgs/msg/float32.h>
#include <std_msgs/msg/string.h>
#define LED_PIN 4 // ขา LED (ผ่าน R 220Ω ลง GND)
#define POT_PIN 34 // ขา Analog Input (กลางของ Pot)
#define LED_BUILTIN 13 // LED builtin สำหรับ ZY-ESP32
#define PUBLISH_FREQUENCY 5 // ส่งข้อมูลทุก 5 Hz (200ms)
#define RCCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){error_loop();}}
#define RCSOFTCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){}}
// ROS objects
rcl_subscription_t subscriber_command; // รับคำสั่ง String เท่านั้น (on/off/auto/manual/brightness:xxx)
rcl_publisher_t publisher_pot_raw; // ส่งค่า Potentiometer ดิบ
rcl_publisher_t publisher_pot_percent; // ส่งค่า Potentiometer เป็น %
rcl_publisher_t publisher_brightness; // ส่งค่าความสว่างที่ใช้จริง
rcl_publisher_t publisher_led_state; // ส่งสถานะ LED (เปิด/ปิด)
rcl_publisher_t publisher_status; // ส่งข้อความสถานะ
rcl_timer_t timer;
std_msgs__msg__String command_msg;
std_msgs__msg__Int32 pot_raw_msg;
std_msgs__msg__Float32 pot_percent_msg;
std_msgs__msg__Int32 brightness_out_msg;
std_msgs__msg__Bool led_state_msg;
std_msgs__msg__String status_msg;
rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;
// Control variables
bool led_enabled = true; // สถานะเปิด/ปิด LED
bool auto_mode = true; // โหมดอัตโนมัติ (ใช้ Potentiometer)
int manual_brightness = 128; // ความสว่างแบบ manual (0-255)
int current_brightness = 0; // ความสว่างปัจจุบัน
void error_loop(){
unsigned long lastToggle = 0;
while(1){
// ใช้ millis() แทน delay() เพื่อไม่ block execution
if (millis() - lastToggle >= 100) {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
lastToggle = millis();
}
}
}
// Callback สำหรับรับคำสั่ง String (รองรับหลายคำสั่ง)
void command_callback(const void * msgin)
{
const std_msgs__msg__String * msg = (const std_msgs__msg__String *)msgin;
// คำสั่ง on/off
if (strcmp(msg->data.data, "on") == 0) {
led_enabled = true;
sprintf(status_msg.data.data, "💡 LED เปิด");
}
else if (strcmp(msg->data.data, "off") == 0) {
led_enabled = false;
sprintf(status_msg.data.data, "💤 LED ปิด");
}
// คำสั่ง auto/manual mode
else if (strcmp(msg->data.data, "auto") == 0) {
auto_mode = true;
sprintf(status_msg.data.data, "🤖 โหมดอัตโนมัติ (ใช้ Potentiometer)");
}
else if (strcmp(msg->data.data, "manual") == 0) {
auto_mode = false;
sprintf(status_msg.data.data, "👤 โหมดควบคุมด้วยมือ");
}
// คำสั่งตั้งค่าความสว่าง: brightness:xxx (เช่น brightness:128)
else if (strncmp(msg->data.data, "brightness:", 11) == 0) {
int value = atoi(msg->data.data + 11); // ดึงตัวเลขหลัง "brightness:"
manual_brightness = constrain(value, 0, 255);
auto_mode = false; // เปลี่ยนเป็นโหมด manual
sprintf(status_msg.data.data, "🔆 Manual Brightness: %d", manual_brightness);
}
else {
sprintf(status_msg.data.data, "⚠️ คำสั่งไม่ถูกต้อง (on/off/auto/manual/brightness:xxx)");
}
status_msg.data.size = strlen(status_msg.data.data);
RCSOFTCHECK(rcl_publish(&publisher_status, &status_msg, NULL));
}
// Timer callback - อ่านและควบคุม LED
void timer_callback(rcl_timer_t * timer, int64_t last_call_time)
{
RCLC_UNUSED(last_call_time);
if (timer != NULL) {
// อ่านค่าจาก Potentiometer
int pot_value = analogRead(POT_PIN);
float pot_percent = (pot_value / 4095.0) * 100.0;
// คำนวณความสว่าง
int target_brightness = 0;
if (led_enabled) {
if (auto_mode) {
target_brightness = map(pot_value, 0, 4095, 0, 255);
} else {
target_brightness = manual_brightness;
}
}
current_brightness = target_brightness;
// ควบคุม LED
analogWrite(LED_PIN, current_brightness);
// ส่งข้อมูลผ่าน ROS topics
// ส่งค่า Potentiometer
pot_raw_msg.data = pot_value;
RCSOFTCHECK(rcl_publish(&publisher_pot_raw, &pot_raw_msg, NULL));
pot_percent_msg.data = pot_percent;
RCSOFTCHECK(rcl_publish(&publisher_pot_percent, &pot_percent_msg, NULL));
// ส่งค่าความสว่าง
brightness_out_msg.data = current_brightness;
RCSOFTCHECK(rcl_publish(&publisher_brightness, &brightness_out_msg, NULL));
// ส่งสถานะ LED
led_state_msg.data = led_enabled;
RCSOFTCHECK(rcl_publish(&publisher_led_state, &led_state_msg, NULL));
// ส่งข้อความสถานะรายละเอียด (ทุก 1 วินาที)
static int status_counter = 0;
if (++status_counter >= PUBLISH_FREQUENCY) {
sprintf(status_msg.data.data,
"🎛️ Pot:%d(%.1f%%) | 💡:%s | 🔆:%d | Mode:%s",
pot_value, pot_percent,
led_enabled ? "ON" : "OFF",
current_brightness,
auto_mode ? "AUTO" : "MANUAL");
status_msg.data.size = strlen(status_msg.data.data);
RCSOFTCHECK(rcl_publish(&publisher_status, &status_msg, NULL));
status_counter = 0;
}
}
}
void setup() {
Serial.begin(115200);
// ตั้งค่า WiFi transport สำหรับ micROS
set_microros_transports();
pinMode(LED_PIN, OUTPUT);
pinMode(LED_BUILTIN, OUTPUT);
delay(2000);
allocator = rcl_get_default_allocator();
// สร้าง init_options
RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));
// สร้าง node
RCCHECK(rclc_node_init_default(&node, "esp32_led_potentiometer", "", &support));
// สร้าง subscriber เพียง 1 topic
RCCHECK(rclc_subscription_init_default(
&subscriber_command,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, String),
"led/command"));
// สร้าง publishers
RCCHECK(rclc_publisher_init_default(
&publisher_pot_raw,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32),
"potentiometer/raw"));
RCCHECK(rclc_publisher_init_default(
&publisher_pot_percent,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Float32),
"potentiometer/percent"));
RCCHECK(rclc_publisher_init_default(
&publisher_brightness,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32),
"led/brightness_actual"));
RCCHECK(rclc_publisher_init_default(
&publisher_led_state,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Bool),
"led/state"));
RCCHECK(rclc_publisher_init_default(
&publisher_status,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, String),
"led/status"));
// สร้าง timer
const unsigned int timer_timeout = 1000 / PUBLISH_FREQUENCY;
RCCHECK(rclc_timer_init_default(
&timer,
&support,
RCL_MS_TO_NS(timer_timeout),
timer_callback));
// เตรียม message buffers
command_msg.data.data = (char*) malloc(50 * sizeof(char));
command_msg.data.size = 0;
command_msg.data.capacity = 50;
status_msg.data.data = (char*) malloc(150 * sizeof(char));
status_msg.data.size = 0;
status_msg.data.capacity = 150;
// สร้าง executor (1 subscription + 1 timer = 2)
RCCHECK(rclc_executor_init(&executor, &support.context, 2, &allocator));
RCCHECK(rclc_executor_add_subscription(&executor, &subscriber_command, &command_msg, &command_callback, ON_NEW_DATA));
RCCHECK(rclc_executor_add_timer(&executor, &timer));
// ส่งข้อความเริ่มต้น
sprintf(status_msg.data.data, "🤖 ESP32 LED+Potentiometer Controller พร้อมใช้งาน");
status_msg.data.size = strlen(status_msg.data.data);
RCSOFTCHECK(rcl_publish(&publisher_status, &status_msg, NULL));
Serial.println("=== micROS LED+Potentiometer Controller ===");
Serial.println("🎛️ โหมดอัตโนมัติ: ใช้ Potentiometer ควบคุม");
}
void loop() {
// ไม่ใส่ delay() เพราะ spin_some จัดการ timing เองอยู่แล้ว
RCSOFTCHECK(rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100)));
}
17.2.3. Read Potentiometer and Publish to ROS2 Topics
// ESP32 with micROS : Read Potentiometer and Publish to ROS2 Topics
#include <micro_ros_arduino.h>
#include <stdio.h>
#include <rcl/rcl.h>
#include <rcl/error_handling.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <std_msgs/msg/int32.h>
#include <std_msgs/msg/float32.h>
#include <std_msgs/msg/string.h>
#define POT_PIN 34 // ขา ADC (Analog Input) — ต่อกลางของ Pot
#define LED_BUILTIN 13 // LED builtin สำหรับ ZY-ESP32
#define PUBLISH_FREQUENCY 5 // ส่งข้อมูลทุก 5 Hz (200ms)
#define RCCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){error_loop();}}
#define RCSOFTCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){}}
// ROS objects
rcl_publisher_t publisher_raw; // ส่งค่าดิบ (0-4095)
rcl_publisher_t publisher_voltage; // ส่งค่าแรงดัน (0-3.3V)
rcl_publisher_t publisher_percent; // ส่งค่าเปอร์เซ็นต์ (0-100%)
rcl_publisher_t publisher_status; // ส่งข้อความสถานะ
rcl_timer_t timer;
std_msgs__msg__Int32 raw_msg;
std_msgs__msg__Float32 voltage_msg;
std_msgs__msg__Float32 percent_msg;
std_msgs__msg__String status_msg;
rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;
int last_pot_value = -1;
void error_loop(){
while(1){
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
delay(100);
}
}
// Timer callback - อ่านและส่งค่า Potentiometer
void timer_callback(rcl_timer_t * timer, int64_t last_call_time)
{
RCLC_UNUSED(last_call_time);
if (timer != NULL) {
// อ่านค่าจาก Potentiometer
int pot_value = analogRead(POT_PIN);
// คำนวณค่าต่างๆ
float voltage = (pot_value / 4095.0) * 3.3; // แปลงเป็นแรงดัน (0-3.3V)
float percent = (pot_value / 4095.0) * 100.0; // แปลงเป็นเปอร์เซ็นต์ (0-100%)
// ส่งค่าดิบ (Raw ADC Value)
raw_msg.data = pot_value;
RCSOFTCHECK(rcl_publish(&publisher_raw, &raw_msg, NULL));
// ส่งค่าแรงดัน
voltage_msg.data = voltage;
RCSOFTCHECK(rcl_publish(&publisher_voltage, &voltage_msg, NULL));
// ส่งค่าเปอร์เซ็นต์
percent_msg.data = percent;
RCSOFTCHECK(rcl_publish(&publisher_percent, &percent_msg, NULL));
// ส่งข้อความสถานะ (เฉพาะเมื่อค่าเปลี่ยน)
if (abs(pot_value - last_pot_value) > 10) { // ป้องกันการส่งบ่อยเกินไป
sprintf(status_msg.data.data, "Pot: %d | %.2fV | %.1f%%",
pot_value, voltage, percent);
status_msg.data.size = strlen(status_msg.data.data);
RCSOFTCHECK(rcl_publish(&publisher_status, &status_msg, NULL));
last_pot_value = pot_value;
}
}
}
void setup() {
Serial.begin(115200);
// ตั้งค่า WiFi transport สำหรับ micROS
set_microros_transports();
pinMode(LED_BUILTIN, OUTPUT);
delay(2000);
allocator = rcl_get_default_allocator();
// สร้าง init_options
RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));
// สร้าง node
RCCHECK(rclc_node_init_default(&node, "esp32_potentiometer", "", &support));
// สร้าง publishers
RCCHECK(rclc_publisher_init_default(
&publisher_raw,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32),
"potentiometer/raw"));
RCCHECK(rclc_publisher_init_default(
&publisher_voltage,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Float32),
"potentiometer/voltage"));
RCCHECK(rclc_publisher_init_default(
&publisher_percent,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Float32),
"potentiometer/percent"));
RCCHECK(rclc_publisher_init_default(
&publisher_status,
&node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, String),
"potentiometer/status"));
// สร้าง timer สำหรับส่งข้อมูลเป็นระยะ
const unsigned int timer_timeout = 1000 / PUBLISH_FREQUENCY; // milliseconds
RCCHECK(rclc_timer_init_default(
&timer,
&support,
RCL_MS_TO_NS(timer_timeout),
timer_callback));
// เตรียม message buffer
status_msg.data.data = (char*) malloc(100 * sizeof(char));
status_msg.data.size = 0;
status_msg.data.capacity = 100;
// สร้าง executor
RCCHECK(rclc_executor_init(&executor, &support.context, 1, &allocator));
RCCHECK(rclc_executor_add_timer(&executor, &timer));
// ส่งข้อความเริ่มต้น
sprintf(status_msg.data.data, "ESP32 Potentiometer Reader พร้อมใช้งาน");
status_msg.data.size = strlen(status_msg.data.data);
RCSOFTCHECK(rcl_publish(&publisher_status, &status_msg, NULL));
Serial.println("🎛️ micROS Potentiometer Reader เริ่มทำงาน");
}
void loop() {
// ไม่ใส่ delay() เพราะ spin_some จัดการ timing เองอยู่แล้ว
RCSOFTCHECK(rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100)));
}