From 2d20c4ec9baa48c013ad463ce04ee8a352fb46eb Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Sun, 29 Jun 2025 14:14:00 +0800
Subject: [PATCH 01/12] Fix merge conflicts with main

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 examples/timer_demo/Cargo.toml                |   8 +
 examples/timer_demo/package.xml               |  21 +
 examples/timer_demo/src/main.rs               |  22 +
 examples/worker_demo/src/main.rs              |  27 +-
 rclrs/Cargo.lock                              | 422 +++++++++-
 rclrs/src/clock.rs                            |   5 +
 rclrs/src/lib.rs                              |   2 +
 rclrs/src/node.rs                             |  78 +-
 rclrs/src/timer.rs                            | 777 ++++++++++++++++++
 rclrs/src/timer/any_timer_callback.rs         |  15 +
 rclrs/src/timer/into_node_timer_callback.rs   |  67 ++
 rclrs/src/timer/into_worker_timer_callback.rs |  95 +++
 rclrs/src/timer/timer_options.rs              | 100 +++
 rclrs/src/worker.rs                           |  67 +-
 14 files changed, 1670 insertions(+), 36 deletions(-)
 create mode 100644 examples/timer_demo/Cargo.toml
 create mode 100644 examples/timer_demo/package.xml
 create mode 100644 examples/timer_demo/src/main.rs
 create mode 100644 rclrs/src/timer.rs
 create mode 100644 rclrs/src/timer/any_timer_callback.rs
 create mode 100644 rclrs/src/timer/into_node_timer_callback.rs
 create mode 100644 rclrs/src/timer/into_worker_timer_callback.rs
 create mode 100644 rclrs/src/timer/timer_options.rs

diff --git a/examples/timer_demo/Cargo.toml b/examples/timer_demo/Cargo.toml
new file mode 100644
index 000000000..23b9f1fa1
--- /dev/null
+++ b/examples/timer_demo/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "timer_demo"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+rclrs = "0.4"
+example_interfaces = "*"
diff --git a/examples/timer_demo/package.xml b/examples/timer_demo/package.xml
new file mode 100644
index 000000000..684cea8fa
--- /dev/null
+++ b/examples/timer_demo/package.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<?xml-model
+   href="http://download.ros.org/schema/package_format3.xsd"
+   schematypens="http://www.w3.org/2001/XMLSchema"?>
+<package format="3">
+  <name>examples_timer_demo</name>
+  <maintainer email="esteve@apache.org">Esteve Fernandez</maintainer>
+  <!-- This project is not military-sponsored, Jacob's employment contract just requires him to use this email address -->
+  <maintainer email="jacob.a.hassold.civ@army.mil">Jacob Hassold</maintainer>
+  <version>0.4.1</version>
+  <description>Package containing an example of how to use a worker in rclrs.</description>
+  <license>Apache License 2.0</license>
+
+  <depend>rclrs</depend>
+  <depend>rosidl_runtime_rs</depend>
+  <depend>example_interfaces</depend>
+
+  <export>
+    <build_type>ament_cargo</build_type>
+  </export>
+</package>
diff --git a/examples/timer_demo/src/main.rs b/examples/timer_demo/src/main.rs
new file mode 100644
index 000000000..608a82f10
--- /dev/null
+++ b/examples/timer_demo/src/main.rs
@@ -0,0 +1,22 @@
+/// Creates a SimpleTimerNode, initializes a node and the timer with a callback
+/// that prints the timer callback execution iteration. The callback is executed
+/// thanks to the spin, which is in charge of executing the timer's events among
+/// other entities' events.
+use rclrs::*;
+use std::time::Duration;
+
+fn main() -> Result<(), RclrsError> {
+    let mut executor = Context::default_from_env()?.create_basic_executor();
+    let node = executor.create_node("timer_demo")?;
+    let worker = node.create_worker::<usize>(0);
+    let timer_period = Duration::from_secs(1);
+    let _timer = worker.create_timer_repeating(timer_period, move |count: &mut usize| {
+        *count += 1;
+        println!(
+            "Drinking 🧉 for the {}th time every {:?}.",
+            *count, timer_period,
+        );
+    })?;
+
+    executor.spin(SpinOptions::default()).first_error()
+}
diff --git a/examples/worker_demo/src/main.rs b/examples/worker_demo/src/main.rs
index 253a95fce..bb3a97615 100644
--- a/examples/worker_demo/src/main.rs
+++ b/examples/worker_demo/src/main.rs
@@ -1,5 +1,5 @@
 use rclrs::*;
-use std::sync::Arc;
+use std::time::Duration;
 
 fn main() -> Result<(), RclrsError> {
     let mut executor = Context::default_from_env()?.create_basic_executor();
@@ -15,27 +15,12 @@ fn main() -> Result<(), RclrsError> {
         },
     )?;
 
-    // // Use this timer-based implementation when timers are available instead
-    // // of using std::thread::spawn.
-    // let _timer = worker.create_timer_repeating(
-    //     Duration::from_secs(1),
-    //     move |data: &mut String| {
-    //         let msg = example_interfaces::msg::String {
-    //             data: data.clone()
-    //         };
-
-    //         publisher.publish(msg).ok();
-    //     }
-    // )?;
-
-    std::thread::spawn(move || loop {
-        std::thread::sleep(std::time::Duration::from_secs(1));
-        let publisher = Arc::clone(&publisher);
-        let _ = worker.run(move |data: &mut String| {
+    let _timer =
+        worker.create_timer_repeating(Duration::from_secs(1), move |data: &mut String| {
             let msg = example_interfaces::msg::String { data: data.clone() };
-            publisher.publish(msg).unwrap();
-        });
-    });
+
+            publisher.publish(msg).ok();
+        })?;
 
     println!(
         "Beginning repeater... \n >> \
diff --git a/rclrs/Cargo.lock b/rclrs/Cargo.lock
index 5fc5e7bdc..834786e1f 100644
--- a/rclrs/Cargo.lock
+++ b/rclrs/Cargo.lock
@@ -4,10 +4,11 @@ version = 3
 
 [[package]]
 name = "action_msgs"
-version = "1.2.1"
+version = "2.0.2"
 dependencies = [
  "builtin_interfaces",
  "rosidl_runtime_rs",
+ "service_msgs",
  "unique_identifier_msgs",
 ]
 
@@ -45,6 +46,126 @@ dependencies = [
  "walkdir",
 ]
 
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
+dependencies = [
+ "async-channel 2.3.1",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3"
+dependencies = [
+ "async-lock",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "tracing",
+ "windows-sys",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
+dependencies = [
+ "event-listener 5.4.0",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-std"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24"
+dependencies = [
+ "async-channel 1.9.0",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
 [[package]]
 name = "autocfg"
 version = "1.4.0"
@@ -92,13 +213,32 @@ version = "2.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
 
+[[package]]
+name = "blocking"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
+dependencies = [
+ "async-channel 2.3.1",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
 [[package]]
 name = "builtin_interfaces"
-version = "1.2.1"
+version = "2.0.2"
 dependencies = [
  "rosidl_runtime_rs",
 ]
 
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
 [[package]]
 name = "cexpr"
 version = "0.6.0"
@@ -125,6 +265,21 @@ dependencies = [
  "libloading",
 ]
 
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
 [[package]]
 name = "either"
 version = "1.15.0"
@@ -141,6 +296,44 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "event-listener"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener 5.4.0",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "example_interfaces"
+version = "0.12.0"
+dependencies = [
+ "action_msgs",
+ "builtin_interfaces",
+ "rosidl_runtime_rs",
+ "service_msgs",
+ "unique_identifier_msgs",
+]
+
 [[package]]
 name = "fastrand"
 version = "2.3.0"
@@ -195,6 +388,19 @@ version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
 
+[[package]]
+name = "futures-lite"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "futures-macro"
 version = "0.3.31"
@@ -260,6 +466,24 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
 
+[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
 [[package]]
 name = "itertools"
 version = "0.8.2"
@@ -278,6 +502,25 @@ dependencies = [
  "either",
 ]
 
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
 [[package]]
 name = "libc"
 version = "0.2.172"
@@ -305,6 +548,9 @@ name = "log"
 version = "0.4.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+dependencies = [
+ "value-bag",
+]
 
 [[package]]
 name = "memchr"
@@ -352,6 +598,12 @@ version = "1.21.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
 
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.16"
@@ -364,6 +616,32 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
+[[package]]
+name = "piper"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "polling"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "tracing",
+ "windows-sys",
+]
+
 [[package]]
 name = "prettyplease"
 version = "0.2.32"
@@ -403,8 +681,10 @@ name = "rclrs"
 version = "0.4.1"
 dependencies = [
  "ament_rs",
+ "async-std",
  "bindgen",
  "cfg-if",
+ "example_interfaces",
  "futures",
  "libloading",
  "rosidl_runtime_rs",
@@ -477,6 +757,12 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
 [[package]]
 name = "same-file"
 version = "1.0.6"
@@ -515,6 +801,14 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "service_msgs"
+version = "2.0.2"
+dependencies = [
+ "builtin_interfaces",
+ "rosidl_runtime_rs",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -556,11 +850,12 @@ dependencies = [
 
 [[package]]
 name = "test_msgs"
-version = "1.2.1"
+version = "2.0.2"
 dependencies = [
  "action_msgs",
  "builtin_interfaces",
  "rosidl_runtime_rs",
+ "service_msgs",
  "unique_identifier_msgs",
 ]
 
@@ -586,6 +881,22 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.18"
@@ -594,11 +905,17 @@ checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
 
 [[package]]
 name = "unique_identifier_msgs"
-version = "2.2.1"
+version = "2.5.0"
 dependencies = [
  "rosidl_runtime_rs",
 ]
 
+[[package]]
+name = "value-bag"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
+
 [[package]]
 name = "walkdir"
 version = "2.5.0"
@@ -618,6 +935,87 @@ dependencies = [
  "wit-bindgen-rt",
 ]
 
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "winapi-util"
 version = "0.1.9"
@@ -775,8 +1173,20 @@ dependencies = [
 
 [[patch.unused]]
 name = "rcl_interfaces"
-version = "1.2.1"
+version = "2.0.2"
+
+[[patch.unused]]
+name = "rclrs"
+version = "0.4.1"
+
+[[patch.unused]]
+name = "rclrs_example_msgs"
+version = "0.4.1"
 
 [[patch.unused]]
 name = "rosgraph_msgs"
-version = "1.2.1"
+version = "2.0.2"
+
+[[patch.unused]]
+name = "std_msgs"
+version = "5.3.6"
diff --git a/rclrs/src/clock.rs b/rclrs/src/clock.rs
index 992cd4b44..8dd543455 100644
--- a/rclrs/src/clock.rs
+++ b/rclrs/src/clock.rs
@@ -88,6 +88,11 @@ impl Clock {
         Self { kind, rcl_clock }
     }
 
+    /// Returns the clock's `rcl_clock_t`.
+    pub(crate) fn get_rcl_clock(&self) -> &Arc<Mutex<rcl_clock_t>> {
+        &self.rcl_clock
+    }
+
     /// Returns the clock's `ClockType`.
     pub fn clock_type(&self) -> ClockType {
         self.kind
diff --git a/rclrs/src/lib.rs b/rclrs/src/lib.rs
index 366e499b8..f5584fa27 100644
--- a/rclrs/src/lib.rs
+++ b/rclrs/src/lib.rs
@@ -189,6 +189,7 @@ mod service;
 mod subscription;
 mod time;
 mod time_source;
+mod timer;
 mod vendor;
 mod wait_set;
 mod worker;
@@ -217,5 +218,6 @@ pub use service::*;
 pub use subscription::*;
 pub use time::*;
 use time_source::*;
+pub use timer::*;
 pub use wait_set::*;
 pub use worker::*;
diff --git a/rclrs/src/node.rs b/rclrs/src/node.rs
index dd01d0605..1a0c67224 100644
--- a/rclrs/src/node.rs
+++ b/rclrs/src/node.rs
@@ -29,12 +29,14 @@ use async_std::future::timeout;
 use rosidl_runtime_rs::Message;
 
 use crate::{
-    rcl_bindings::*, Client, ClientOptions, ClientState, Clock, ContextHandle, ExecutorCommands,
-    IntoAsyncServiceCallback, IntoAsyncSubscriptionCallback, IntoNodeServiceCallback,
-    IntoNodeSubscriptionCallback, LogParams, Logger, ParameterBuilder, ParameterInterface,
-    ParameterVariant, Parameters, Promise, Publisher, PublisherOptions, PublisherState, RclrsError,
-    Service, ServiceOptions, ServiceState, Subscription, SubscriptionOptions, SubscriptionState,
-    TimeSource, ToLogParams, Worker, WorkerOptions, WorkerState, ENTITY_LIFECYCLE_MUTEX,
+    rcl_bindings::*, AnyTimerCallback, Client, ClientOptions, ClientState, Clock, ContextHandle,
+    ExecutorCommands, IntoAsyncServiceCallback, IntoAsyncSubscriptionCallback,
+    IntoNodeServiceCallback, IntoNodeSubscriptionCallback, IntoNodeTimerOneshotCallback,
+    IntoNodeTimerRepeatingCallback, IntoTimerOptions, LogParams, Logger, ParameterBuilder,
+    ParameterInterface, ParameterVariant, Parameters, Promise, Publisher, PublisherOptions,
+    PublisherState, RclrsError, Service, ServiceOptions, ServiceState, Subscription,
+    SubscriptionOptions, SubscriptionState, TimeSource, Timer, TimerState, ToLogParams, Worker,
+    WorkerOptions, WorkerState, ENTITY_LIFECYCLE_MUTEX,
 };
 
 /// A processing unit that can communicate with other nodes. See the API of
@@ -893,6 +895,70 @@ impl NodeState {
         )
     }
 
+    /// Create a [`Timer`] with a repeating callback.
+    ///
+    /// See also:
+    /// * [`Self::create_timer_oneshot`]
+    /// * [`Self::create_timer_inert`]
+    pub fn create_timer_repeating<'a, Args>(
+        &self,
+        options: impl IntoTimerOptions<'a>,
+        callback: impl IntoNodeTimerRepeatingCallback<Args>,
+    ) -> Result<Timer, RclrsError> {
+        self.create_timer(options, callback.into_node_timer_repeating_callback())
+    }
+
+    /// Create a [`Timer`] whose callback will be triggered once after the period
+    /// of the timer has elapsed. After that you will need to use
+    /// [`Timer::set_repeating`] or [`Timer::set_oneshot`] or else nothing will happen
+    /// the following times that the `Timer` elapses.
+    ///
+    /// See also:
+    /// * [`Self::create_timer_repeating`]
+    /// * [`Self::create_time_inert`]
+    pub fn create_timer_oneshot<'a, Args>(
+        &self,
+        options: impl IntoTimerOptions<'a>,
+        callback: impl IntoNodeTimerOneshotCallback<Args>,
+    ) -> Result<Timer, RclrsError> {
+        self.create_timer(options, callback.into_node_timer_oneshot_callback())
+    }
+
+    /// Create a [`Timer`] without a callback. Nothing will happen when this
+    /// `Timer` elapses until you use [`Timer::set_callback`] or a related method.
+    ///
+    /// See also:
+    /// * [`Self::create_timer_repeating`]
+    /// * [`Self::create_timer_oneshot`]
+    pub fn create_timer_inert<'a>(
+        &self,
+        options: impl IntoTimerOptions<'a>,
+    ) -> Result<Timer, RclrsError> {
+        self.create_timer(options, AnyTimerCallback::Inert)
+    }
+
+    /// Used internally to create a [`Timer`].
+    ///
+    /// Downstream users should instead use:
+    /// * [`Self::create_timer_repeating`]
+    /// * [`Self::create_timer_oneshot`]
+    /// * [`Self::create_timer_inert`]
+    fn create_timer<'a>(
+        &self,
+        options: impl IntoTimerOptions<'a>,
+        callback: AnyTimerCallback<Node>,
+    ) -> Result<Timer, RclrsError> {
+        let options = options.into_timer_options();
+        let clock = options.clock.as_clock(self);
+        TimerState::create(
+            options.period,
+            clock,
+            callback,
+            self.commands.async_worker_commands(),
+            &self.handle.context_handle,
+        )
+    }
+
     /// Returns the ROS domain ID that the node is using.
     ///
     /// The domain ID controls which nodes can send messages to each other, see the [ROS 2 concept article][1].
diff --git a/rclrs/src/timer.rs b/rclrs/src/timer.rs
new file mode 100644
index 000000000..934060fd1
--- /dev/null
+++ b/rclrs/src/timer.rs
@@ -0,0 +1,777 @@
+use crate::{
+    clock::Clock, context::ContextHandle, error::RclrsError, log_error, rcl_bindings::*, Node,
+    RclPrimitive, RclPrimitiveHandle, RclPrimitiveKind, ToLogParams, ToResult, Waitable,
+    WaitableLifecycle, WorkScope, Worker, WorkerCommands, ENTITY_LIFECYCLE_MUTEX,
+};
+// TODO: fix me when the callback type is properly defined.
+// use std::fmt::Debug;
+use std::{
+    any::Any,
+    sync::{Arc, Mutex, Weak},
+    time::Duration,
+};
+
+mod any_timer_callback;
+pub use any_timer_callback::*;
+
+mod timer_options;
+pub use timer_options::*;
+
+mod into_node_timer_callback;
+pub use into_node_timer_callback::*;
+
+mod into_worker_timer_callback;
+pub use into_worker_timer_callback::*;
+
+/// Struct for executing periodic events.
+///
+/// The executor needs to be [spinning][1] for a timer's callback to be triggered.
+///
+/// Timers can be created by a [`Node`] using one of these methods:
+/// - [`NodeState::create_timer_repeating`]
+/// - [`NodeState::create_timer_oneshot`]
+/// - [`NodeState::create_timer_inert`]
+///
+/// Timers can also be created by a [`Worker`], in which case they can access the worker's payload:
+/// - [`Worker::create_timer_repeating`]
+/// - [`Worker::create_timer_oneshot`]
+/// - [`Worker::create_timer_inert`]
+///
+/// The API of timers is given by [`TimerState`].
+///
+/// [1]: crate::Executor::spin
+pub type Timer = Arc<TimerState<Node>>;
+
+/// A [`Timer`] that runs on a [`Worker`].
+///
+/// Create a worker timer using [`Worker::create_timer_repeating`],
+/// [`Worker::create_timer_oneshot`], or [`Worker::create_timer_inert`].
+pub type WorkerTimer<Payload> = Arc<TimerState<Worker<Payload>>>;
+
+/// The inner state of a [`Timer`].
+///
+/// This is public so that you can choose to create a [`Weak`] reference to it
+/// if you want to be able to refer to a [`Timer`] in a non-owning way. It is
+/// generally recommended to manage the `TimerState` inside of an [`Arc`], and
+/// [`Timer`] is provided as a convenience alias for that.
+///
+/// The public API of [`Timer`] is implemented via `TimerState`.
+///
+/// Timers that run inside of a [`Worker`] are represented by [`WorkerTimer`].
+pub struct TimerState<Scope: WorkScope> {
+    pub(crate) handle: Arc<TimerHandle>,
+    /// The callback function that runs when the timer is due.
+    callback: Mutex<Option<AnyTimerCallback<Scope>>>,
+    /// What was the last time lapse between calls to this timer
+    last_elapse: Mutex<Duration>,
+    /// We use Mutex<Option<>> here because we need to construct the TimerState object
+    /// before we can get the lifecycle handle.
+    #[allow(unused)]
+    lifecycle: Mutex<Option<WaitableLifecycle>>,
+    _ignore: std::marker::PhantomData<Scope>,
+}
+
+impl<Scope: WorkScope> TimerState<Scope> {
+    /// Gets the period of the timer
+    pub fn get_timer_period(&self) -> Result<Duration, RclrsError> {
+        let mut timer_period_ns = 0;
+        unsafe {
+            let rcl_timer = self.handle.rcl_timer.lock().unwrap();
+            rcl_timer_get_period(&*rcl_timer, &mut timer_period_ns)
+        }
+        .ok()?;
+
+        rcl_duration(timer_period_ns)
+    }
+
+    /// Cancels the timer, stopping the execution of the callback
+    pub fn cancel(&self) -> Result<(), RclrsError> {
+        let mut rcl_timer = self.handle.rcl_timer.lock().unwrap();
+        let cancel_result = unsafe { rcl_timer_cancel(&mut *rcl_timer) }.ok()?;
+        Ok(cancel_result)
+    }
+
+    /// Checks whether the timer is canceled or not
+    pub fn is_canceled(&self) -> Result<bool, RclrsError> {
+        let mut is_canceled = false;
+        unsafe {
+            let rcl_timer = self.handle.rcl_timer.lock().unwrap();
+            rcl_timer_is_canceled(&*rcl_timer, &mut is_canceled)
+        }
+        .ok()?;
+        Ok(is_canceled)
+    }
+
+    /// Get the last time lapse between calls to the timer.
+    ///
+    /// This is different from [`Self::time_since_last_call`] because it remains
+    /// constant between calls to the Timer.
+    ///
+    /// It keeps track of the what the value of [`Self::time_since_last_call`]
+    /// was immediately before the most recent call to the callback. This will
+    /// be [`Duration::ZERO`] if the `Timer` has never been triggered.
+    pub fn last_elapse(&self) -> Duration {
+        *self.last_elapse.lock().unwrap()
+    }
+
+    /// Retrieves the time since the last call to the callback
+    pub fn time_since_last_call(&self) -> Result<Duration, RclrsError> {
+        let mut time_value_ns: i64 = 0;
+        unsafe {
+            let rcl_timer = self.handle.rcl_timer.lock().unwrap();
+            rcl_timer_get_time_since_last_call(&*rcl_timer, &mut time_value_ns)
+        }
+        .ok()?;
+
+        rcl_duration(time_value_ns)
+    }
+
+    /// Retrieves the time until the next call of the callback
+    pub fn time_until_next_call(&self) -> Result<Duration, RclrsError> {
+        let mut time_value_ns: i64 = 0;
+        unsafe {
+            let rcl_timer = self.handle.rcl_timer.lock().unwrap();
+            rcl_timer_get_time_until_next_call(&*rcl_timer, &mut time_value_ns)
+        }
+        .ok()?;
+
+        rcl_duration(time_value_ns)
+    }
+
+    /// Resets the timer.
+    pub fn reset(&self) -> Result<(), RclrsError> {
+        let mut rcl_timer = self.handle.rcl_timer.lock().unwrap();
+        unsafe { rcl_timer_reset(&mut *rcl_timer) }.ok()
+    }
+
+    /// Checks if the timer is ready (not canceled)
+    pub fn is_ready(&self) -> Result<bool, RclrsError> {
+        let is_ready = unsafe {
+            let mut is_ready: bool = false;
+            let rcl_timer = self.handle.rcl_timer.lock().unwrap();
+            rcl_timer_is_ready(&*rcl_timer, &mut is_ready).ok()?;
+            is_ready
+        };
+
+        Ok(is_ready)
+    }
+
+    /// Get the clock that this timer runs on.
+    pub fn clock(&self) -> &Clock {
+        &self.handle.clock
+    }
+
+    /// Set a new callback for the timer. This will return whatever callback
+    /// was already present unless you are calling the function from inside of
+    /// the timer's callback, in which case you will receive [`None`].
+    ///
+    /// See also:
+    /// * [`Self::set_repeating`]
+    /// * [`Self::set_oneshot`]
+    /// * [`Self::set_inert`].
+    pub fn set_callback(
+        &self,
+        callback: AnyTimerCallback<Scope>,
+    ) -> Option<AnyTimerCallback<Scope>> {
+        self.callback.lock().unwrap().replace(callback)
+    }
+
+    /// Remove the callback from the timer.
+    ///
+    /// This does not cancel the timer; it will continue to wake up and be
+    /// triggered at its regular period. However, nothing will happen when the
+    /// timer is triggered until you give a new callback to the timer.
+    ///
+    /// You can give the timer a new callback at any time by calling:
+    /// * [`Self::set_repeating`]
+    /// * [`Self::set_oneshot`]
+    pub fn set_inert(&self) -> Option<AnyTimerCallback<Scope>> {
+        self.set_callback(AnyTimerCallback::Inert)
+    }
+
+    /// Creates a new timer. Users should call one of [`Node::create_timer`],
+    /// [`Node::create_timer_repeating`], [`Node::create_timer_oneshot`], or
+    /// [`Node::create_timer_inert`].
+    pub(crate) fn create<'a>(
+        period: Duration,
+        clock: Clock,
+        callback: AnyTimerCallback<Scope>,
+        commands: &Arc<WorkerCommands>,
+        context: &ContextHandle,
+    ) -> Result<Arc<Self>, RclrsError> {
+        let period = period.as_nanos() as i64;
+
+        // Callbacks will be handled at the rclrs layer.
+        let rcl_timer_callback: rcl_timer_callback_t = None;
+
+        let rcl_timer = Arc::new(Mutex::new(
+            // SAFETY: Zero-initializing a timer is always safe
+            unsafe { rcl_get_zero_initialized_timer() },
+        ));
+
+        unsafe {
+            let mut rcl_clock = clock.get_rcl_clock().lock().unwrap();
+            let mut rcl_context = context.rcl_context.lock().unwrap();
+
+            // SAFETY: Getting a default value is always safe.
+            let allocator = rcutils_get_default_allocator();
+
+            let _lifecycle = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
+            // SAFETY: We lock the lifecycle mutex since rcl_timer_init is not
+            // thread-safe.
+            rcl_timer_init(
+                &mut *rcl_timer.lock().unwrap(),
+                &mut *rcl_clock,
+                &mut *rcl_context,
+                period,
+                rcl_timer_callback,
+                allocator,
+            )
+        }
+        .ok()?;
+
+        let timer = Arc::new(TimerState {
+            handle: Arc::new(TimerHandle { rcl_timer, clock }),
+            callback: Mutex::new(Some(callback)),
+            last_elapse: Mutex::new(Duration::ZERO),
+            lifecycle: Mutex::default(),
+            _ignore: Default::default(),
+        });
+
+        let (waitable, lifecycle) = Waitable::new(
+            Box::new(TimerExecutable::<Scope> {
+                timer: Arc::downgrade(&timer),
+                handle: Arc::clone(&timer.handle),
+            }),
+            Some(Arc::clone(commands.get_guard_condition())),
+        );
+
+        *timer.lifecycle.lock().unwrap() = Some(lifecycle);
+
+        commands.add_to_wait_set(waitable);
+
+        Ok(timer)
+    }
+
+    /// Force the timer to be called, even if it is not ready to be triggered yet.
+    /// We could consider making this public, but the behavior may confuse users.
+    fn call(self: &Arc<Self>, any_payload: &mut dyn Any) -> Result<(), RclrsError> {
+        // Keep track of the time elapsed since the last call. We need to run
+        // this before we trigger rcl_call.
+        let last_elapse = self.time_since_last_call().unwrap_or(Duration::ZERO);
+        *self.last_elapse.lock().unwrap() = last_elapse;
+
+        if let Err(err) = self.rcl_call() {
+            log_error!("timer", "Unable to call timer: {err:?}",);
+        }
+
+        let Some(callback) = self.callback.lock().unwrap().take() else {
+            log_error!(
+                "timer".once(),
+                "Timer is missing its callback information. This should not \
+                be possible, please report it to the maintainers of rclrs.",
+            );
+            return Ok(());
+        };
+
+        let Some(payload) = any_payload.downcast_mut::<Scope::Payload>() else {
+            return Err(RclrsError::InvalidPayload {
+                expected: std::any::TypeId::of::<Scope::Payload>(),
+                received: (*any_payload).type_id(),
+            });
+        };
+
+        match callback {
+            AnyTimerCallback::Repeating(mut callback) => {
+                callback(payload, self);
+                self.restore_callback(AnyTimerCallback::Repeating(callback).into());
+            }
+            AnyTimerCallback::OneShot(callback) => {
+                callback(payload, self);
+                self.restore_callback(AnyTimerCallback::Inert);
+            }
+            AnyTimerCallback::Inert => {
+                self.restore_callback(AnyTimerCallback::Inert);
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Updates the state of the rcl_timer to know that it has been called. This
+    /// should only be called by [`Self::call`].
+    ///
+    /// The callback held by the rcl_timer is null because we store the callback
+    /// in the [`Timer`] struct. This means there are no side-effects to this
+    /// except to keep track of when the timer has been called.
+    fn rcl_call(&self) -> Result<(), RclrsError> {
+        let mut rcl_timer = self.handle.rcl_timer.lock().unwrap();
+        unsafe { rcl_timer_call(&mut *rcl_timer) }.ok()
+    }
+
+    /// Used by [`Timer::execute`] to restore the state of the callback if and
+    /// only if the user has not already set a new callback.
+    fn restore_callback(&self, callback: AnyTimerCallback<Scope>) {
+        let mut self_callback = self.callback.lock().unwrap();
+        if self_callback.is_none() {
+            *self_callback = Some(callback);
+        }
+    }
+}
+
+impl TimerState<Node> {
+    /// Set a repeating callback for this timer.
+    ///
+    /// See also:
+    /// * [`Self::set_oneshot`]
+    /// * [`Self::set_inert`]
+    pub fn set_repeating<Args>(
+        &self,
+        f: impl IntoNodeTimerRepeatingCallback<Args>,
+    ) -> Option<AnyTimerCallback<Node>> {
+        self.set_callback(f.into_node_timer_repeating_callback())
+    }
+
+    /// Set a one-shot callback for the timer.
+    ///
+    /// The next time the timer is triggered, the callback will be set to
+    /// [`AnyTimerCallback::Inert`] after this callback is triggered. To keep the
+    /// timer useful, you can reset the Timer callback at any time, including
+    /// inside the one-shot callback itself.
+    ///
+    /// See also:
+    /// * [`Self::set_repeating`]
+    /// * [`Self::set_inert`]
+    pub fn set_oneshot<Args>(
+        &self,
+        f: impl IntoNodeTimerOneshotCallback<Args>,
+    ) -> Option<AnyTimerCallback<Node>> {
+        self.set_callback(f.into_node_timer_oneshot_callback())
+    }
+}
+
+impl<Payload: 'static + Send + Sync> TimerState<Worker<Payload>> {
+    /// Set a repeating callback for this worker timer.
+    ///
+    /// See also:
+    /// * [`Self::set_worker_oneshot`]
+    /// * [`Self::set_inert`]
+    pub fn set_worker_repeating<Args>(
+        &self,
+        f: impl IntoWorkerTimerRepeatingCallback<Worker<Payload>, Args>,
+    ) -> Option<AnyTimerCallback<Worker<Payload>>> {
+        self.set_callback(f.into_worker_timer_repeating_callback())
+    }
+
+    /// Set a one-shot callback for the worker timer.
+    ///
+    /// The next time the timer is triggered, the callback will be set to
+    /// [`AnyTimerCallback::Inert`] after this callback is triggered. To keep the
+    /// timer useful, you can reset the Timer callback at any time, including
+    /// inside the one-shot callback itself.
+    ///
+    /// See also:
+    /// * [`Self::set_worker_repeating`]
+    /// * [`Self::set_inert`]
+    pub fn set_worker_oneshot<Args>(
+        &self,
+        f: impl IntoWorkerTimerOneshotCallback<Worker<Payload>, Args>,
+    ) -> Option<AnyTimerCallback<Worker<Payload>>> {
+        self.set_callback(f.into_worker_timer_oneshot_callback())
+    }
+}
+
+struct TimerExecutable<Scope: WorkScope> {
+    timer: Weak<TimerState<Scope>>,
+    handle: Arc<TimerHandle>,
+}
+
+impl<Scope: WorkScope> RclPrimitive for TimerExecutable<Scope> {
+    unsafe fn execute(&mut self, payload: &mut dyn Any) -> Result<(), RclrsError> {
+        if let Some(timer) = self.timer.upgrade() {
+            if timer.is_ready()? {
+                timer.call(payload)?;
+            }
+        }
+
+        Ok(())
+    }
+
+    fn kind(&self) -> RclPrimitiveKind {
+        RclPrimitiveKind::Timer
+    }
+
+    fn handle(&self) -> RclPrimitiveHandle {
+        RclPrimitiveHandle::Timer(self.handle.rcl_timer.lock().unwrap())
+    }
+}
+
+impl<Scope: WorkScope> PartialEq for TimerState<Scope> {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.handle.rcl_timer, &other.handle.rcl_timer)
+    }
+}
+
+fn rcl_duration(duration_value_ns: i64) -> Result<Duration, RclrsError> {
+    if duration_value_ns < 0 {
+        Err(RclrsError::NegativeDuration(duration_value_ns))
+    } else {
+        Ok(Duration::from_nanos(duration_value_ns as u64))
+    }
+}
+
+/// Manage the lifecycle of an `rcl_timer_t`, including managing its dependency
+/// on `rcl_clock_t` by ensuring that this dependency are [dropped after][1]
+/// the `rcl_timer_t`.
+///
+/// [1]: <https://doc.rust-lang.org/reference/destructors.html>
+pub(crate) struct TimerHandle {
+    pub(crate) rcl_timer: Arc<Mutex<rcl_timer_t>>,
+    clock: Clock,
+}
+
+/// 'Drop' trait implementation to be able to release the resources
+impl Drop for TimerHandle {
+    fn drop(&mut self) {
+        let _lifecycle = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
+        // SAFETY: The lifecycle mutex is locked and the clock for the timer
+        // must still be valid because TimerHandle keeps it alive.
+        unsafe { rcl_timer_fini(&mut *self.rcl_timer.lock().unwrap()) };
+    }
+}
+
+// SAFETY: The functions accessing this type, including drop(), shouldn't care about the thread
+// they are running in. Therefore, this type can be safely sent to another thread.
+unsafe impl Send for rcl_timer_t {}
+
+#[cfg(test)]
+mod tests {
+    use super::TimerExecutable;
+    use crate::*;
+    use std::{
+        sync::{
+            atomic::{AtomicBool, Ordering},
+            Arc,
+        },
+        thread,
+        time::Duration,
+    };
+
+    #[test]
+    fn traits() {
+        use crate::test_helpers::*;
+
+        assert_send::<TimerState<Node>>();
+        assert_sync::<TimerState<Node>>();
+    }
+
+    #[test]
+    fn test_new_with_system_clock() {
+        let executor = Context::default().create_basic_executor();
+        let result = TimerState::<Node>::create(
+            Duration::from_millis(1),
+            Clock::system(),
+            (|| {}).into_node_timer_repeating_callback(),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        );
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_new_with_steady_clock() {
+        let executor = Context::default().create_basic_executor();
+        let result = TimerState::<Node>::create(
+            Duration::from_millis(1),
+            Clock::steady(),
+            (|| {}).into_node_timer_repeating_callback(),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        );
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_new_with_source_clock() {
+        let (clock, source) = Clock::with_source();
+        // No manual time set, it should default to 0
+        assert_eq!(clock.now().nsec, 0);
+        let set_time = 1234i64;
+        source.set_ros_time_override(set_time);
+
+        // ROS time is set, should return the value that was set
+        assert_eq!(clock.now().nsec, set_time);
+
+        let executor = Context::default().create_basic_executor();
+        let result = TimerState::<Node>::create(
+            Duration::from_millis(1),
+            clock,
+            (|| {}).into_node_timer_repeating_callback(),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        );
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_get_period() {
+        let period = Duration::from_millis(1);
+
+        let executor = Context::default().create_basic_executor();
+
+        let result = TimerState::<Node>::create(
+            period,
+            Clock::steady(),
+            (|| {}).into_node_timer_repeating_callback(),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        );
+
+        let timer = result.unwrap();
+        let timer_period = timer.get_timer_period().unwrap();
+        assert_eq!(timer_period, period);
+    }
+
+    #[test]
+    fn test_cancel() {
+        let executor = Context::default().create_basic_executor();
+
+        let result = TimerState::<Node>::create(
+            Duration::from_millis(1),
+            Clock::steady(),
+            (|| {}).into_node_timer_repeating_callback(),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        );
+
+        let timer = result.unwrap();
+        assert!(!timer.is_canceled().unwrap());
+        timer.cancel().unwrap();
+        assert!(timer.is_canceled().unwrap());
+    }
+
+    #[test]
+    fn test_time_since_last_call_before_first_event() {
+        let executor = Context::default().create_basic_executor();
+
+        let result = TimerState::<Node>::create(
+            Duration::from_millis(2),
+            Clock::steady(),
+            (|| {}).into_node_timer_repeating_callback(),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        );
+        let timer = result.unwrap();
+
+        let sleep_period = Duration::from_millis(1);
+        thread::sleep(sleep_period);
+
+        let time_since_last_call = timer.time_since_last_call().unwrap();
+        assert!(
+            time_since_last_call >= sleep_period,
+            "time_since_last_call: {:?} vs sleep period: {:?}",
+            time_since_last_call,
+            sleep_period,
+        );
+    }
+
+    #[test]
+    fn test_time_until_next_call_before_first_event() {
+        let executor = Context::default().create_basic_executor();
+        let period = Duration::from_millis(2);
+
+        let result = TimerState::<Node>::create(
+            period,
+            Clock::steady(),
+            (|| {}).into_node_timer_repeating_callback(),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        );
+        let timer = result.unwrap();
+
+        let time_until_next_call = timer.time_until_next_call().unwrap();
+        assert!(
+            time_until_next_call <= period,
+            "time_until_next_call: {:?} vs period: {:?}",
+            time_until_next_call,
+            period,
+        );
+    }
+
+    #[test]
+    fn test_reset() {
+        let executor = Context::default().create_basic_executor();
+        let period = Duration::from_millis(2);
+        let timer = TimerState::<Node>::create(
+            period,
+            Clock::steady(),
+            (|| {}).into_node_timer_repeating_callback(),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        )
+        .unwrap();
+
+        // The unwrap will panic if the remaining time is negative
+        timer.time_until_next_call().unwrap();
+
+        // Sleep until we're past the timer period
+        thread::sleep(Duration::from_millis(3));
+
+        // Now the time until next call should give an error
+        assert!(matches!(
+            timer.time_until_next_call(),
+            Err(RclrsError::NegativeDuration(_))
+        ));
+
+        // Reset the timer so its interval begins again
+        assert!(timer.reset().is_ok());
+
+        // The unwrap will panic if the remaining time is negative
+        timer.time_until_next_call().unwrap();
+    }
+
+    #[test]
+    fn test_call() {
+        let executor = Context::default().create_basic_executor();
+        let timer = TimerState::<Node>::create(
+            Duration::from_millis(1),
+            Clock::steady(),
+            (|| {}).into_node_timer_repeating_callback(),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        )
+        .unwrap();
+
+        // The unwrap will panic if the remaining time is negative
+        timer.time_until_next_call().unwrap();
+
+        // Sleep until we're past the timer period
+        thread::sleep(Duration::from_micros(1500));
+
+        // Now the time until the next call should give an error
+        assert!(matches!(
+            timer.time_until_next_call(),
+            Err(RclrsError::NegativeDuration(_))
+        ));
+
+        // The unwrap will panic if anything went wrong with the call
+        timer.call(&mut ()).unwrap();
+
+        // The unwrap will panic if the remaining time is negative
+        timer.time_until_next_call().unwrap();
+    }
+
+    #[test]
+    fn test_is_ready() {
+        let executor = Context::default().create_basic_executor();
+        let timer = TimerState::<Node>::create(
+            Duration::from_millis(1),
+            Clock::steady(),
+            (|| {}).into_node_timer_repeating_callback(),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        )
+        .unwrap();
+
+        assert!(!timer.is_ready().unwrap());
+
+        // Sleep until the period has elapsed
+        thread::sleep(Duration::from_micros(1100));
+
+        assert!(timer.is_ready().unwrap());
+    }
+
+    #[test]
+    fn test_callback() {
+        let clock = Clock::steady();
+        let initial_time = clock.now();
+
+        let executor = Context::default().create_basic_executor();
+        let executed = Arc::new(AtomicBool::new(false));
+
+        let timer = TimerState::<Node>::create(
+            Duration::from_millis(1),
+            clock,
+            create_timer_callback_for_testing(initial_time, Arc::clone(&executed)),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        )
+        .unwrap();
+
+        timer.call(&mut ()).unwrap();
+        assert!(executed.load(Ordering::Acquire));
+    }
+
+    #[test]
+    fn test_execute_when_is_not_ready() {
+        let clock = Clock::steady();
+        let initial_time = clock.now();
+
+        let executor = Context::default().create_basic_executor();
+        let executed = Arc::new(AtomicBool::new(false));
+
+        let timer = TimerState::<Node>::create(
+            Duration::from_millis(1),
+            clock,
+            create_timer_callback_for_testing(initial_time, Arc::clone(&executed)),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        )
+        .unwrap();
+
+        let mut executable = TimerExecutable {
+            timer: Arc::downgrade(&timer),
+            handle: Arc::clone(&timer.handle),
+        };
+
+        // SAFETY: Node timers expect a payload of ()
+        unsafe {
+            executable.execute(&mut ()).unwrap();
+        }
+        assert!(!executed.load(Ordering::Acquire));
+    }
+
+    #[test]
+    fn test_execute_when_is_ready() {
+        let clock = Clock::steady();
+        let initial_time = clock.now();
+
+        let executor = Context::default().create_basic_executor();
+        let executed = Arc::new(AtomicBool::new(false));
+
+        let timer = TimerState::<Node>::create(
+            Duration::from_millis(1),
+            clock,
+            create_timer_callback_for_testing(initial_time, Arc::clone(&executed)),
+            executor.commands().async_worker_commands(),
+            &executor.commands().context().handle,
+        )
+        .unwrap();
+
+        let mut executable = TimerExecutable {
+            timer: Arc::downgrade(&timer),
+            handle: Arc::clone(&timer.handle),
+        };
+
+        thread::sleep(Duration::from_millis(2));
+
+        // SAFETY: Node timers expect a payload of ()
+        unsafe {
+            executable.execute(&mut ()).unwrap();
+        }
+        assert!(executed.load(Ordering::Acquire));
+    }
+
+    fn create_timer_callback_for_testing(
+        initial_time: Time,
+        executed: Arc<AtomicBool>,
+    ) -> AnyTimerCallback<Node> {
+        (move |t: Time| {
+            assert!(t
+                .compare_with(&initial_time, |t, initial| t >= initial)
+                .unwrap());
+            executed.store(true, Ordering::Release);
+        })
+        .into_node_timer_oneshot_callback()
+    }
+}
diff --git a/rclrs/src/timer/any_timer_callback.rs b/rclrs/src/timer/any_timer_callback.rs
new file mode 100644
index 000000000..3c4493d72
--- /dev/null
+++ b/rclrs/src/timer/any_timer_callback.rs
@@ -0,0 +1,15 @@
+use crate::{TimerState, WorkScope};
+use std::sync::Arc;
+
+/// A callback that can be triggered when a timer elapses.
+pub enum AnyTimerCallback<Scope: WorkScope> {
+    /// This callback will be triggered repeatedly, each time the period of the
+    /// timer elapses.
+    Repeating(Box<dyn FnMut(&mut Scope::Payload, &Arc<TimerState<Scope>>) + Send>),
+    /// This callback will be triggered exactly once, the first time the period
+    /// of the timer elapses.
+    OneShot(Box<dyn FnOnce(&mut Scope::Payload, &Arc<TimerState<Scope>>) + Send>),
+    /// Do nothing when the timer elapses. This can be replaced later so that
+    /// the timer does something.
+    Inert,
+}
diff --git a/rclrs/src/timer/into_node_timer_callback.rs b/rclrs/src/timer/into_node_timer_callback.rs
new file mode 100644
index 000000000..5ef3772fb
--- /dev/null
+++ b/rclrs/src/timer/into_node_timer_callback.rs
@@ -0,0 +1,67 @@
+use crate::{AnyTimerCallback, Node, Time, Timer};
+
+/// This trait is used to create timer callbacks for repeating timers in a Node.
+pub trait IntoNodeTimerRepeatingCallback<Args>: 'static + Send {
+    /// Convert a suitable object into a repeating timer callback for the node scope
+    fn into_node_timer_repeating_callback(self) -> AnyTimerCallback<Node>;
+}
+
+impl<Func> IntoNodeTimerRepeatingCallback<()> for Func
+where
+    Func: FnMut() + 'static + Send,
+{
+    fn into_node_timer_repeating_callback(mut self) -> AnyTimerCallback<Node> {
+        AnyTimerCallback::Repeating(Box::new(move |_, _| self())).into()
+    }
+}
+
+impl<Func> IntoNodeTimerRepeatingCallback<Timer> for Func
+where
+    Func: FnMut(&Timer) + 'static + Send,
+{
+    fn into_node_timer_repeating_callback(mut self) -> AnyTimerCallback<Node> {
+        AnyTimerCallback::Repeating(Box::new(move |_, t| self(t))).into()
+    }
+}
+
+impl<Func> IntoNodeTimerRepeatingCallback<Time> for Func
+where
+    Func: FnMut(Time) + 'static + Send,
+{
+    fn into_node_timer_repeating_callback(mut self) -> AnyTimerCallback<Node> {
+        AnyTimerCallback::Repeating(Box::new(move |_, t| self(t.handle.clock.now()))).into()
+    }
+}
+
+/// This trait is used to create timer callbacks for one-shot timers in a Node.
+pub trait IntoNodeTimerOneshotCallback<Args>: 'static + Send {
+    /// Convert a suitable object into a one-shot timer callback for a node scope
+    fn into_node_timer_oneshot_callback(self) -> AnyTimerCallback<Node>;
+}
+
+impl<Func> IntoNodeTimerOneshotCallback<()> for Func
+where
+    Func: FnOnce() + 'static + Send,
+{
+    fn into_node_timer_oneshot_callback(self) -> AnyTimerCallback<Node> {
+        AnyTimerCallback::OneShot(Box::new(move |_, _| self())).into()
+    }
+}
+
+impl<Func> IntoNodeTimerOneshotCallback<Timer> for Func
+where
+    Func: FnOnce(&Timer) + 'static + Send,
+{
+    fn into_node_timer_oneshot_callback(self) -> AnyTimerCallback<Node> {
+        AnyTimerCallback::OneShot(Box::new(move |_, t| self(t))).into()
+    }
+}
+
+impl<Func> IntoNodeTimerOneshotCallback<Time> for Func
+where
+    Func: FnOnce(Time) + 'static + Send,
+{
+    fn into_node_timer_oneshot_callback(self) -> AnyTimerCallback<Node> {
+        AnyTimerCallback::OneShot(Box::new(move |_, t| self(t.handle.clock.now()))).into()
+    }
+}
diff --git a/rclrs/src/timer/into_worker_timer_callback.rs b/rclrs/src/timer/into_worker_timer_callback.rs
new file mode 100644
index 000000000..bdb752411
--- /dev/null
+++ b/rclrs/src/timer/into_worker_timer_callback.rs
@@ -0,0 +1,95 @@
+use crate::{AnyTimerCallback, Time, TimerState, WorkScope};
+use std::sync::Arc;
+
+/// This trait is used to create timer callbacks for repeating timers in a Worker.
+pub trait IntoWorkerTimerRepeatingCallback<Scope: WorkScope, Args>: 'static + Send {
+    /// Convert a suitable object into a repeating timer callback for a worker scope
+    fn into_worker_timer_repeating_callback(self) -> AnyTimerCallback<Scope>;
+}
+
+impl<Scope: WorkScope, Func> IntoWorkerTimerRepeatingCallback<Scope, ()> for Func
+where
+    Func: FnMut() + 'static + Send,
+{
+    fn into_worker_timer_repeating_callback(mut self) -> AnyTimerCallback<Scope> {
+        AnyTimerCallback::Repeating(Box::new(move |_, _| self())).into()
+    }
+}
+
+impl<Scope: WorkScope, Func> IntoWorkerTimerRepeatingCallback<Scope, (Scope::Payload,)> for Func
+where
+    Func: FnMut(&mut Scope::Payload) + 'static + Send,
+{
+    fn into_worker_timer_repeating_callback(mut self) -> AnyTimerCallback<Scope> {
+        AnyTimerCallback::Repeating(Box::new(move |payload, _| self(payload))).into()
+    }
+}
+
+impl<Scope: WorkScope, Func>
+    IntoWorkerTimerRepeatingCallback<Scope, (Scope::Payload, Arc<TimerState<Scope>>)> for Func
+where
+    Func: FnMut(&mut Scope::Payload, &Arc<TimerState<Scope>>) + 'static + Send,
+{
+    fn into_worker_timer_repeating_callback(self) -> AnyTimerCallback<Scope> {
+        AnyTimerCallback::Repeating(Box::new(self)).into()
+    }
+}
+
+impl<Scope: WorkScope, Func> IntoWorkerTimerRepeatingCallback<Scope, (Scope::Payload, Time)>
+    for Func
+where
+    Func: FnMut(&mut Scope::Payload, Time) + 'static + Send,
+{
+    fn into_worker_timer_repeating_callback(mut self) -> AnyTimerCallback<Scope> {
+        AnyTimerCallback::Repeating(Box::new(move |payload, t| {
+            self(payload, t.handle.clock.now())
+        }))
+        .into()
+    }
+}
+
+/// This trait is used to create timer callbacks for one-shot timers in a Worker.
+pub trait IntoWorkerTimerOneshotCallback<Scope: WorkScope, Args>: 'static + Send {
+    /// Convert a suitable object into a one-shot timer callback for a worker scope
+    fn into_worker_timer_oneshot_callback(self) -> AnyTimerCallback<Scope>;
+}
+
+impl<Scope: WorkScope, Func> IntoWorkerTimerOneshotCallback<Scope, ()> for Func
+where
+    Func: FnOnce() + 'static + Send,
+{
+    fn into_worker_timer_oneshot_callback(self) -> AnyTimerCallback<Scope> {
+        AnyTimerCallback::OneShot(Box::new(move |_, _| self())).into()
+    }
+}
+
+impl<Scope: WorkScope, Func> IntoWorkerTimerOneshotCallback<Scope, (Scope::Payload,)> for Func
+where
+    Func: FnOnce(&mut Scope::Payload) + 'static + Send,
+{
+    fn into_worker_timer_oneshot_callback(self) -> AnyTimerCallback<Scope> {
+        AnyTimerCallback::OneShot(Box::new(move |payload, _| self(payload))).into()
+    }
+}
+
+impl<Scope: WorkScope, Func>
+    IntoWorkerTimerOneshotCallback<Scope, (Scope::Payload, Arc<TimerState<Scope>>)> for Func
+where
+    Func: FnOnce(&mut Scope::Payload, &Arc<TimerState<Scope>>) + 'static + Send,
+{
+    fn into_worker_timer_oneshot_callback(self) -> AnyTimerCallback<Scope> {
+        AnyTimerCallback::OneShot(Box::new(self)).into()
+    }
+}
+
+impl<Scope: WorkScope, Func> IntoWorkerTimerOneshotCallback<Scope, (Scope::Payload, Time)> for Func
+where
+    Func: FnMut(&mut Scope::Payload, Time) + 'static + Send,
+{
+    fn into_worker_timer_oneshot_callback(mut self) -> AnyTimerCallback<Scope> {
+        AnyTimerCallback::OneShot(Box::new(move |payload, t| {
+            self(payload, t.handle.clock.now())
+        }))
+        .into()
+    }
+}
diff --git a/rclrs/src/timer/timer_options.rs b/rclrs/src/timer/timer_options.rs
new file mode 100644
index 000000000..81bd34164
--- /dev/null
+++ b/rclrs/src/timer/timer_options.rs
@@ -0,0 +1,100 @@
+use std::time::Duration;
+
+use crate::{Clock, NodeState};
+
+/// Options for creating a [`Timer`][crate::Timer].
+///
+/// The trait [`IntoTimerOptions`] can implicitly convert a single [`Duration`]
+/// into `TimerOptions`.
+#[derive(Debug, Clone, Copy)]
+#[non_exhaustive]
+pub struct TimerOptions<'a> {
+    /// The period of the timer's interval
+    pub period: Duration,
+    /// The clock that the timer will reference for its intervals
+    pub clock: TimerClock<'a>,
+}
+
+impl TimerOptions<'_> {
+    /// Make a new timer with a certain interval period, with all other options
+    /// as default.
+    pub fn new(period: Duration) -> Self {
+        Self {
+            period,
+            clock: TimerClock::default(),
+        }
+    }
+}
+
+/// Trait to implicitly convert a suitable object into [`TimerOptions`].
+pub trait IntoTimerOptions<'a>: Sized {
+    /// Convert a suitable object into [`TimerOptions`]. This can be used on
+    /// [`Duration`] or [`TimerOptions`] itself.
+    fn into_timer_options(self) -> TimerOptions<'a>;
+
+    /// Use [`std::time::Instant`] for the timer. This the default so you
+    /// shouldn't generally have to call this.
+    fn steady_time(self) -> TimerOptions<'a> {
+        let mut options = self.into_timer_options();
+        options.clock = TimerClock::SteadyTime;
+        options
+    }
+
+    /// Use [`std::time::SystemTime`] for the timer
+    fn system_time(self) -> TimerOptions<'a> {
+        let mut options = self.into_timer_options();
+        options.clock = TimerClock::SystemTime;
+        options
+    }
+
+    /// Use the node clock for the timer
+    fn node_time(self) -> TimerOptions<'a> {
+        let mut options = self.into_timer_options();
+        options.clock = TimerClock::NodeTime;
+        options
+    }
+
+    /// Use a specific clock for the
+    fn clock(self, clock: &'a Clock) -> TimerOptions<'a> {
+        let mut options = self.into_timer_options();
+        options.clock = TimerClock::Clock(clock);
+        options
+    }
+}
+
+/// This parameter can specify a type of clock for a timer to use
+#[derive(Debug, Default, Clone, Copy)]
+pub enum TimerClock<'a> {
+    /// Use [`std::time::Instant`] for tracking time
+    #[default]
+    SteadyTime,
+    /// Use [`std::time::SystemTime`] for tracking time
+    SystemTime,
+    /// Use the parent node's clock for tracking time
+    NodeTime,
+    /// Use a specific clock for tracking time
+    Clock(&'a Clock),
+}
+
+impl TimerClock<'_> {
+    pub(crate) fn as_clock(&self, node: &NodeState) -> Clock {
+        match self {
+            TimerClock::SteadyTime => Clock::steady(),
+            TimerClock::SystemTime => Clock::system(),
+            TimerClock::NodeTime => node.get_clock(),
+            TimerClock::Clock(clock) => (*clock).clone(),
+        }
+    }
+}
+
+impl<'a> IntoTimerOptions<'a> for TimerOptions<'a> {
+    fn into_timer_options(self) -> TimerOptions<'a> {
+        self
+    }
+}
+
+impl<'a> IntoTimerOptions<'a> for Duration {
+    fn into_timer_options(self) -> TimerOptions<'a> {
+        TimerOptions::new(self)
+    }
+}
diff --git a/rclrs/src/worker.rs b/rclrs/src/worker.rs
index f89da20fc..3c4af1d87 100644
--- a/rclrs/src/worker.rs
+++ b/rclrs/src/worker.rs
@@ -1,7 +1,9 @@
 use crate::{
-    log_fatal, IntoWorkerServiceCallback, IntoWorkerSubscriptionCallback, Node, Promise,
-    RclrsError, ServiceOptions, ServiceState, SubscriptionOptions, SubscriptionState,
-    WorkerCommands, WorkerService, WorkerSubscription,
+    log_fatal, AnyTimerCallback, IntoTimerOptions, IntoWorkerServiceCallback,
+    IntoWorkerSubscriptionCallback, IntoWorkerTimerOneshotCallback,
+    IntoWorkerTimerRepeatingCallback, Node, Promise, RclrsError, ServiceOptions, ServiceState,
+    SubscriptionOptions, SubscriptionState, TimerState, WorkerCommands, WorkerService,
+    WorkerSubscription, WorkerTimer,
 };
 use futures::channel::oneshot;
 use rosidl_runtime_rs::{Message, Service as IdlService};
@@ -369,6 +371,65 @@ impl<Payload: 'static + Send + Sync> WorkerState<Payload> {
         )
     }
 
+    /// Create a [`WorkerTimer`] with a repeating callback.
+    ///
+    /// See also:
+    /// * [`Self::create_timer_oneshot`]
+    /// * [`Self::create_timer_inert`]
+    pub fn create_timer_repeating<'a, Args>(
+        &self,
+        options: impl IntoTimerOptions<'a>,
+        callback: impl IntoWorkerTimerRepeatingCallback<Worker<Payload>, Args>,
+    ) -> Result<WorkerTimer<Payload>, RclrsError> {
+        self.create_timer(options, callback.into_worker_timer_repeating_callback())
+    }
+
+    /// Create a [`WorkerTimer`] whose callback will be triggered once after the
+    /// period of the timer has elapsed. After that you will need to use
+    /// [`WorkerTimer::set_worker_oneshot`] or [`WorkerTimer::set_worker_repeating`]
+    /// or else nothing will happen the following times that the `Timer` elapses.
+    ///
+    /// See also:
+    /// * [`Self::create_timer_repeating`]
+    /// * [`Self::create_time_inert`]
+    pub fn create_timer_oneshot<'a, Args>(
+        &self,
+        options: impl IntoTimerOptions<'a>,
+        callback: impl IntoWorkerTimerOneshotCallback<Worker<Payload>, Args>,
+    ) -> Result<WorkerTimer<Payload>, RclrsError> {
+        self.create_timer(options, callback.into_worker_timer_oneshot_callback())
+    }
+
+    /// Create a [`WorkerTimer`] without a callback. Nothing will happen when this
+    /// `WorkerTimer` elapses until you use [`WorkerTimer::set_worker_repeating`]
+    /// or [`WorkerTimer::set_worker_oneshot`].
+    ///
+    /// See also:
+    /// * [`Self::create_timer_repeating`]
+    /// * [`Self::create_timer_oneshot`]
+    pub fn create_timer_inert<'a>(
+        &self,
+        options: impl IntoTimerOptions<'a>,
+    ) -> Result<WorkerTimer<Payload>, RclrsError> {
+        self.create_timer(options, AnyTimerCallback::Inert)
+    }
+
+    fn create_timer<'a>(
+        &self,
+        options: impl IntoTimerOptions<'a>,
+        callback: AnyTimerCallback<Worker<Payload>>,
+    ) -> Result<WorkerTimer<Payload>, RclrsError> {
+        let options = options.into_timer_options();
+        let clock = options.clock.as_clock(&*self.node);
+        TimerState::create(
+            options.period,
+            clock,
+            callback,
+            &self.commands,
+            &self.node.handle().context_handle,
+        )
+    }
+
     /// Used by [`Node`][crate::Node] to create a `WorkerState`. Users should
     /// call [`Node::create_worker`][crate::NodeState::create_worker] instead of
     /// this.

From c9c2127b4fa90888b221444524d61fdb295d1a37 Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Sun, 29 Jun 2025 15:55:27 +0800
Subject: [PATCH 02/12] Update documentation of timers

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 examples/timer_demo/Cargo.lock                | 973 ++++++++++++++++++
 rclrs/src/node.rs                             | 152 ++-
 .../subscription/readonly_loaned_message.rs   |   2 -
 rclrs/src/timer.rs                            |  26 +-
 rclrs/src/worker.rs                           | 134 ++-
 5 files changed, 1269 insertions(+), 18 deletions(-)
 create mode 100644 examples/timer_demo/Cargo.lock

diff --git a/examples/timer_demo/Cargo.lock b/examples/timer_demo/Cargo.lock
new file mode 100644
index 000000000..c342f0f78
--- /dev/null
+++ b/examples/timer_demo/Cargo.lock
@@ -0,0 +1,973 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "action_msgs"
+version = "2.0.2"
+dependencies = [
+ "builtin_interfaces",
+ "rosidl_runtime_rs",
+ "service_msgs",
+ "unique_identifier_msgs",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
+dependencies = [
+ "async-channel 2.3.1",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3"
+dependencies = [
+ "async-lock",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
+dependencies = [
+ "event-listener 5.4.0",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-std"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24"
+dependencies = [
+ "async-channel 1.9.0",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "bindgen"
+version = "0.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "blocking"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
+dependencies = [
+ "async-channel 2.3.1",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "builtin_interfaces"
+version = "2.0.2"
+dependencies = [
+ "rosidl_runtime_rs",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "errno"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "event-listener"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener 5.4.0",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "example_interfaces"
+version = "0.12.0"
+dependencies = [
+ "action_msgs",
+ "builtin_interfaces",
+ "rosidl_runtime_rs",
+ "service_msgs",
+ "unique_identifier_msgs",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "libloading"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+dependencies = [
+ "value-bag",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "polling"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rclrs"
+version = "0.4.1"
+dependencies = [
+ "async-std",
+ "bindgen",
+ "cfg-if",
+ "futures",
+ "rosidl_runtime_rs",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rosidl_runtime_rs"
+version = "0.4.1"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "service_msgs"
+version = "2.0.2"
+dependencies = [
+ "builtin_interfaces",
+ "rosidl_runtime_rs",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "slab"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "timer_demo"
+version = "0.1.0"
+dependencies = [
+ "example_interfaces",
+ "rclrs",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unique_identifier_msgs"
+version = "2.5.0"
+dependencies = [
+ "rosidl_runtime_rs",
+]
+
+[[package]]
+name = "value-bag"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[patch.unused]]
+name = "rcl_interfaces"
+version = "2.0.2"
+
+[[patch.unused]]
+name = "rclrs_example_msgs"
+version = "0.4.1"
+
+[[patch.unused]]
+name = "rosgraph_msgs"
+version = "2.0.2"
+
+[[patch.unused]]
+name = "std_msgs"
+version = "5.3.6"
+
+[[patch.unused]]
+name = "test_msgs"
+version = "2.0.2"
diff --git a/rclrs/src/node.rs b/rclrs/src/node.rs
index 1a0c67224..777e38981 100644
--- a/rclrs/src/node.rs
+++ b/rclrs/src/node.rs
@@ -900,6 +900,110 @@ impl NodeState {
     /// See also:
     /// * [`Self::create_timer_oneshot`]
     /// * [`Self::create_timer_inert`]
+    ///
+    /// # Behavior
+    ///
+    /// While the callback of this timer is running, no other callbacks associated
+    /// with this node will be able to run. This is in contrast to callbacks given
+    /// to [`Self::create_subscription`] which can run multiple times in parallel.
+    ///
+    /// Since the callback of this timer may block other callbacks from being able
+    /// to run, it is strongly recommended to ensure that the callback returns
+    /// quickly. If the callback needs to trigger long-running behavior then you
+    /// can consider using [`std::thread::spawn`], or for async behaviors you can
+    /// capture an [`ExecutorCommands`] in your callback and use [`ExecutorCommands::run`]
+    /// to issue a task for the executor to run in its async task pool.
+    ///
+    /// Since these callbacks are blocking, you may use [`FnMut`] here instead of
+    /// being limited to [`Fn`].
+    ///
+    /// # Timer Options
+    ///
+    /// You can choose both
+    /// 1. a timer period (duration) which determines how often the callback is triggered
+    /// 2. a clock to measure the passage of time
+    ///
+    /// Both of these choices are expressed by [`TimerOptions`][1].
+    ///
+    /// By default the steady clock time will be used, but you could choose
+    /// node time instead if you want the timer to automatically use simulated
+    /// time when running as part of a simulation:
+    /// ```
+    /// # use rclrs::*;
+    /// # let executor = Context::default().create_basic_executor();
+    /// # let node = executor.create_node("my_node").unwrap();
+    /// use std::time::Duration;
+    ///
+    /// let timer = node.create_timer_repeating(
+    ///     TimerOptions::new(Duration::from_secs(1))
+    ///     .node_time(),
+    ///     || {
+    ///         println!("Triggering once each simulated second");
+    ///     },
+    /// )?;
+    /// # Ok::<(), RclrsError>(())
+    /// ```
+    ///
+    /// If there is a specific manually-driven clock you want to use, you can
+    /// also select that:
+    /// ```
+    /// # use rclrs::*;
+    /// # let executor = Context::default().create_basic_executor();
+    /// # let node = executor.create_node("my_node").unwrap();
+    /// use std::time::Duration;
+    ///
+    /// let (my_clock, my_source) = Clock::with_source();
+    ///
+    /// let timer = node.create_timer_repeating(
+    ///     TimerOptions::new(Duration::from_secs(1))
+    ///     .clock(&my_clock),
+    ///     || {
+    ///         println!("Triggering once each simulated second");
+    ///     },
+    /// )?;
+    ///
+    /// my_source.set_ros_time_override(1_500_000_000);
+    /// # Ok::<(), RclrsError>(())
+    /// ```
+    ///
+    /// If you are okay with the default choice of clock (steady clock) then you
+    /// can choose to simply pass a duration in as the options:
+    /// ```
+    /// # use rclrs::*;
+    /// # let executor = Context::default().create_basic_executor();
+    /// # let node = executor.create_node("my_node").unwrap();
+    /// use std::time::Duration;
+    ///
+    /// let timer = node.create_timer_repeating(
+    ///     Duration::from_secs(1),
+    ///     || {
+    ///         println!("Triggering per steady clock second");
+    ///     },
+    /// )?;
+    /// # Ok::<(), RclrsError>(())
+    /// ```
+    ///
+    /// # Node Timer Repeating Callbacks
+    ///
+    /// Node Timer repeating callbacks support three signatures:
+    /// - <code>[FnMut] ()</code>
+    /// - <code>[FnMut] ([Time][2])</code>
+    /// - <code>[FnMut] (&[Timer])</code>
+    ///
+    /// You can choose to receive the current time when the callback is being
+    /// triggered.
+    ///
+    /// Or instead of the current time, you can get a borrow of the [`Timer`]
+    /// itself, that way if you need to access it from inside the callback, you
+    /// do not need to worry about capturing a [`Weak`][3] and then locking it.
+    /// This is useful if you need to change the callback of the timer from inside
+    /// the callback of the timer.
+    ///
+    /// For an [`FnOnce`] instead of [`FnMut`], use [`Self::create_timer_oneshot`].
+    ///
+    /// [1]: crate::TimerOptions
+    /// [2]: crate::Time
+    /// [3]: std::sync::Weak
     pub fn create_timer_repeating<'a, Args>(
         &self,
         options: impl IntoTimerOptions<'a>,
@@ -910,12 +1014,51 @@ impl NodeState {
 
     /// Create a [`Timer`] whose callback will be triggered once after the period
     /// of the timer has elapsed. After that you will need to use
-    /// [`Timer::set_repeating`] or [`Timer::set_oneshot`] or else nothing will happen
-    /// the following times that the `Timer` elapses.
+    /// [`TimerState::set_repeating`] or [`TimerState::set_oneshot`] or else
+    /// nothing will happen the following times that the `Timer` elapses.
     ///
     /// See also:
     /// * [`Self::create_timer_repeating`]
-    /// * [`Self::create_time_inert`]
+    /// * [`Self::create_timer_inert`]
+    ///
+    /// # Behavior
+    ///
+    /// While the callback of this timer is running, no other callbacks associated
+    /// with this node will be able to run. This is in contrast to callbacks given
+    /// to [`Self::create_subscription`] which can run multiple times in parallel.
+    ///
+    /// Since the callback of this timer may block other callbacks from being able
+    /// to run, it is strongly recommended to ensure that the callback returns
+    /// quickly. If the callback needs to trigger long-running behavior then you
+    /// can consider using [`std::thread::spawn`], or for async behaviors you can
+    /// capture an [`ExecutorCommands`] in your callback and use [`ExecutorCommands::run`]
+    /// to issue a task for the executor to run in its async task pool.
+    ///
+    /// Since these callbacks will only be triggered once, you may use [`FnOnce`] here.
+    ///
+    /// # Timer Options
+    ///
+    /// See [`NodeSate::create_timer_repeating`][3] for examples of setting the
+    /// timer options.
+    ///
+    /// # Node Timer Oneshot Callbacks
+    ///
+    /// Node Timer repeating callbacks support three signatures:
+    /// - <code>[FnMut] ()</code>
+    /// - <code>[FnMut] ([Time][2])</code>
+    /// - <code>[FnMut] (&[Timer])</code>
+    ///
+    /// You can choose to receive the current time when the callback is being
+    /// triggered.
+    ///
+    /// Or instead of the current time, you can get a borrow of the [`Timer`]
+    /// itself, that way if you need to access it from inside the callback, you
+    /// do not need to worry about capturing a [`Weak`][3] and then locking it.
+    /// This is useful if you need to change the callback of the timer from inside
+    /// the callback of the timer.
+    ///
+    /// [2]: crate::Time
+    /// [3]: std::sync::Weak
     pub fn create_timer_oneshot<'a, Args>(
         &self,
         options: impl IntoTimerOptions<'a>,
@@ -925,7 +1068,8 @@ impl NodeState {
     }
 
     /// Create a [`Timer`] without a callback. Nothing will happen when this
-    /// `Timer` elapses until you use [`Timer::set_callback`] or a related method.
+    /// `Timer` elapses until you use [`TimerState::set_repeating`] or
+    /// [`TimerState::set_oneshot`].
     ///
     /// See also:
     /// * [`Self::create_timer_repeating`]
diff --git a/rclrs/src/subscription/readonly_loaned_message.rs b/rclrs/src/subscription/readonly_loaned_message.rs
index 67ac3fd69..0ea73c1f2 100644
--- a/rclrs/src/subscription/readonly_loaned_message.rs
+++ b/rclrs/src/subscription/readonly_loaned_message.rs
@@ -12,8 +12,6 @@ use crate::{rcl_bindings::*, subscription::SubscriptionHandle, ToResult};
 ///
 /// This type may be used in subscription callbacks to receive a message. The
 /// loan is returned by dropping the `ReadOnlyLoanedMessage`.
-///
-/// [1]: crate::SubscriptionState::take_loaned
 pub struct ReadOnlyLoanedMessage<T>
 where
     T: Message,
diff --git a/rclrs/src/timer.rs b/rclrs/src/timer.rs
index 934060fd1..a6cd23ef6 100644
--- a/rclrs/src/timer.rs
+++ b/rclrs/src/timer.rs
@@ -28,24 +28,34 @@ pub use into_worker_timer_callback::*;
 /// The executor needs to be [spinning][1] for a timer's callback to be triggered.
 ///
 /// Timers can be created by a [`Node`] using one of these methods:
-/// - [`NodeState::create_timer_repeating`]
-/// - [`NodeState::create_timer_oneshot`]
-/// - [`NodeState::create_timer_inert`]
+/// - [`NodeState::create_timer_repeating`][2]
+/// - [`NodeState::create_timer_oneshot`][3]
+/// - [`NodeState::create_timer_inert`][4]
 ///
 /// Timers can also be created by a [`Worker`], in which case they can access the worker's payload:
-/// - [`Worker::create_timer_repeating`]
-/// - [`Worker::create_timer_oneshot`]
-/// - [`Worker::create_timer_inert`]
+/// - [`WorkerState::create_timer_repeating`][5]
+/// - [`WorkerState::create_timer_oneshot`][6]
+/// - [`WorkerState::create_timer_inert`][7]
 ///
 /// The API of timers is given by [`TimerState`].
 ///
 /// [1]: crate::Executor::spin
+/// [2]: crate::NodeState::create_timer_repeating
+/// [3]: crate::NodeState::create_timer_oneshot
+/// [4]: crate::NodeState::create_timer_inert
+/// [5]: crate::WorkerState::create_timer_repeating
+/// [6]: crate::WorkerState::create_timer_oneshot
+/// [7]: crate::WorkerState::create_timer_inert
 pub type Timer = Arc<TimerState<Node>>;
 
 /// A [`Timer`] that runs on a [`Worker`].
 ///
-/// Create a worker timer using [`Worker::create_timer_repeating`],
-/// [`Worker::create_timer_oneshot`], or [`Worker::create_timer_inert`].
+/// Create a worker timer using [`create_timer_repeating`][1],
+/// [`create_timer_oneshot`][2], or [`create_timer_inert`][3].
+///
+/// [1]: crate::WorkerState::create_timer_repeating
+/// [2]: crate::WorkerState::create_timer_oneshot
+/// [3]: crate::WorkerState::create_timer_inert
 pub type WorkerTimer<Payload> = Arc<TimerState<Worker<Payload>>>;
 
 /// The inner state of a [`Timer`].
diff --git a/rclrs/src/worker.rs b/rclrs/src/worker.rs
index 3c4af1d87..aa134b77a 100644
--- a/rclrs/src/worker.rs
+++ b/rclrs/src/worker.rs
@@ -373,9 +373,74 @@ impl<Payload: 'static + Send + Sync> WorkerState<Payload> {
 
     /// Create a [`WorkerTimer`] with a repeating callback.
     ///
+    /// Unlike timers created from a [`Node`], the callbacks for these timers
+    /// can have an additional argument to get a mutable borrow of the [`Worker`]'s
+    /// payload. This allows the timer callback to operate on state data that gets
+    /// shared with other callbacks.
+    ///
     /// See also:
     /// * [`Self::create_timer_oneshot`]
     /// * [`Self::create_timer_inert`]
+    ///
+    /// # Behavior
+    ///
+    /// While the callback of this timer is running, no other callbacks associated
+    /// with the [`Worker`] will be able to run. This is necessary to guarantee
+    /// that the mutable borrow of the payload is safe.
+    ///
+    /// Since the callback of this timer may block other callbacks from being
+    /// able to run, it is strongly recommended to ensure that the callback
+    /// returns quickly. If the callback needs to trigger long-running behavior
+    /// then you can condier using [`std::thread::spawn`], or for async behaviors
+    /// you can capture an [`ExecutorCommands`][1] in your callback and use
+    /// [`ExecutorCommands::run`][2] to issue a task for the executor to run in
+    /// its async task pool.
+    ///
+    /// # Timer Options
+    ///
+    /// See [`NodeState::create_timer_repeating`][3] for examples of setting the
+    /// timer options.
+    ///
+    /// # Worker Timer Repeating Callbacks
+    ///
+    /// Worker Timer repeating callbacks support four signatures:
+    /// - <code>[FnMut] ()</code>
+    /// - <code>[FnMut] ( &mut Payload )</code>
+    /// - <code>[FnMut] ( &mut Payload, [Time][4] )</code>
+    /// - <code>[FnMut] ( &mut Payload, &[WorkerTimer]&lt;Payload&gt; )</code>
+    ///
+    /// You can choose to access the payload of the worker. You can additionally
+    /// choose to receive the current time when the callback is being triggered.
+    ///
+    /// Or instead of the current time, you can get a borrow of the [`WorkerTimer`]
+    /// itself, that way if you need to access it from inside the callback, you
+    /// do not need to worry about capturing a [`Weak`] and then locking it. This
+    /// is useful if you need to change the callback of the timer from inside the
+    /// callback of the timer.
+    ///
+    /// For an [`FnOnce`] callback instead of [`FnMut`], use [`Self::create_timer_oneshot`].
+    ///
+    /// ```
+    /// # use rclrs::*;
+    /// # let executor = Context::default().create_basic_executor();
+    /// # let node = executor.create_node("my_node").unwrap();
+    /// use std::time::Duration;
+    ///
+    /// let worker = node.create_worker::<usize>(0);
+    /// let timer = worker.create_timer_repeating(
+    ///     Duration::from_secs(1),
+    ///     |count: &mut usize, time: Time| {
+    ///         *count += 1;
+    ///         println!("Drinking my {}th 🧉 at {:?}.", *count, time);
+    ///     },
+    /// )?;
+    /// # Ok::<(), RclrsError>(())
+    /// ```
+    ///
+    /// [1]: crate::ExecutorCommands
+    /// [2]: crate::ExecutorCommands::run
+    /// [3]: crate::NodeState::create_timer_repeating
+    /// [4]: crate::Time
     pub fn create_timer_repeating<'a, Args>(
         &self,
         options: impl IntoTimerOptions<'a>,
@@ -386,12 +451,73 @@ impl<Payload: 'static + Send + Sync> WorkerState<Payload> {
 
     /// Create a [`WorkerTimer`] whose callback will be triggered once after the
     /// period of the timer has elapsed. After that you will need to use
-    /// [`WorkerTimer::set_worker_oneshot`] or [`WorkerTimer::set_worker_repeating`]
+    /// [`TimerState::set_worker_oneshot`] or [`TimerState::set_worker_repeating`]
     /// or else nothing will happen the following times that the `Timer` elapses.
     ///
     /// See also:
     /// * [`Self::create_timer_repeating`]
-    /// * [`Self::create_time_inert`]
+    /// * [`Self::create_timer_inert`]
+    ///
+    /// # Behavior
+    ///
+    /// While the callback of this timer is running, no other callbacks associated
+    /// with the [`Worker`] will be able to run. This is necessary to guarantee
+    /// that the mutable borrow of the payload is safe.
+    ///
+    /// Since the callback of this timer may block other callbacks from being
+    /// able to run, it is strongly recommended to ensure that the callback
+    /// returns quickly. If the callback needs to trigger long-running behavior
+    /// then you can condier using `std::thread::spawn`, or for async behaviors
+    /// you can capture an [`ExecutorCommands`][1] in your callback and use
+    /// [`ExecutorCommands::run`][2] to issue a task for the executor to run in
+    /// its async task pool.
+    ///
+    /// # Timer Options
+    ///
+    /// See [`NodeSate::create_timer_repeating`][3] for examples of setting the
+    /// timer options.
+    ///
+    /// # Worker Timer Oneshot Callbacks
+    ///
+    /// Worker Timer oneshot callbacks support four signatures:
+    /// - <code>[FnOnce] ()</code>
+    /// - <code>[FnOnce] ( &mut Payload )</code>
+    /// - <code>[FnOnce] ( &mut Payload, [Time][4] )</code>
+    /// - <code>[FnOnce] ( &mut Payload, &[WorkerTimer]&lt;Payload&gt; )</code>
+    ///
+    /// You can choose to access the payload of the worker. You can additionally
+    /// choose to receive the current time when the callback is being triggered.
+    ///
+    /// Or instead of the current time, you can get a borrow of the [`WorkerTimer`]
+    /// itself, that way if you need to access it from inside the callback, you
+    /// do not need to worry about capturing a [`Weak`] and then locking it. This
+    /// is useful if you need to change the callback of the timer from inside the
+    /// callback of the timer, which may be needed often for oneshot callbacks.
+    ///
+    /// The callback will only be triggered once. After that, this will effectively
+    /// be an [inert][5] timer.
+    ///
+    /// ```
+    /// # use rclrs::*;
+    /// # let executor = Context::default().create_basic_executor();
+    /// # let node = executor.create_node("my_node").unwrap();
+    /// # let worker = node.create_worker::<()>(());
+    /// use std::time::Duration;
+    ///
+    /// let timer = worker.create_timer_oneshot(
+    ///     Duration::from_secs(1),
+    ///     || {
+    ///         println!("This will only fire once");
+    ///     }
+    /// )?;
+    /// # Ok::<(), RclrsError>(())
+    /// ```
+    ///
+    /// [1]: crate::ExecutorCommands
+    /// [2]: crate::ExecutorCommands::run
+    /// [3]: crate::NodeState::create_timer_repeating
+    /// [4]: crate::Time
+    /// [5]: Self::create_timer_inert
     pub fn create_timer_oneshot<'a, Args>(
         &self,
         options: impl IntoTimerOptions<'a>,
@@ -401,8 +527,8 @@ impl<Payload: 'static + Send + Sync> WorkerState<Payload> {
     }
 
     /// Create a [`WorkerTimer`] without a callback. Nothing will happen when this
-    /// `WorkerTimer` elapses until you use [`WorkerTimer::set_worker_repeating`]
-    /// or [`WorkerTimer::set_worker_oneshot`].
+    /// `WorkerTimer` elapses until you use [`TimerState::set_worker_repeating`]
+    /// or [`TimerState::set_worker_oneshot`].
     ///
     /// See also:
     /// * [`Self::create_timer_repeating`]

From 7ad4ece355169accafd07ef39cdf043eb18ebd1b Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Sun, 29 Jun 2025 16:02:44 +0800
Subject: [PATCH 03/12] Fix compatibility with kilted and rolling

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 rclrs/src/timer.rs | 45 +++++++++++++++++++++++++++++++++++----------
 1 file changed, 35 insertions(+), 10 deletions(-)

diff --git a/rclrs/src/timer.rs b/rclrs/src/timer.rs
index a6cd23ef6..b3db0b178 100644
--- a/rclrs/src/timer.rs
+++ b/rclrs/src/timer.rs
@@ -227,16 +227,41 @@ impl<Scope: WorkScope> TimerState<Scope> {
             let allocator = rcutils_get_default_allocator();
 
             let _lifecycle = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
-            // SAFETY: We lock the lifecycle mutex since rcl_timer_init is not
-            // thread-safe.
-            rcl_timer_init(
-                &mut *rcl_timer.lock().unwrap(),
-                &mut *rcl_clock,
-                &mut *rcl_context,
-                period,
-                rcl_timer_callback,
-                allocator,
-            )
+
+            // The API for initializing timers changed with the kilted releaase.
+            #[cfg(any(ros_distro = "humble", ros_distro = "jazzy"))]
+            {
+                // SAFETY: We lock the lifecycle mutex since rcl_timer_init is not
+                // thread-safe.
+                rcl_timer_init(
+                    &mut *rcl_timer.lock().unwrap(),
+                    &mut *rcl_clock,
+                    &mut *rcl_context,
+                    period,
+                    rcl_timer_callback,
+                    allocator,
+                )
+            }
+
+            // The API for initializing timers changed with the kilted releaase.
+            // This new API allows you to opt out of automatically starting the
+            // timer as soon as it is created. We could consider exposing this
+            // capability to the user, but for now we are just telling it to
+            // immediately start the timer.
+            #[cfg(not(any(ros_distro = "humble", ros_distro = "jazzy")))]
+            {
+                // SAFETY: We lock the lifecycle mutex since rcl_timer_init is not
+                // thread-safe.
+                rcl_timer_init(
+                    &mut *rcl_timer.lock().unwrap(),
+                    &mut *rcl_clock,
+                    &mut *rcl_context,
+                    period,
+                    rcl_timer_callback,
+                    allocator,
+                    true,
+                )
+            }
         }
         .ok()?;
 

From 802925bd930f338cc6ff9109e72c19f732567d51 Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Sun, 29 Jun 2025 21:00:12 +0800
Subject: [PATCH 04/12] Fix typo

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 rclrs/src/timer.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/rclrs/src/timer.rs b/rclrs/src/timer.rs
index b3db0b178..abaac3fb9 100644
--- a/rclrs/src/timer.rs
+++ b/rclrs/src/timer.rs
@@ -252,7 +252,7 @@ impl<Scope: WorkScope> TimerState<Scope> {
             {
                 // SAFETY: We lock the lifecycle mutex since rcl_timer_init is not
                 // thread-safe.
-                rcl_timer_init(
+                rcl_timer_init2(
                     &mut *rcl_timer.lock().unwrap(),
                     &mut *rcl_clock,
                     &mut *rcl_context,

From f24b978af3b2429a562bc4612dcec14bb59321ab Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Sun, 29 Jun 2025 21:18:25 +0800
Subject: [PATCH 05/12] Remove Cargo.lock for timers demo

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 examples/timer_demo/Cargo.lock | 973 ---------------------------------
 1 file changed, 973 deletions(-)
 delete mode 100644 examples/timer_demo/Cargo.lock

diff --git a/examples/timer_demo/Cargo.lock b/examples/timer_demo/Cargo.lock
deleted file mode 100644
index c342f0f78..000000000
--- a/examples/timer_demo/Cargo.lock
+++ /dev/null
@@ -1,973 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "action_msgs"
-version = "2.0.2"
-dependencies = [
- "builtin_interfaces",
- "rosidl_runtime_rs",
- "service_msgs",
- "unique_identifier_msgs",
-]
-
-[[package]]
-name = "aho-corasick"
-version = "1.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "async-channel"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
-dependencies = [
- "concurrent-queue",
- "event-listener 2.5.3",
- "futures-core",
-]
-
-[[package]]
-name = "async-channel"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
-dependencies = [
- "concurrent-queue",
- "event-listener-strategy",
- "futures-core",
- "pin-project-lite",
-]
-
-[[package]]
-name = "async-executor"
-version = "1.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa"
-dependencies = [
- "async-task",
- "concurrent-queue",
- "fastrand",
- "futures-lite",
- "pin-project-lite",
- "slab",
-]
-
-[[package]]
-name = "async-global-executor"
-version = "2.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
-dependencies = [
- "async-channel 2.3.1",
- "async-executor",
- "async-io",
- "async-lock",
- "blocking",
- "futures-lite",
- "once_cell",
-]
-
-[[package]]
-name = "async-io"
-version = "2.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3"
-dependencies = [
- "async-lock",
- "cfg-if",
- "concurrent-queue",
- "futures-io",
- "futures-lite",
- "parking",
- "polling",
- "rustix",
- "slab",
- "tracing",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "async-lock"
-version = "3.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
-dependencies = [
- "event-listener 5.4.0",
- "event-listener-strategy",
- "pin-project-lite",
-]
-
-[[package]]
-name = "async-std"
-version = "1.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24"
-dependencies = [
- "async-channel 1.9.0",
- "async-global-executor",
- "async-io",
- "async-lock",
- "crossbeam-utils",
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-lite",
- "gloo-timers",
- "kv-log-macro",
- "log",
- "memchr",
- "once_cell",
- "pin-project-lite",
- "pin-utils",
- "slab",
- "wasm-bindgen-futures",
-]
-
-[[package]]
-name = "async-task"
-version = "4.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
-
-[[package]]
-name = "atomic-waker"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
-
-[[package]]
-name = "bindgen"
-version = "0.70.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
-dependencies = [
- "bitflags",
- "cexpr",
- "clang-sys",
- "itertools",
- "log",
- "prettyplease",
- "proc-macro2",
- "quote",
- "regex",
- "rustc-hash",
- "shlex",
- "syn",
-]
-
-[[package]]
-name = "bitflags"
-version = "2.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
-
-[[package]]
-name = "blocking"
-version = "1.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
-dependencies = [
- "async-channel 2.3.1",
- "async-task",
- "futures-io",
- "futures-lite",
- "piper",
-]
-
-[[package]]
-name = "builtin_interfaces"
-version = "2.0.2"
-dependencies = [
- "rosidl_runtime_rs",
-]
-
-[[package]]
-name = "bumpalo"
-version = "3.19.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
-
-[[package]]
-name = "cexpr"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
-dependencies = [
- "nom",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
-
-[[package]]
-name = "clang-sys"
-version = "1.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
-dependencies = [
- "glob",
- "libc",
- "libloading",
-]
-
-[[package]]
-name = "concurrent-queue"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.8.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
-
-[[package]]
-name = "either"
-version = "1.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
-
-[[package]]
-name = "errno"
-version = "0.3.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
-dependencies = [
- "libc",
- "windows-sys 0.60.2",
-]
-
-[[package]]
-name = "event-listener"
-version = "2.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
-
-[[package]]
-name = "event-listener"
-version = "5.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
-dependencies = [
- "concurrent-queue",
- "parking",
- "pin-project-lite",
-]
-
-[[package]]
-name = "event-listener-strategy"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
-dependencies = [
- "event-listener 5.4.0",
- "pin-project-lite",
-]
-
-[[package]]
-name = "example_interfaces"
-version = "0.12.0"
-dependencies = [
- "action_msgs",
- "builtin_interfaces",
- "rosidl_runtime_rs",
- "service_msgs",
- "unique_identifier_msgs",
-]
-
-[[package]]
-name = "fastrand"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
-
-[[package]]
-name = "futures"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-channel"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
-dependencies = [
- "futures-core",
- "futures-sink",
-]
-
-[[package]]
-name = "futures-core"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
-
-[[package]]
-name = "futures-executor"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-io"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
-
-[[package]]
-name = "futures-lite"
-version = "2.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
-dependencies = [
- "fastrand",
- "futures-core",
- "futures-io",
- "parking",
- "pin-project-lite",
-]
-
-[[package]]
-name = "futures-macro"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "futures-sink"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
-
-[[package]]
-name = "futures-task"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
-
-[[package]]
-name = "futures-util"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-macro",
- "futures-sink",
- "futures-task",
- "memchr",
- "pin-project-lite",
- "pin-utils",
- "slab",
-]
-
-[[package]]
-name = "glob"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
-
-[[package]]
-name = "gloo-timers"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
-dependencies = [
- "futures-channel",
- "futures-core",
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "hermit-abi"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
-
-[[package]]
-name = "itertools"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
-dependencies = [
- "either",
-]
-
-[[package]]
-name = "js-sys"
-version = "0.3.77"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
-dependencies = [
- "once_cell",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "kv-log-macro"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
-dependencies = [
- "log",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.174"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
-
-[[package]]
-name = "libloading"
-version = "0.8.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
-dependencies = [
- "cfg-if",
- "windows-targets 0.53.2",
-]
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
-
-[[package]]
-name = "log"
-version = "0.4.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
-dependencies = [
- "value-bag",
-]
-
-[[package]]
-name = "memchr"
-version = "2.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
-
-[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
-name = "nom"
-version = "7.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
-dependencies = [
- "memchr",
- "minimal-lexical",
-]
-
-[[package]]
-name = "once_cell"
-version = "1.21.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
-
-[[package]]
-name = "parking"
-version = "2.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
-
-[[package]]
-name = "pin-project-lite"
-version = "0.2.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
-
-[[package]]
-name = "pin-utils"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
-
-[[package]]
-name = "piper"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
-dependencies = [
- "atomic-waker",
- "fastrand",
- "futures-io",
-]
-
-[[package]]
-name = "polling"
-version = "3.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
-dependencies = [
- "cfg-if",
- "concurrent-queue",
- "hermit-abi",
- "pin-project-lite",
- "rustix",
- "tracing",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "prettyplease"
-version = "0.2.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
-dependencies = [
- "proc-macro2",
- "syn",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.95"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.40"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "rclrs"
-version = "0.4.1"
-dependencies = [
- "async-std",
- "bindgen",
- "cfg-if",
- "futures",
- "rosidl_runtime_rs",
-]
-
-[[package]]
-name = "regex"
-version = "1.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
-
-[[package]]
-name = "rosidl_runtime_rs"
-version = "0.4.1"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "rustc-hash"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
-
-[[package]]
-name = "rustix"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
-dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "rustversion"
-version = "1.0.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
-
-[[package]]
-name = "service_msgs"
-version = "2.0.2"
-dependencies = [
- "builtin_interfaces",
- "rosidl_runtime_rs",
-]
-
-[[package]]
-name = "shlex"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
-[[package]]
-name = "slab"
-version = "0.4.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
-
-[[package]]
-name = "syn"
-version = "2.0.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "timer_demo"
-version = "0.1.0"
-dependencies = [
- "example_interfaces",
- "rclrs",
-]
-
-[[package]]
-name = "tracing"
-version = "0.1.41"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
-dependencies = [
- "pin-project-lite",
- "tracing-core",
-]
-
-[[package]]
-name = "tracing-core"
-version = "0.1.34"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
-
-[[package]]
-name = "unique_identifier_msgs"
-version = "2.5.0"
-dependencies = [
- "rosidl_runtime_rs",
-]
-
-[[package]]
-name = "value-bag"
-version = "1.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
-
-[[package]]
-name = "wasm-bindgen"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
-dependencies = [
- "cfg-if",
- "once_cell",
- "rustversion",
- "wasm-bindgen-macro",
-]
-
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
-dependencies = [
- "bumpalo",
- "log",
- "proc-macro2",
- "quote",
- "syn",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-futures"
-version = "0.4.50"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
-dependencies = [
- "cfg-if",
- "js-sys",
- "once_cell",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
-dependencies = [
- "quote",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "wasm-bindgen-backend",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "web-sys"
-version = "0.3.77"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
-dependencies = [
- "windows-targets 0.53.2",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
-dependencies = [
- "windows_aarch64_gnullvm 0.52.6",
- "windows_aarch64_msvc 0.52.6",
- "windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm 0.52.6",
- "windows_i686_msvc 0.52.6",
- "windows_x86_64_gnu 0.52.6",
- "windows_x86_64_gnullvm 0.52.6",
- "windows_x86_64_msvc 0.52.6",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.53.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
-dependencies = [
- "windows_aarch64_gnullvm 0.53.0",
- "windows_aarch64_msvc 0.53.0",
- "windows_i686_gnu 0.53.0",
- "windows_i686_gnullvm 0.53.0",
- "windows_i686_msvc 0.53.0",
- "windows_x86_64_gnu 0.53.0",
- "windows_x86_64_gnullvm 0.53.0",
- "windows_x86_64_msvc 0.53.0",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
-
-[[patch.unused]]
-name = "rcl_interfaces"
-version = "2.0.2"
-
-[[patch.unused]]
-name = "rclrs_example_msgs"
-version = "0.4.1"
-
-[[patch.unused]]
-name = "rosgraph_msgs"
-version = "2.0.2"
-
-[[patch.unused]]
-name = "std_msgs"
-version = "5.3.6"
-
-[[patch.unused]]
-name = "test_msgs"
-version = "2.0.2"

From 9b6b1d1197fb33b25f3b320c4a19a5aeaf8cb751 Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Sun, 29 Jun 2025 23:09:29 +0800
Subject: [PATCH 06/12] Fix example package names

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 examples/logging_demo/Cargo.toml | 2 +-
 examples/worker_demo/Cargo.toml  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/examples/logging_demo/Cargo.toml b/examples/logging_demo/Cargo.toml
index 778981c51..a959249ac 100644
--- a/examples/logging_demo/Cargo.toml
+++ b/examples/logging_demo/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "logging_demo"
+name = "examples_logging_demo"
 version = "0.1.0"
 edition = "2021"
 
diff --git a/examples/worker_demo/Cargo.toml b/examples/worker_demo/Cargo.toml
index 7fd78c74b..2cde2426d 100644
--- a/examples/worker_demo/Cargo.toml
+++ b/examples/worker_demo/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "worker_demo"
+name = "examples_worker_demo"
 version = "0.1.0"
 edition = "2021"
 

From 73b34b37a78b05e7feab91fe96563d4902b21d1f Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Sun, 29 Jun 2025 23:15:42 +0800
Subject: [PATCH 07/12] Fix name of timer demo

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 examples/timer_demo/Cargo.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/timer_demo/Cargo.toml b/examples/timer_demo/Cargo.toml
index 23b9f1fa1..b4356436a 100644
--- a/examples/timer_demo/Cargo.toml
+++ b/examples/timer_demo/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "timer_demo"
+name = "examples_timer_demo"
 version = "0.1.0"
 edition = "2021"
 

From 030f59c8c5c66b578b1bf8ad90b7a1ede23b4297 Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Sun, 29 Jun 2025 23:27:02 +0800
Subject: [PATCH 08/12] Fix name of parameter demo

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 examples/parameter_demo/Cargo.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/parameter_demo/Cargo.toml b/examples/parameter_demo/Cargo.toml
index 4f90061a9..6a20a9a33 100644
--- a/examples/parameter_demo/Cargo.toml
+++ b/examples/parameter_demo/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "parameter_demo"
+name = "examples_parameter_demo"
 version = "0.1.0"
 edition = "2021"
 

From a589997b24b07b7a934b7431604f25d8e82a815a Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Thu, 17 Jul 2025 20:17:18 +0800
Subject: [PATCH 09/12] Add safety comments

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 rclrs/src/timer.rs | 85 +++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 72 insertions(+), 13 deletions(-)

diff --git a/rclrs/src/timer.rs b/rclrs/src/timer.rs
index abaac3fb9..d84ebc879 100644
--- a/rclrs/src/timer.rs
+++ b/rclrs/src/timer.rs
@@ -86,7 +86,13 @@ impl<Scope: WorkScope> TimerState<Scope> {
     pub fn get_timer_period(&self) -> Result<Duration, RclrsError> {
         let mut timer_period_ns = 0;
         unsafe {
+            // SAFETY: The unwrap is safe here since we never use the rcl_timer
+            // in a way that could panic while the mutex is locked.
             let rcl_timer = self.handle.rcl_timer.lock().unwrap();
+
+            // SAFETY: The rcl_timer is kept valid by the TimerState. This C
+            // function call is thread-safe and only requires a valid rcl_timer
+            // to be passed in.
             rcl_timer_get_period(&*rcl_timer, &mut timer_period_ns)
         }
         .ok()?;
@@ -94,10 +100,22 @@ impl<Scope: WorkScope> TimerState<Scope> {
         rcl_duration(timer_period_ns)
     }
 
-    /// Cancels the timer, stopping the execution of the callback
+    /// Cancels the timer, stopping the execution of the callback.
+    ///
+    /// [`TimerState::is_ready`] will always return false while the timer is in
+    /// a cancelled state. [`TimerState::reset`] can be used to revert the timer
+    /// out of the cancelled state.
     pub fn cancel(&self) -> Result<(), RclrsError> {
-        let mut rcl_timer = self.handle.rcl_timer.lock().unwrap();
-        let cancel_result = unsafe { rcl_timer_cancel(&mut *rcl_timer) }.ok()?;
+        let cancel_result = unsafe {
+            // SAFETY: The unwrap is safe here since we never use the rcl_timer
+            // in a way that could panic while the mutex is locked.
+            let mut rcl_timer = self.handle.rcl_timer.lock().unwrap();
+
+            // SAFETY: The rcl_timer is kept valid by the TimerState. This C
+            // function call is thread-safe and only requires a valid rcl_timer
+            // to be passed in.
+            rcl_timer_cancel(&mut *rcl_timer)
+        }.ok()?;
         Ok(cancel_result)
     }
 
@@ -105,7 +123,13 @@ impl<Scope: WorkScope> TimerState<Scope> {
     pub fn is_canceled(&self) -> Result<bool, RclrsError> {
         let mut is_canceled = false;
         unsafe {
+            // SAFETY: The unwrap is safe here since we never use the rcl_timer
+            // in a way that could panic while the mutex is locked.
             let rcl_timer = self.handle.rcl_timer.lock().unwrap();
+
+            // SAFETY: The rcl_timer is kept valid by the TimerState. This C
+            // function call is thread-safe and only requires a valid rcl_timer
+            // to be passed in.
             rcl_timer_is_canceled(&*rcl_timer, &mut is_canceled)
         }
         .ok()?;
@@ -128,7 +152,13 @@ impl<Scope: WorkScope> TimerState<Scope> {
     pub fn time_since_last_call(&self) -> Result<Duration, RclrsError> {
         let mut time_value_ns: i64 = 0;
         unsafe {
+            // SAFETY: The unwrap is safe here since we never use the rcl_timer
+            // in a way that could panic while the mutex is locked.
             let rcl_timer = self.handle.rcl_timer.lock().unwrap();
+
+            // SAFETY: The rcl_timer is kept valid by the TimerState. This C
+            // function call is thread-safe and only requires a valid rcl_timer
+            // to be passed in.
             rcl_timer_get_time_since_last_call(&*rcl_timer, &mut time_value_ns)
         }
         .ok()?;
@@ -140,7 +170,13 @@ impl<Scope: WorkScope> TimerState<Scope> {
     pub fn time_until_next_call(&self) -> Result<Duration, RclrsError> {
         let mut time_value_ns: i64 = 0;
         unsafe {
+            // SAFETY: The unwrap is safe here since we never use the rcl_timer
+            // in a way that could panic while the mutex is locked.
             let rcl_timer = self.handle.rcl_timer.lock().unwrap();
+
+            // SAFETY: The rcl_timer is kept valid by the TimerState. This C
+            // function call is thread-safe and only requires a valid rcl_timer
+            // to be passed in.
             rcl_timer_get_time_until_next_call(&*rcl_timer, &mut time_value_ns)
         }
         .ok()?;
@@ -149,9 +185,20 @@ impl<Scope: WorkScope> TimerState<Scope> {
     }
 
     /// Resets the timer.
+    ///
+    /// For all timers it will reset the last call time to now. For cancelled
+    /// timers it will revert the timer to no longer being cancelled.
     pub fn reset(&self) -> Result<(), RclrsError> {
+        // SAFETY: The unwrap is safe here since we never use the rcl_timer
+        // in a way that could panic while the mutex is locked.
         let mut rcl_timer = self.handle.rcl_timer.lock().unwrap();
-        unsafe { rcl_timer_reset(&mut *rcl_timer) }.ok()
+
+        unsafe {
+            // SAFETY: The rcl_timer is kept valid by the TimerState. This C
+            // function call is thread-safe and only requires a valid rcl_timer
+            // to be passed in.
+            rcl_timer_reset(&mut *rcl_timer)
+        }.ok()
     }
 
     /// Checks if the timer is ready (not canceled)
@@ -215,8 +262,10 @@ impl<Scope: WorkScope> TimerState<Scope> {
         let rcl_timer_callback: rcl_timer_callback_t = None;
 
         let rcl_timer = Arc::new(Mutex::new(
-            // SAFETY: Zero-initializing a timer is always safe
-            unsafe { rcl_get_zero_initialized_timer() },
+            unsafe {
+                // SAFETY: Zero-initializing a timer is always safe
+                rcl_get_zero_initialized_timer()
+            },
         ));
 
         unsafe {
@@ -340,8 +389,16 @@ impl<Scope: WorkScope> TimerState<Scope> {
     /// in the [`Timer`] struct. This means there are no side-effects to this
     /// except to keep track of when the timer has been called.
     fn rcl_call(&self) -> Result<(), RclrsError> {
+        // SAFETY: The unwrap is safe here since we never use the rcl_timer
+        // in a way that could panic while the mutex is locked.
         let mut rcl_timer = self.handle.rcl_timer.lock().unwrap();
-        unsafe { rcl_timer_call(&mut *rcl_timer) }.ok()
+
+        unsafe {
+            // SAFETY: The rcl_timer is kept valid by the TimerState. This C
+            // function call is thread-safe and only requires a valid rcl_timer
+            // to be passed in.
+            rcl_timer_call(&mut *rcl_timer)
+        }.ok()
     }
 
     /// Used by [`Timer::execute`] to restore the state of the callback if and
@@ -469,15 +526,17 @@ pub(crate) struct TimerHandle {
 impl Drop for TimerHandle {
     fn drop(&mut self) {
         let _lifecycle = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
-        // SAFETY: The lifecycle mutex is locked and the clock for the timer
-        // must still be valid because TimerHandle keeps it alive.
-        unsafe { rcl_timer_fini(&mut *self.rcl_timer.lock().unwrap()) };
+        unsafe {
+            // SAFETY: The lifecycle mutex is locked and the clock for the timer
+            // must still be valid because TimerHandle keeps it alive.
+            rcl_timer_fini(&mut *self.rcl_timer.lock().unwrap())
+        };
     }
 }
 
 // SAFETY: The functions accessing this type, including drop(), shouldn't care about the thread
 // they are running in. Therefore, this type can be safely sent to another thread.
-unsafe impl Send for rcl_timer_t {}
+impl Send for rcl_timer_t {}
 
 #[cfg(test)]
 mod tests {
@@ -759,8 +818,8 @@ mod tests {
             handle: Arc::clone(&timer.handle),
         };
 
-        // SAFETY: Node timers expect a payload of ()
         unsafe {
+            // SAFETY: Node timers expect a payload of ()
             executable.execute(&mut ()).unwrap();
         }
         assert!(!executed.load(Ordering::Acquire));
@@ -790,8 +849,8 @@ mod tests {
 
         thread::sleep(Duration::from_millis(2));
 
-        // SAFETY: Node timers expect a payload of ()
         unsafe {
+            // SAFETY: Node timers expect a payload of ()
             executable.execute(&mut ()).unwrap();
         }
         assert!(executed.load(Ordering::Acquire));

From a4fa5cb2d45aed105b2e80337a899081383b54aa Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Thu, 17 Jul 2025 20:26:48 +0800
Subject: [PATCH 10/12] Add an explicit binary name for timer demo

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 examples/timer_demo/Cargo.toml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/examples/timer_demo/Cargo.toml b/examples/timer_demo/Cargo.toml
index b4356436a..d3bfd1583 100644
--- a/examples/timer_demo/Cargo.toml
+++ b/examples/timer_demo/Cargo.toml
@@ -3,6 +3,10 @@ name = "examples_timer_demo"
 version = "0.1.0"
 edition = "2021"
 
+[[bin]]
+name = "timer_demo"
+path = "src/main.rs"
+
 [dependencies]
 rclrs = "0.4"
 example_interfaces = "*"

From 35fa24ac0a53e6a9c70b671a17401c46ddd67900 Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Thu, 17 Jul 2025 20:54:47 +0800
Subject: [PATCH 11/12] Restore unsafe tag to Send trait

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
---
 rclrs/Cargo.lock   | 14 +-------------
 rclrs/src/timer.rs |  2 +-
 2 files changed, 2 insertions(+), 14 deletions(-)

diff --git a/rclrs/Cargo.lock b/rclrs/Cargo.lock
index 834786e1f..c874b022b 100644
--- a/rclrs/Cargo.lock
+++ b/rclrs/Cargo.lock
@@ -726,7 +726,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
 [[package]]
 name = "rosidl_runtime_rs"
-version = "0.4.1"
+version = "0.4.2"
 dependencies = [
  "cfg-if",
  "serde",
@@ -1175,18 +1175,6 @@ dependencies = [
 name = "rcl_interfaces"
 version = "2.0.2"
 
-[[patch.unused]]
-name = "rclrs"
-version = "0.4.1"
-
-[[patch.unused]]
-name = "rclrs_example_msgs"
-version = "0.4.1"
-
 [[patch.unused]]
 name = "rosgraph_msgs"
 version = "2.0.2"
-
-[[patch.unused]]
-name = "std_msgs"
-version = "5.3.6"
diff --git a/rclrs/src/timer.rs b/rclrs/src/timer.rs
index d84ebc879..c1605615c 100644
--- a/rclrs/src/timer.rs
+++ b/rclrs/src/timer.rs
@@ -536,7 +536,7 @@ impl Drop for TimerHandle {
 
 // SAFETY: The functions accessing this type, including drop(), shouldn't care about the thread
 // they are running in. Therefore, this type can be safely sent to another thread.
-impl Send for rcl_timer_t {}
+unsafe impl Send for rcl_timer_t {}
 
 #[cfg(test)]
 mod tests {

From 775b170811518aa0c4e02f52c45b24963d9d64f7 Mon Sep 17 00:00:00 2001
From: "Michael X. Grey" <greyxmike@gmail.com>
Date: Fri, 18 Jul 2025 16:31:03 +0800
Subject: [PATCH 12/12] Include authors of
 https://github.com/ros2-rust/ros2_rust/pull/440
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Agustin Alba Chicar <ag.albachicar@gmail.com>
Co-authored-by: Jesús Silva <79662866+JesusSilvaUtrera@users.noreply.github.com>
Signed-off-by: Agustin Alba Chicar <ag.albachicar@gmail.com>
Signed-off-by: Jesús Silva <79662866+JesusSilvaUtrera@users.noreply.github.com>
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>