\compose\ package that provides the Duckiedrone-specific dashboard widgets (block renderers) and the default mission layout shown on the /dashboard/robot/mission_control page of the Duckiedrone device dashboard (robot/dt-device-dashboard).
This package is consumed by the dashboard via dependencies-compose.txt:
duckietown_duckiedrone==v2.2.3
The dashboard runs inside \compose\, a PHP package system. This repo is one of those packages.
compose-pkg-duckietown-duckiedrone/
├── VERSION # Semantic version — bumped on every release
├── metadata.json # Package name, description, compose compatibility
├── configuration/
│ └── schema.json # Configurable-from-UI settings schema
├── modules/
│ └── renderers/
│ └── blocks/ # Widget definitions (PHP BlockRenderer classes)
│ ├── Duckiedrone_Arming.php # class Mavros_Arming
│ ├── Duckiedrone_Altitude.php
│ ├── Duckiedrone_Control.php
│ ├── Duckiedrone_Heartbeat.php
│ ├── Duckiedrone_Heartbeats_Monitor.php
│ ├── Duckiedrone_IMU_Orientation.php
│ └── DuckietownMsgs_DroneMotorCommand.php
├── data/
│ └── private/
│ └── default_missions/
│ └── duckietown_duckiedrone_missions/
│ ├── default.json # Mission layout shown by default
│ └── ...
├── css/, js/, images/ # Static assets served by the dashboard
├── scripts/ # Optional helper scripts
├── post_install # Runs once on package install
├── post_update # Runs on install AND on package upgrade
├── bump-version.sh # Helper to bump VERSION + tag
└── CHANGELOG.md # Keep in sync with VERSION on every release
Both hooks run with $COMPOSE_USERDATA_DIR pointing at the dashboard's user-data directory (/var/www/html/public_html/system/user-data inside the container). post_update copies the mission files in data/private/default_missions/ into ${USERDATA_DIR}/databases/data/, which is the path the dashboard reads mission layouts from. post_install just delegates to post_update.
If you add a new default mission file, drop it in data/private/default_missions/duckietown_duckiedrone_missions/ and it will be installed on the next build of the image (or on the next mount-and-reload loop, see Development workflow below).
Each PHP file under modules/renderers/blocks/ defines one widget. A widget is a PHP class extending \system\classes\BlockRenderer with:
$ICON— Font Awesome icon shown in the widget picker$ARGUMENTS— schema of user-configurable widget options (rendered into an HTML form when the user adds the widget to a mission)render($id, &$args)— the HTML + JS that gets emitted on the mission page
The $id is unique per widget instance; use it to namespace DOM ids and JS variables when a mission has more than one copy of the same widget.
| File | Class | Purpose |
|---|---|---|
Duckiedrone_Arming.php |
Mavros_Arming |
ARM/DISARM toggle + flight-mode toggle (OFFBOARD/ALTITUDE) + kill switch + takeoff button. Talks to mavros services. |
Duckiedrone_Altitude.php |
Duckiedrone_Altitude |
Altitude plot for /altitude_node/altitude with a desired-height overlay and dynamic Y-axis scaling based on live samples. |
Duckiedrone_Control.php |
Duckiedrone_Control |
Virtual joystick plus full keyboard control publishing to /mavros/manual_control/send, reading /mavros/state, with optional legacy override params for older fly-commands mux setups. Keyboard: w/a/s/d = pitch/roll, arrows ↑↓ = throttle (two-stage ramp with a vertical thrust gauge), arrows ←→ = yaw, space = disarm. |
Duckiedrone_Heartbeat.php |
Duckiedrone_Heartbeat |
Single-topic heartbeat indicator. |
Duckiedrone_Heartbeats_Monitor.php |
Duckiedrone_Heartbeats_Monitor |
Multi-topic heartbeat grid for joystick / altitude / state_estimator / pid. |
DuckietownMsgs_DroneMotorCommand.php |
DuckietownMsgs_DroneMotorCommand |
Bar chart of the four motor PWM values. Reads legacy flight_controller_node/motors messages and falls back to /mavros/rc/out for PX4/MAVROS deployments. |
The current shell-managed Duckiedrone stacks use MAVROS and PX4 calibration endpoints by default. The shipped mission therefore targets /mavros/* and /px4_calibration/*; legacy /flight_controller_node/* and /fly_commands_mux_node/* hooks remain available only for manual compatibility with older deployments.
The default Duckiedrone mission currently renders these panels, in this order:
| Panel | Renderer | Surface | Notes |
|---|---|---|---|
Joystick Heartbeat |
Duckiedrone_Heartbeat |
~/joystick/heartbeat |
Robot-scoped heartbeat pulse. |
Motors PWM |
DuckietownMsgs_DroneMotorCommand |
/mavros/rc/out |
Reads mavros_msgs/RCOut directly. |
Heartbeats Monitor |
Duckiedrone_Heartbeats_Monitor |
~/joystick/heartbeat, ~/altitude_node/heartbeat, ~/state_estimator_node/heartbeat, ~/pid_controller_node/heartbeat |
Aggregated liveness grid for the main companion nodes. |
Remote Control |
Duckiedrone_Control |
/mavros/manual_control/send, /mavros/state |
Virtual joystick and keyboard control (WASD + arrows) plus live command bars and a thrust gauge. |
Arm / Disarm |
Mavros_Arming |
/mavros/cmd/arming, /mavros/cmd/command, /mavros/set_mode, /mavros/state |
ARM/DISARM, flight mode, kill switch. |
Altitude |
Duckiedrone_Altitude |
~/altitude_node/altitude, ~/pid_controller_node/desired/height |
Tilt-corrected altitude with desired-height overlay. |
Time-of-Flight |
Duckiedrone_TimeOfFlight |
~/bottom_tof_driver_node/range, ~/front_tof_driver_node/range, ~/left_tof_driver_node/range, ~/right_tof_driver_node/range, ~/top_tof_driver_node/range |
Multi-sensor ToF plot for all five DD24 rangefinders. |
IMU - Orientation |
Duckiedrone_IMU_Orientation |
/mavros/imu/data |
Roll/pitch/yaw plot plus PX4 calibration controls. |
Camera |
SensorMsgs_CompressedImage |
~/camera_node/image/compressed |
Shared ros package renderer. |
Minimal pattern:
<?php
use \system\classes\Core;
use \system\classes\BlockRenderer;
use \system\packages\ros\ROS;
class My_Widget extends BlockRenderer {
static protected $ICON = [
"class" => "fa",
"name" => "rocket"
];
static protected $ARGUMENTS = [
"ros_hostname" => [
"name" => "ROSbridge hostname",
"type" => "text",
"mandatory" => False,
"default" => "" // empty -> resolved to current robot
],
"topic" => [
"name" => "Topic",
"type" => "text",
"mandatory" => True,
"default" => "~/my_topic"
],
"frequency" => [
"name" => "Frequency (Hz)",
"type" => "number",
"mandatory" => True,
"default" => 10
]
];
protected static function render($id, &$args) {
$host = ROS::sanitize_hostname($args["ros_hostname"]);
?>
<div id="my_widget_<?php echo $id; ?>"></div>
<script type="text/javascript">
(function() {
ROS.connect('<?php echo $host; ?>', function(ros) {
const topic = new ROSLIB.Topic({
ros: ros,
name: '<?php echo $args["topic"]; ?>',
messageType: 'std_msgs/String'
});
topic.subscribe(function(msg) {
document.getElementById('my_widget_<?php echo $id; ?>').innerText = msg.data;
});
});
})();
</script>
<?php
}
}
?>Key conventions:
- Always namespace DOM ids with
<?php echo $id; ?>so multiple instances don't collide. - Use
ROS::sanitize_hostname($args["ros_hostname"])to get the rosbridge host. An emptyros_hostnamemeans "current robot" — the dashboard resolves it server-side. - Use
ROS::connect(host, callback)in JS to share the singleton rosbridge connection; do not instantiateROSLIB.Rosdirectly. - Gate frequent work by checking
anybody_listening()on the ROS side, not on the widget side — widgets are the consumer, not the publisher.
Edit data/private/default_missions/duckietown_duckiedrone_missions/default.json and add a block entry:
{
"shape": { "rows": 1, "cols": 3 },
"renderer": "My_Widget",
"title": "My widget",
"subtitle": "what it does",
"args": {
"ros_hostname": "",
"topic": "/my_topic",
"frequency": 10
}
}The renderer field must match the PHP class name (not the filename).
Known issue —
~/path resolution for shared endpoints. On a virtual drone, rosbridge resolves~to/<robot>/rosbridge_websocket, not to/. That breaks entries like~/mavros/...and~/px4_calibration/...because those shared services/topics do not live under the robot namespace. Prefer absolute paths (/mavros/cmd/arming,/mavros/state,/px4_calibration/...) for MAVROS and PX4 calibration endpoints until the upstreamROS::sanitize_hostnamebehavior is fixed. Seedocs/dashboard-test-report/README.md.
The default Duckiedrone mission in this repo already follows that rule for MAVROS and PX4 calibration endpoints, while intentionally keeping ~/... for robot-scoped topics such as ~/joystick/heartbeat, ~/altitude_node/altitude, and ~/camera_node/image/compressed.
The dashboard image bakes this package in at build time via dependencies-compose.txt. For fast iteration you mount the local checkout into a running dashboard sandbox container, so PHP changes show up on page reload without a rebuild.
From robot/dt-device-dashboard:
dts devel buildThis pulls duckietown_duckiedrone==v2.2.3 (or whichever version is pinned). You need the package installed in the image at least once so that the autoload paths exist.
cd robot/dt-device-dashboard/sandbox
make run EXTRA_ARGS='-v "/workspaces/dt-env-developer/compose/compose-pkg-duckietown-duckiedrone:/user-data/packages/duckietown_duckiedrone:rw"'The dashboard is then reachable at http://localhost:8888. PHP file changes are picked up on the next page load. Changes to post_update / default missions require a container restart because they only run on package install/update — or you can docker exec into the container and run /user-data/packages/duckietown_duckiedrone/post_update manually.
Build and run on the robot (physical or virtual):
dts devel build -f -H ROBOT_NAME
dts devel run -H ROBOT_NAMEFor a virtual drone, see the dashboard README's Testing with a virtual Duckiedrone section.
./bump-version.sh # bumps VERSION, creates git tagUpdate CHANGELOG.md with a new entry, then bump the pin in robot/dt-device-dashboard/dependencies-compose.txt to the new tag. The dashboard image needs to be rebuilt and redeployed for the new version to ship to robots.
- Tag and push this package: the
entebranch tag (e.g.v2.2.3) is whatdependencies-compose.txtreferences. - In
dt-device-dashboard, bumpduckietown_duckiedrone==v2.2.3independencies-compose.txt, commit, push. - Rebuild and publish the dashboard image; it is then picked up the next time
dts duckiebot update ROBOT_NAMEruns on a robot.
- Widget class name = PHP filename stem, but may differ when we're renaming (e.g.
Duckiedrone_Arming.phpdefinesclass Mavros_Arming). Therendererkey in mission JSON uses the class name. - Use
snake_casefor$ARGUMENTSkeys; they map 1:1 toargsin the mission JSON. - Color/style args should pass through
data-*attributes rather than inline styles when possible — avoids PHP string escaping bugs. - Prefer absolute ROS paths (
/mavros/...) in default missions; see the note above.