ESP32 + micro-ROS 2 node featuring directional ultrasonic sensing, Kalman filtering, FreeRTOS scheduling, and servo camera control for robotics and drones.
- 📡 Five-direction ultrasonic obstacle sensing
- 🔧 Adaptive Kalman filter (tunable Q/R, windowed variance)
- ⏱️ FreeRTOS-based concurrent tasks (sensor polling, ROS 2 publishing)
- 🎥 Servo-based camera angle control (ROS 2 service)
- 🔌 Fully ROS 2 (micro-ROS) compatible over Serial/UDP
- Clone the repo
- Open in PlatformIO (VS Code or CLI)
- Upload to ESP32 with one click
- Connect micro-ROS agent
- Done ✅
- Coverage Pattern
- System Architecture
- Real-Time OS Implementation
- Kalman Filter Algorithm
- Hardware Setup
- PlatformIO Build & Deployment
- ROS 2 Subscriber Examples
- API Reference
- Troubleshooting
- License
5-directional sensing configuration:
graph LR
F[Forward ] --> C[Controller]
L[Left ] --> C
R[Right ] --> C
B[Back ] --> C
D[Downward ] --> C
Key Specifications:
- Angular resolution: 5 discrete directions
- Range: 2cm to 4m
- Update rate: 20Hz (50ms cycle)
flowchart TB
S1["Downward Sensor (HC-SR04)"] --> SP
S2["Forward Sensor (HC-SR04)"] --> SP
S3["Left Sensor (HC-SR04)"] --> SP
S4["Right Sensor (HC-SR04)"] --> SP
S5["Back Sensor (HC-SR04)"] --> SP
subgraph ESP32["ESP32-WROOM"]
direction TB
SP["Sensor Polling\n(20Hz)"] --> KF["Kalman Filter\n(Adaptive)"]
KF --> RP["ROS 2 Publisher"]
RP --> M["micro-ROS Client"]
end
M["micro-ROS Client\n(Serial/UDP)"] --> R["ROS 2 Agent"]
R --> T1["Topic: /ultrasonic_sensor/downward/filtered"]
R --> T2["Topic: /ultrasonic_sensor/forward/filtered"]
R --> T3["Topic: /ultrasonic_sensor/left/filtered"]
R --> T4["Topic: /ultrasonic_sensor/right/filtered"]
R --> T5["Topic: /ultrasonic_sensor/back/filtered"]
| Task | Priority | Frequency | Execution Time | Description |
|---|---|---|---|---|
SensorPollTask |
3 (High) | 20Hz | 5-10ms | Reads all 5 sensors sequentially |
KalmanFilterTask |
3 (High) | 20Hz | 1-2ms/sensor | Processes each sensor reading |
PublishTask |
2 | 20Hz | 3-5ms | Publishes to ROS topics |
MainLoop |
1 (Low) | 10Hz | Variable | Services and diagnostics |
FreeRTOS implements a preemptive, priority-based scheduling algorithm with the following characteristics:
flowchart LR
S[Scheduler] --> P[Priority Queue]
P --> H[Highest Priority Ready Task]
H --> R[Running]
R -->|Preemption| NP[New Higher Priority Task?]
NP -->|Yes| H
NP -->|No| C[Continue Execution]
C -->|Block/Suspend| W[Wait for Event]
W --> T[Time or Event Trigger]
T --> P
Key Scheduling Principles:
- Priority-Based: Tasks with higher priority (higher number) always preempt lower priority tasks
- Preemptive: Running task is immediately interrupted when higher priority task becomes ready
- Round-Robin: Tasks of equal priority share CPU time in time slices (configurable)
- Deterministic: Worst-case execution times (WCET) guarantee real-time performance
gantt
title FreeRTOS Task Execution Timeline (50ms Period)
dateFormat ms
axisFormat %S.%L
section Tasks
SensorPollTask :a1, 0, 10ms
KalmanFilterTask :a2, after a1, 10ms
PublishTask :a3, after a2, 5ms
MainLoop :a4, 0, 35ms
Timeline Explanation:
- 0-10ms:
SensorPollTask(Prio 3) reads all 5 sensors - 10-20ms:
KalmanFilterTask(Prio 3) processes sensor data - 20-25ms:
PublishTask(Prio 2) sends data to ROS - 0-35ms:
MainLoop(Prio 1) runs in background when CPU available
// Acquire mutex before accessing shared data
xSemaphoreTake(data_mutex, portMAX_DELAY);
// Critical section: Update sensor data
raw_readings[0] = sensor_value_0;
// ... update all sensors
// Release mutex when done
xSemaphoreGive(data_mutex);// In SensorPollTask after reading all sensors:
xTaskNotifyGive(KalmanFilterTask); // Trigger Kalman processing
// In KalmanFilterTask:
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // Wait for notificationSynchronization Flow:
- Sensor task completes readings and notifies Kalman task
- Kalman task wakes immediately (same priority)
- Publishing task waits until Kalman completes
- Main loop runs only when no other tasks need CPU
Schedulability Test:
Total CPU Utilization = Σ(Task Execution Time / Period)
= (10ms/50ms) + (10ms/50ms) + (5ms/50ms) + (35ms/100ms)
= 0.2 + 0.2 + 0.1 + 0.35 = 0.85 (85% < 100%)
System is schedulable with 15% idle margin
Worst-Case Scenario:
- All tasks ready simultaneously
- Execution order: SensorPoll → KalmanFilter → PublishTask → MainLoop
- Maximum latency: 10ms + 10ms + 5ms = 25ms
-
Priority Assignment:
- Sensor and Kalman tasks at same highest priority
- Publishing at medium priority
- System services at lowest priority
-
Minimizing Latency:
- Direct task notification for immediate wakeup
- Mutex protection with minimal critical sections
- Avoidance of priority inversion
-
Determinism:
- Worst-case execution time measurement
- Fixed-size buffers (no dynamic allocation)
- Minimal interrupt usage
-
Robustness:
- Timeout handling on all blocking calls
- Stack overflow protection
- Watchdog monitoring
Implementation Details:
- Sliding Window: Maintains last 10 innovations for variance calculation
- Exponential Smoothing: α = 0.01 for gradual noise adaptation
- State Initialization: Auto-initializes on first valid measurement
- NaN Handling: Bypasses filter during invalid readings
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Range
class SensorMonitor(Node):
def __init__(self):
super().__init__('sensor_monitor')
self.subscriptions = []
# Create subscribers for all directions
directions = ['downward', 'forward', 'left', 'right', 'back']
for dir in directions:
sub = self.create_subscription(
Range,
f'/ultrasonic_sensor/{dir}/filtered',
lambda msg, d=dir: self.sensor_callback(msg, d),
10
)
self.subscriptions.append(sub)
self.get_logger().info("Sensor monitor started")
def sensor_callback(self, msg, direction):
if not math.isnan(msg.range):
self.get_logger().info(
f"{direction.capitalize()}: {msg.range:.2f}m",
throttle_duration_sec=1 # Limit to 1Hz output
)
def main():
rclpy.init()
node = SensorMonitor()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()| Topic | Type | Description | QoS |
|---|---|---|---|
/ultrasonic_sensor/downward/filtered |
sensor_msgs/Range |
Downward distance (m) | Best effort |
/ultrasonic_sensor/forward/filtered |
sensor_msgs/Range |
Forward distance (m) | Best effort |
/ultrasonic_sensor/left/filtered |
sensor_msgs/Range |
Left distance (m) | Best effort |
/ultrasonic_sensor/right/filtered |
sensor_msgs/Range |
Right distance (m) | Best effort |
/ultrasonic_sensor/back/filtered |
sensor_msgs/Range |
Back distance (m) | Best effort |
/diagnostics |
diagnostic_msgs/DiagnosticStatus |
System health | Reliable |
| Service | Type | Description |
|---|---|---|
/servo_cam_service |
servocam_interfaces/srv/Servocam |
Pan/tilt control |
[env:upesy_wroom]
platform = espressif32
board = upesy_wroom
framework = arduino
monitor_speed = 115200
lib_deps =
teckel12/NewPing@^1.9.7
micro-ROS/micro_ros_platformio@^0.4.0
build_flags =
-I include
-Wno-unused-variable
-Wno-unused-parameter- Install dependencies:
pio lib install- Build the firmware:
pio run- Flash to ESP32:
pio run -t upload- Monitor serial output:
pio device monitor# For ROS 2 Humble
sudo apt install ros-humble-micro-ros-agent
# For ROS 2 Foxy
sudo apt install ros-foxy-micro-ros-agentros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -b 115200- Hardware initialization
- Sensor diagnostic check
- FreeRTOS task creation
- micro-ROS node initialization
- Continuous sensor reading and publishing
Starting Ultrasonic Sensor System...
--- Sensor Status Report ---
Sensor downward (TRIG:4, ECHO:16): OK (1.23m)
Sensor forward (TRIG:17, ECHO:14): OK (0.87m)
...
----------------------------
System initialization complete!
micro-ROS connection active
Hardware status: ALL SENSORS OK
Servo status: CONFIGURED
Topics:
/ultrasonic_sensor/downward/raw
/ultrasonic_sensor/downward/filtered
...
Service: /servo_cam_service
[downward] Raw: 1.23m | Filtered: 1.22m
[forward] Raw: 0.87m | Filtered: 0.86m
...
Troubleshooting No ROS 2 topicsCause: micro-ROS agent not running or wrong portSolution: Start agent with:
ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -b115200Intermittent readingsCause: Voltage divider mismatch or loose wiringSolution: Verify 1 kΩ/2 kΩ resistor divider and secure all sensor connections.
Slow Kalman convergenceCause: Low adaptation rate (α too small)Solution: Increase α in code (e.g., from 0.01 to 0.02).
Over-filteringCause: Minimum process noise (Q_min) too lowSolution: Raise Q_min (e.g., from 0.001 to 0.01).
Noise spikesCause: Sliding window size too smallSolution: Increase WINDOW_SIZE (e.g., from 10 to 20 samples).
RTOS preemption lagCause: Priority inversion or misconfigured task prioritiesSolution: Ensure FreeRTOS priorities: SensorTask (highest) > PublishTask > main loop.
Servo no responseCause: PWM duty change below detection thresholdSolution: Guarantee angle commands result in ≥10 duty unit change
