π Live Project Website
ESP32 + micro-ROS 2 node featuring directional ultrasonic sensing, Kalman filtering, FreeRTOS scheduling, and servo camera control for robotics and drones.
5-directional sensing configuration:
graph LR
F[Forward ] --> C[Controller]
L[Left ] --> C
R[Right ] --> C
B[Back ] --> C
D[Downward ] --> C
Key Specifications:
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:
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:
SensorPollTask
(Prio 3) reads all 5 sensorsKalmanFilterTask
(Prio 3) processes sensor dataPublishTask
(Prio 2) sends data to ROSMainLoop
(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 notification
Synchronization Flow:
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:
\begin{align*}
\text{Prediction:} & \\
x_{\text{prior}} &= x_{\text{prev}} \\
P_{\text{prior}} &= P_{\text{prev}} + Q \\
\\
\text{Update:} & \\
K &= \frac{P_{\text{prior}}}{P_{\text{prior}} + R} \\
x &= x_{\text{prior}} + K(z - x_{\text{prior}}) \\
P &= (1 - K)P_{\text{prior}}
\end{align*}
\begin{align*}
\text{Innovation:} & \quad \epsilon = z - x_{\text{prior}} \\
\text{Measurement Noise:} & \quad R = (1 - \alpha)R_{\text{prev}} + \alpha \epsilon^2 \\
\text{Process Noise:} & \quad Q = \max(0.001, 0.1 \times \sigma^2_{\text{window}})
\end{align*}
Implementation Details:
#!/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
pio lib install
pio run
pio run -t upload
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-agent
ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -b 115200
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 -b115200
Intermittent 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