-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwalkthrough.html
More file actions
2092 lines (1804 loc) · 144 KB
/
walkthrough.html
File metadata and controls
2092 lines (1804 loc) · 144 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ROS 2 for Humanoids — Walkthrough</title>
<style>
:root {
--bg: #0f1115;
--panel: #161922;
--panel-2: #1c2030;
--ink: #e5e7eb;
--ink-dim: #9ca3af;
--accent: #7dd3fc;
--accent-2: #fbbf24;
--warn: #f87171;
--ok: #86efac;
--line: #2a2f3e;
--code-bg: #0b0d12;
--code-ink: #d1d5db;
--code-kw: #c4b5fd;
--code-str: #fcd34d;
--code-com: #6b7280;
--code-num: #fda4af;
--code-fn: #93c5fd;
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
background: var(--bg); color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
font-size: 16px; line-height: 1.65;
}
body { display: flex; min-height: 100vh; }
nav.sidebar {
position: sticky; top: 0;
width: 260px; max-height: 100vh; overflow-y: auto;
background: var(--panel); border-right: 1px solid var(--line);
padding: 1.5rem 1.25rem;
flex-shrink: 0;
}
nav.sidebar h1 {
font-size: 1.1rem; margin: 0 0 .5rem 0;
color: var(--accent); letter-spacing: -.01em;
}
nav.sidebar p.tag {
margin: 0 0 1.25rem 0; color: var(--ink-dim); font-size: .82rem;
}
nav.sidebar ol {
list-style: none; padding: 0; margin: 0;
border-top: 1px solid var(--line);
}
nav.sidebar li {
border-bottom: 1px solid var(--line);
}
nav.sidebar a {
display: block; padding: .55rem .25rem;
color: var(--ink); text-decoration: none;
font-size: .92rem;
}
nav.sidebar a:hover { color: var(--accent); }
/* Body / content links — explicit so they don't fall back to the
browser default deep blue, which is illegible on the dark bg. */
main a,
main a:visited,
.callout a,
.callout a:visited,
figcaption a,
figcaption a:visited,
footer a,
footer a:visited {
color: var(--accent);
text-decoration: underline;
text-decoration-color: rgba(125, 211, 252, 0.45);
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
main a:hover,
.callout a:hover,
figcaption a:hover,
footer a:hover {
color: var(--accent-2);
text-decoration-color: var(--accent-2);
}
nav.sidebar a .num {
display: inline-block; width: 1.8rem;
color: var(--accent-2); font-weight: 600;
font-variant-numeric: tabular-nums;
}
nav.sidebar a .sub {
display: block; color: var(--ink-dim); font-size: .78rem;
padding-left: 1.8rem; margin-top: .15rem;
}
main {
flex: 1; max-width: 920px;
padding: 3rem clamp(1.5rem, 4vw, 3rem);
}
h1.title {
font-size: 2.2rem; margin: 0 0 .25rem 0;
letter-spacing: -.01em;
}
p.subtitle {
color: var(--ink-dim); margin: 0 0 3rem 0;
font-size: 1.05rem;
}
section.lesson { margin: 0 0 4.5rem 0; }
section.lesson > header {
border-left: 3px solid var(--accent); padding-left: 1rem;
margin: 0 0 1.5rem 0;
}
section.lesson > header .crumb {
color: var(--accent); font-size: .82rem; letter-spacing: .04em;
text-transform: uppercase; font-weight: 600;
}
section.lesson > header h2 {
margin: .35rem 0 .25rem 0; font-size: 1.65rem;
letter-spacing: -.01em;
}
section.lesson > header p.summary {
margin: 0; color: var(--ink-dim); font-size: 1.02rem;
}
section.lesson h3 {
margin: 2rem 0 .5rem 0; font-size: 1.15rem;
color: var(--accent-2);
}
section.lesson p { margin: .75rem 0; }
section.lesson ul, section.lesson ol { padding-left: 1.4rem; }
section.lesson li { margin: .25rem 0; }
section.lesson strong { color: var(--accent); }
section.lesson em { color: var(--ink-dim); font-style: italic; }
section.lesson figure.shot {
margin: 1.25rem 0;
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: 6px;
padding: .75rem;
}
section.lesson figure.shot img {
display: block;
width: 100%;
height: auto;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,.4);
}
section.lesson figure.shot figcaption {
margin-top: .55rem;
font-size: .82rem;
color: var(--ink-dim);
text-align: center;
font-style: italic;
}
section.lesson p.hint {
margin: -.25rem 0 .75rem 0;
padding: .35rem .6rem;
background: rgba(125,211,252,0.08);
border-left: 2px solid var(--accent);
font-size: .9rem;
color: var(--ink-dim);
}
section.lesson p.hint em { color: var(--ink); }
section.lesson kbd {
background: var(--code-bg); color: var(--accent-2);
padding: .05rem .4rem; border-radius: 3px;
border: 1px solid var(--line);
font-family: "JetBrains Mono", "Fira Mono", Menlo, Consolas, monospace;
font-size: .85em;
}
section.lesson .toolgrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin: 1.25rem 0;
}
section.lesson .toolcard {
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: 6px;
padding: .9rem 1rem;
}
section.lesson .toolcard h4 {
margin: 0 0 .35rem 0;
font-size: .98rem;
color: var(--accent);
}
section.lesson .toolcard p {
margin: 0 0 .5rem 0;
font-size: .9rem;
color: var(--ink-dim);
}
section.lesson .toolcard pre {
margin: 0;
font-size: .82rem;
padding: .6rem .7rem;
}
section.lesson code {
background: var(--code-bg); color: var(--code-ink);
padding: .1rem .35rem; border-radius: 3px;
font-family: "JetBrains Mono", "Fira Mono", Menlo, Consolas, monospace;
font-size: .9em;
}
pre {
background: var(--code-bg); color: var(--code-ink);
padding: 1rem 1.1rem; border-radius: 6px;
border: 1px solid var(--line);
font-family: "JetBrains Mono", "Fira Mono", Menlo, Consolas, monospace;
font-size: .88rem; line-height: 1.55;
overflow-x: auto;
}
pre .kw { color: var(--code-kw); }
pre .str { color: var(--code-str); }
pre .com { color: var(--code-com); font-style: italic; }
pre .num { color: var(--code-num); }
pre .fn { color: var(--code-fn); }
pre .pmt { color: var(--accent-2); }
.callout {
background: var(--panel-2);
border-left: 3px solid var(--accent-2);
padding: .9rem 1.1rem;
margin: 1.25rem 0;
border-radius: 4px;
}
.callout.warn { border-left-color: var(--warn); }
.callout.ok { border-left-color: var(--ok); }
.callout > strong:first-child {
display: block; margin-bottom: .25rem;
color: var(--accent-2); font-size: .85rem;
text-transform: uppercase; letter-spacing: .05em;
}
.callout.warn > strong:first-child { color: var(--warn); }
.callout.ok > strong:first-child { color: var(--ok); }
table {
width: 100%; border-collapse: collapse; margin: 1rem 0;
font-size: .94rem;
}
th, td {
border-bottom: 1px solid var(--line);
padding: .55rem .65rem; text-align: left;
}
th { color: var(--accent-2); font-weight: 600; }
td code { font-size: .85em; }
hr {
border: 0; border-top: 1px solid var(--line); margin: 2.5rem 0;
}
footer {
margin-top: 5rem; padding-top: 2rem;
border-top: 1px solid var(--line);
color: var(--ink-dim); font-size: .85rem;
}
@media (max-width: 900px) {
body { flex-direction: column; }
nav.sidebar { width: 100%; max-height: none; position: relative; }
main { padding: 2rem 1.25rem; max-width: 100%; }
}
/* ─────────── copy-to-clipboard buttons on every <pre> ─────────── */
.codewrap {
position: relative;
margin: 1rem 0;
}
.codewrap pre { margin: 0; }
.copy-btn {
position: absolute;
top: 6px; right: 6px;
padding: .25rem .55rem;
font-size: .7rem;
font-family: inherit;
font-weight: 500;
color: var(--ink-dim);
background: rgba(20, 24, 36, .92);
border: 1px solid var(--line);
border-radius: 4px;
cursor: pointer;
opacity: 0;
transition: opacity .15s, color .15s, background .15s;
user-select: none;
z-index: 2;
letter-spacing: .03em;
}
.codewrap:hover .copy-btn,
.copy-btn:focus-visible { opacity: 1; }
.copy-btn:hover {
color: var(--accent);
background: rgba(20, 24, 36, 1);
border-color: var(--accent);
}
.copy-btn.copied {
color: var(--ok);
border-color: var(--ok);
opacity: 1;
}
/* Per-line copy buttons in command-style blocks */
.cmd-line {
position: relative;
display: block;
}
.cmd-line .line-copy {
position: absolute;
right: -.15rem;
top: 50%;
transform: translateY(-50%);
padding: 0 .35rem;
height: 1.4em;
line-height: 1.4em;
font-size: .65rem;
font-family: inherit;
color: var(--ink-dim);
background: rgba(20,24,36,.92);
border: 1px solid var(--line);
border-radius: 3px;
cursor: pointer;
opacity: 0;
transition: opacity .12s, color .12s, border-color .12s;
user-select: none;
}
/* The block-level "copy commands" button lives in the top-right corner
of the wrap. If the first cmd-line's per-line button sits at the
same right edge it would overlay the block button — push the first
line's button further left so the two are always distinct. */
.cmd-line:first-child .line-copy {
right: 7.5rem;
}
.cmd-line:hover .line-copy { opacity: .95; }
.cmd-line .line-copy:hover {
color: var(--accent);
border-color: var(--accent);
}
.cmd-line .line-copy.copied {
color: var(--ok);
border-color: var(--ok);
opacity: 1;
}
/* ─────────── sidebar search box + results ─────────── */
.searchbox {
position: relative;
margin: 0 0 1rem 0;
}
.searchbox input {
width: 100%;
padding: .55rem .7rem .55rem 2rem;
background: var(--code-bg);
color: var(--ink);
border: 1px solid var(--line);
border-radius: 5px;
font-size: .88rem;
font-family: inherit;
outline: none;
transition: border-color .15s, background .15s;
}
.searchbox input::placeholder { color: var(--ink-dim); }
.searchbox input:focus {
border-color: var(--accent);
background: #0d1018;
}
.searchbox::before {
content: "⌕";
position: absolute;
left: .65rem;
top: 50%;
transform: translateY(-52%);
color: var(--ink-dim);
font-size: 1rem;
pointer-events: none;
}
.searchbox kbd.kshort {
position: absolute;
right: .5rem;
top: 50%;
transform: translateY(-50%);
font-size: .7rem;
color: var(--ink-dim);
background: transparent;
border: 1px solid var(--line);
padding: .05rem .3rem;
border-radius: 3px;
pointer-events: none;
}
.searchbox input:focus ~ kbd.kshort { display: none; }
#search-results {
display: none;
margin: -.5rem 0 1rem 0;
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: 5px;
max-height: 60vh;
overflow-y: auto;
}
#search-results.open { display: block; }
#search-results .res {
display: block;
padding: .55rem .75rem;
border-bottom: 1px solid var(--line);
text-decoration: none;
color: var(--ink);
cursor: pointer;
}
#search-results .res:last-child { border-bottom: none; }
#search-results .res:hover,
#search-results .res.active {
background: rgba(125,211,252,0.08);
}
#search-results .res .ttl {
color: var(--accent);
font-size: .85rem;
font-weight: 600;
margin-bottom: .15rem;
}
#search-results .res .ttl .badge {
display: inline-block;
margin-right: .35rem;
font-size: .65rem;
padding: .05rem .35rem;
border-radius: 3px;
background: rgba(251, 191, 36, .14);
color: var(--accent-2);
vertical-align: middle;
letter-spacing: .04em;
text-transform: uppercase;
}
#search-results .res .ttl .badge.ref {
background: rgba(125, 211, 252, .12);
color: var(--accent);
}
#search-results .res .snip {
color: var(--ink-dim);
font-size: .78rem;
line-height: 1.45;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#search-results .res .snip mark {
background: rgba(251,191,36,0.25);
color: var(--ink);
border-radius: 2px;
padding: 0 .12rem;
}
#search-results .empty {
padding: .75rem;
font-size: .85rem;
color: var(--ink-dim);
text-align: center;
}
</style>
</head>
<body>
<nav class="sidebar">
<h1>ROS 2 for Humanoids</h1>
<p class="tag">Debian/WSL2 · RoboStack · Unitree G1</p>
<div class="searchbox">
<input id="search-input" type="search" placeholder="Search the tutorial…" autocomplete="off" spellcheck="false">
<kbd class="kshort">/</kbd>
</div>
<div id="search-results" role="listbox" aria-label="Search results"></div>
<ol>
<li><a href="#eli5"><span class="num">★</span>ROS 2 in 10 minutes<span class="sub">read this first</span></a></li>
<li><a href="#install"><span class="num">00</span>Install & verify<span class="sub">Get the env running</span></a></li>
<li><a href="#first"><span class="num">▸</span>First robot in rviz2<span class="sub">topics, services, params live</span></a></li>
<li><a href="#L01"><span class="num">01</span>A minimal node<span class="sub">init, spin, shutdown</span></a></li>
<li><a href="#L02"><span class="num">02</span>Pub/sub + QoS<span class="sub">why best_effort matters</span></a></li>
<li><a href="#L03"><span class="num">03</span>Services<span class="sub">one-shot state changes</span></a></li>
<li><a href="#L04"><span class="num">04</span>Actions<span class="sub">long-running goals</span></a></li>
<li><a href="#L05"><span class="num">05</span>Lifecycle nodes<span class="sub">deliberate activation</span></a></li>
<li><a href="#L06"><span class="num">06</span>TF2<span class="sub">where is the head?</span></a></li>
<li><a href="#toolkit"><span class="num">▸</span>Roboticist's toolkit<span class="sub">rqt, bag, every CLI</span></a></li>
<li><a href="#sim"><span class="num">▸</span>Real Unitree G1 URDF<span class="sub">upstream meshes in rviz2</span></a></li>
<li><a href="#L07"><span class="num">07</span>G1 bridge<span class="sub">DDS ↔ ROS 2</span></a></li>
<li><a href="#L08"><span class="num">08</span>Voice I/O<span class="sub">listen & speak</span></a></li>
<li><a href="#L09"><span class="num">09</span>High-level control<span class="sub">sit, stand, wave</span></a></li>
<li><a href="#physics"><span class="num">▸</span>G1 in physics<span class="sub">MuJoCo, falling, balance</span></a></li>
<li><a href="#further"><span class="num">→</span>What to build next</a></li>
</ol>
</nav>
<main>
<h1 class="title">ROS 2 for Humanoids</h1>
<p class="subtitle">From a one-node "hello" to a Unitree G1 that listens, talks, and moves. Nine lessons, one workspace, no Docker.</p>
<p class="subtitle" style="font-size:.85rem;margin-top:-2rem;">
Tip: hover any code block to copy the whole thing, or hover a single line for a per-line copy — the <code>$</code> prompt is stripped automatically. Press <kbd>/</kbd> to search the entire tutorial + reference docs.
</p>
<!-- ============================================================ -->
<section class="lesson" id="eli5">
<header>
<div class="crumb">★ start here</div>
<h2>ROS 2 in 10 minutes — every core concept, explained simply</h2>
<p class="summary">A robot's nervous system, in plain language. Read this first; the rest of the tutorial assumes you've internalized these ideas.</p>
</header>
<h3>The one-sentence definition</h3>
<p><strong>ROS 2 is the operating system for a robot's nervous system.</strong> It's not Linux — your robot already runs Linux. It's the layer above Linux that lets a dozen small programs (each watching one sensor, each driving one motor, each making one decision) talk to each other without knowing where each other lives, without crashing when one of them dies, and without needing a single "main" program.</p>
<h3>Why "2"? The forty-second history</h3>
<p>ROS 1 (2007–2025) had a master process — a phone book everyone called into. Lose the master, lose the robot. ROS 2 (2017–) replaced that with <strong>DDS</strong>, a peer-to-peer messaging standard borrowed from aerospace. Every node finds every other node by listening on the network. No central process. No single point of failure. That's the whole point of the rewrite.</p>
<h3>The five concepts you actually need</h3>
<h3>① Node — a single small program</h3>
<p>A <strong>node</strong> is one process that does one thing. The camera driver is a node. The motor controller is a node. The face detector is a node. A real robot runs <em>dozens</em> of nodes at once, each isolated, each replaceable.</p>
<p class="hint">Mental model: <em>each node is a Linux process — htop sees it.</em> Crash one node, the rest keep running.</p>
<h3>② Topic — a public bulletin board</h3>
<p>A <strong>topic</strong> is a named channel that any node can publish to and any node can subscribe to. Nobody addresses anyone directly. The camera publishes images to <code>/camera/image</code>; whoever cares — the face detector, the recorder, the dashboard — subscribes to that name. The camera doesn't know they exist.</p>
<p>You'll inspect topics constantly:</p>
<pre><span class="pmt">$</span> ros2 topic list <span class="com"># every topic right now</span>
<span class="pmt">$</span> ros2 topic echo /joint_states <span class="com"># watch messages flow</span>
<span class="pmt">$</span> ros2 topic hz /joint_states <span class="com"># at what rate?</span>
<span class="pmt">$</span> ros2 topic info /joint_states -v <span class="com"># who's publishing, what QoS?</span></pre>
<p class="hint">Mental model: <em>a Slack channel</em>. People post; whoever's subscribed sees it. Posters and readers don't know each other.</p>
<h3>③ Message — a typed letter</h3>
<p>Topics carry <strong>messages</strong>, and every topic has exactly one message type. <code>/camera/image</code> carries <code>sensor_msgs/Image</code>. <code>/cmd_vel</code> carries <code>geometry_msgs/Twist</code>. The type is a contract: subscribers know what fields to expect.</p>
<pre><span class="pmt">$</span> ros2 topic type /joint_states <span class="com"># sensor_msgs/msg/JointState</span>
<span class="pmt">$</span> ros2 interface show sensor_msgs/msg/JointState
Header header
string[] name
float64[] position
float64[] velocity
float64[] effort</pre>
<p class="hint">Mental model: <em>a typed envelope</em>. Same channel name + same message type on both ends, or no mail is delivered.</p>
<h3>④ Service — a phone call</h3>
<p>A <strong>service</strong> is request/reply. Caller sends a request, blocks (or waits asynchronously), gets a single response. Use this for atomic state changes: "switch to damping mode," "reset the IMU bias," "start recording."</p>
<pre><span class="pmt">$</span> ros2 service list
<span class="pmt">$</span> ros2 service call /g1/safety/engage std_srvs/srv/Trigger '{}'</pre>
<p class="hint">Mental model: <em>a phone call</em>. Calling makes you wait until someone answers. Topics are voicemail blasts to a list.</p>
<h3>⑤ Action — a long phone call with status updates</h3>
<p>An <strong>action</strong> is like a service that takes time and reports progress. Caller sends a goal, server accepts (or rejects), feedback streams during execution, and a single result lands at the end. The caller can <strong>cancel</strong> at any point.</p>
<p>Motion commands are almost always actions: "walk to (1.5, 0)" takes seconds, the caller wants progress, and might want to stop the robot if a human walks in.</p>
<pre><span class="pmt">$</span> ros2 action list
<span class="pmt">$</span> ros2 action send_goal /fibonacci example_interfaces/action/Fibonacci '{order: 6}' --feedback</pre>
<h3>Three smaller concepts you'll meet immediately</h3>
<h3>⚙ Parameters — runtime config knobs</h3>
<p>Each node has a flat dict of named, typed parameters. Tune things at runtime, not in code:</p>
<pre><span class="pmt">$</span> ros2 param list /joint_animator
<span class="pmt">$</span> ros2 param get /joint_animator pattern
<span class="pmt">$</span> ros2 param set /joint_animator pattern walk</pre>
<h3>🧭 TF — a coordinate-frame graph</h3>
<p><strong>TF</strong> answers "where is X relative to Y?" for the whole robot. Each link (base, chest, head, left hand, camera, …) has a coordinate frame. TF stores every parent→child transform; you query the graph for any pair. Without TF, every node reinvents kinematics.</p>
<pre><span class="pmt">$</span> ros2 run tf2_ros tf2_echo base_link head_link</pre>
<h3>📡 QoS — quality-of-service for the data lane</h3>
<p>Each pub/sub pair has a <strong>QoS profile</strong>: reliability (reliable / best-effort), durability (volatile / transient_local), depth (queue size), history. The profile must match on both ends or DDS routes nothing. Two profiles cover 95% of cases:</p>
<ul>
<li><strong><code>sensor_data</code></strong> — best-effort, volatile, depth 5. For state streams (IMU, joints, lidar). Drop is OK; latest matters.</li>
<li><strong><code>reliable + transient_local</code></strong> — for latches. A safety bit, a map, a robot description. Late subscribers still see the last value.</li>
</ul>
<h3>The big picture: how a "robot program" looks</h3>
<pre><span class="com"> ┌──────────────┐ publishes /joint_states</span>
<span class="com"> │ motor driver │ ───────────────────────┐</span>
<span class="com"> └──────────────┘ │</span>
<span class="com"> ▼</span>
<span class="com"> ┌──────────────┐ publishes /cmd_vel ┌──────────────┐</span>
<span class="com"> │ teleop │ ───────────────────▶ │ controller │</span>
<span class="com"> └──────────────┘ │ (subscribes)│</span>
<span class="com"> └──────┬───────┘</span>
<span class="com"> ┌──────────────┐ service /set_mode │</span>
<span class="com"> │ GUI / shell │ ◀───────────────────────────┤</span>
<span class="com"> └──────────────┘ ▼</span>
<span class="com"> publishes /lowcmd</span></pre>
<p>None of these nodes know about each other. They know <em>names</em>: topic <code>/cmd_vel</code>, service <code>/set_mode</code>. Anyone can write a replacement for any node as long as it speaks the same topics. That's why ROS 2 is composable instead of monolithic.</p>
<h3>The single command to remember</h3>
<p>When you're confused — first day, hundredth day — type:</p>
<pre><span class="pmt">$</span> ros2 node list
<span class="pmt">$</span> ros2 topic list
<span class="pmt">$</span> ros2 topic info <topic> -v</pre>
<p>The graph is the truth. If a node isn't there, it's not running. If a topic isn't there, no one is publishing to it. The CLI tells you what's true right now, regardless of what your code says it's doing.</p>
<div class="callout ok">
<strong>You're equipped</strong>
That's everything. The rest of this walkthrough is just <em>practicing these five ideas</em> until they feel like reflexes. Topics, services, actions, params, TF — every robot you'll ever build with ROS 2 is some combination of those.
</div>
</section>
<!-- ============================================================ -->
<section class="lesson" id="install">
<header>
<div class="crumb">step 00</div>
<h2>Install & verify</h2>
<p class="summary">The shortest path to a green <code>ros2 doctor</code>. Idempotent. Safe to re-run.</p>
</header>
<p>This tutorial targets <strong>Debian 13 (trixie) WSL2 + NVIDIA discrete GPU</strong>. ROS 2 doesn't ship apt packages for trixie, so we use <strong>RoboStack</strong> — the same ROS 2 binaries, packaged for conda-forge, work on any Linux.</p>
<p>The install scripts are <em>discovery-first</em>: they look for an existing micromamba and ROS env before downloading anything. If you already have <code>~/micromamba/envs/ros2_jazzy</code> from another project, the install will reuse it and only add the small audio extras on top.</p>
<pre><span class="pmt">$</span> <span class="fn">bash</span> install/install.sh
<span class="com"># the script will print, in order:</span>
<span class="com"># [01_micromamba] found micromamba: ~/.local/bin/micromamba</span>
<span class="com"># [02_robostack] found working ROS 2 env: ~/micromamba/envs/ros2_jazzy</span>
<span class="com"># [03_pip_layer] installing pip extras: sounddevice, onnxruntime, …</span>
<span class="com"># [04_unitree_sdk] unitree_sdk2py present (v?)</span>
<span class="com"># [05_verify] install verified.</span></pre>
<p>Once that's green, activate the env in your shell:</p>
<pre><span class="pmt">$</span> <span class="fn">source</span> scripts/ros2_env.sh
[ros2_env] ROS_DISTRO=jazzy RMW=rmw_cyclonedds_cpp DOMAIN=42
[ros2_env] env: /home/you/micromamba/envs/ros2_jazzy
[ros2_env] ws : /home/you/SOFTWARE/ROS_tutorial/ws</pre>
<p>Then build the workspace:</p>
<pre><span class="pmt">$</span> <span class="fn">cd</span> ws
<span class="pmt">$</span> colcon build
<span class="com"># 30 seconds to 2 minutes depending on what changed</span>
<span class="pmt">$</span> <span class="fn">source</span> install/setup.bash
<span class="pmt">$</span> ros2 pkg list <span class="kw">|</span> grep -E <span class="str">'tutorial_|g1_|humanoid'</span>
g1_bridge
g1_controller
g1_speech
humanoid_msgs
tutorial_actions
tutorial_basics
tutorial_lifecycle
tutorial_pubsub
tutorial_services
tutorial_tf</pre>
<div class="callout">
<strong>Where am I?</strong>
If you ever lose track of the env state, <code>bash scripts/doctor.sh</code> dumps everything in one place — Python version, ROS distro, env packages, network interfaces, GPU presence — and writes a timestamped report you can paste into a bug report.
</div>
</section>
<!-- ============================================================ -->
<section class="lesson" id="first">
<header>
<div class="crumb">▸ first robot</div>
<h2>Bring up the Unitree G1 in rviz2 — your first ROS 2 robot</h2>
<p class="summary">The actual Unitree G1 URDF — 41 links, 165 STL meshes, 29 driven joints. Topics, services, and parameters all visible at once. Drive it before you write a line of code.</p>
</header>
<p>The package <code>tutorial_sim</code> brings up the real Unitree G1 URDF (vendored from <a href="https://github.com/unitreerobotics/unitree_ros"><code>unitreerobotics/unitree_ros</code></a> — same model used in mujoco_ros and the official ROS 2 stack). Three nodes come up via one launch file:</p>
<ul>
<li><strong><code>robot_state_publisher</code></strong> — reads the URDF, republishes each link's transform on the TF tree given the current <code>/joint_states</code>.</li>
<li><strong><code>joint_animator</code></strong> — publishes <code>/joint_states</code> at 30 Hz, driving all 29 G1 joints through one of six patterns (<code>wave</code>, <code>walk</code>, <code>squat</code>, <code>tpose</code>, <code>stretch</code>, <code>zero</code>).</li>
<li><strong><code>rviz2</code></strong> — preconfigured to show the RobotModel + TF + world grid.</li>
</ul>
<h3>Bring it up</h3>
<pre><span class="pmt">$</span> <span class="fn">bash</span> scripts/tools/sim_up.sh <span class="com"># default: wave</span>
<span class="pmt">$</span> <span class="fn">bash</span> scripts/tools/sim_up.sh walk <span class="com"># walking gait</span>
<span class="pmt">$</span> <span class="fn">bash</span> scripts/tools/sim_up.sh squat <span class="com"># squat cycle</span>
<span class="pmt">$</span> <span class="fn">bash</span> scripts/tools/sim_up.sh tpose <span class="com"># T-pose</span>
<span class="pmt">$</span> <span class="fn">bash</span> scripts/tools/sim_up.sh stretch <span class="com"># full-body rotation</span>
<span class="pmt">$</span> <span class="fn">bash</span> scripts/tools/sim_up.sh wave false <span class="com"># headless (no rviz window)</span></pre>
<p>If the G1 assets aren't fetched yet, <code>sim_up.sh</code> prints the two commands you need to run first (<code>install/06_fetch_g1_assets.sh</code> + <code>ros2_build.sh g1_description</code>). <code>scripts/deploy_and_test.sh</code> auto-fetches them.</p>
<h3>The five patterns, side by side</h3>
<figure class="shot">
<img src="screenshots/g1_wave.png" alt="G1 in rviz with right hand raised, waving">
<figcaption><strong>wave</strong> — right shoulder lifted overhead, hand waving side-to-side. Left arm relaxed at side, legs straight.</figcaption>
</figure>
<figure class="shot">
<img src="screenshots/g1_walk.png" alt="G1 in rviz with knees bent, mid-walking gait">
<figcaption><strong>walk</strong> — alternating hip pitch + knee bend, opposite-arm counter-swing, slight torso roll for balance. ~0.7 Hz gait.</figcaption>
</figure>
<figure class="shot">
<img src="screenshots/g1_squat.png" alt="G1 in rviz mid-squat, knees deeply bent">
<figcaption><strong>squat</strong> — slow knee + hip + ankle flex (4-second cycle). Arms reach forward to counter-balance.</figcaption>
</figure>
<figure class="shot">
<img src="screenshots/g1_tpose.png" alt="G1 in T-pose, arms straight out to the sides">
<figcaption><strong>tpose</strong> — both shoulders at ±π/2, arms straight out. The classic calibration pose for animation rigs.</figcaption>
</figure>
<h3>What just happened — every concept from the intro, live</h3>
<p>In a second shell, see the system you just stood up:</p>
<pre><span class="pmt">$</span> <span class="fn">source</span> scripts/ros2_env.sh
<span class="pmt">$</span> ros2 node list
/joint_animator
/robot_state_publisher
/rviz2
<span class="pmt">$</span> ros2 topic list -t
/joint_states sensor_msgs/msg/JointState
/robot_description std_msgs/msg/String
/tf tf2_msgs/msg/TFMessage
/tf_static tf2_msgs/msg/TFMessage</pre>
<p>Watch the joint stream — all 29 G1 joints in one message:</p>
<pre><span class="pmt">$</span> ros2 topic echo /joint_states --once
header: { stamp: { sec: 1778449045 } }
name:
- left_hip_pitch_joint
- left_hip_roll_joint
- left_hip_yaw_joint
- left_knee_joint
- left_ankle_pitch_joint
- left_ankle_roll_joint
- right_hip_pitch_joint
… (29 joints total)
- right_wrist_yaw_joint
position: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, …]</pre>
<pre><span class="pmt">$</span> ros2 topic hz /joint_states
average rate: 30.000 <span class="com"># exactly the publisher rate</span></pre>
<p>Publish to <code>/joint_states</code> directly — hold an arbitrary pose by hand:</p>
<pre><span class="pmt">$</span> ros2 topic pub --once /joint_states sensor_msgs/msg/JointState \
<span class="str">'{name: ["left_shoulder_pitch_joint","right_shoulder_pitch_joint"], position: [-1.5, -1.5]}'</span>
<span class="com"># the robot snaps to arms-overhead, then the animator overrides on the next tick</span></pre>
<h3>Switch the animation pattern at runtime — exercise <code>ros2 param</code></h3>
<pre><span class="pmt">$</span> ros2 param list /joint_animator
pattern
rate_hz
use_sim_time
<span class="pmt">$</span> ros2 param get /joint_animator pattern
String value is: wave
<span class="pmt">$</span> ros2 param set /joint_animator pattern walk
Set parameter successful
<span class="com"># rviz transitions to a walking gait — no restart needed</span>
<span class="pmt">$</span> ros2 param set /joint_animator pattern squat
<span class="pmt">$</span> ros2 param set /joint_animator pattern stretch
<span class="pmt">$</span> ros2 param set /joint_animator pattern zero
<span class="com"># zero = URDF natural standing pose (every joint at 0)</span></pre>
<h3>Query the G1's kinematics — exercise <code>tf2</code></h3>
<pre><span class="pmt">$</span> ros2 run tf2_ros tf2_echo pelvis head_link
- Translation: [0.000, 0.000, 0.450]
- Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.000, 1.000]
<span class="pmt">$</span> ros2 run tf2_ros tf2_echo pelvis left_wrist_yaw_link
<span class="com"># the wrist pose updates as the animation runs.</span>
<span class="pmt">$</span> ros2 run tf2_tools view_frames
<span class="com"># produces frames.pdf showing every parent→child link in the tree (41 links)</span></pre>
<h3>Drive a Twist topic — exercise the <code>cmd_vel</code> contract</h3>
<p>The <code>twist_listener</code> entry point in <code>tutorial_sim</code> subscribes to <code>/cmd_vel</code> and prints what it sees. Pair it with <code>teleop_twist_keyboard</code> for the canonical "drive a robot from the keyboard" pattern:</p>
<pre><span class="pmt">$</span> ros2 run tutorial_sim twist_listener
[INFO] listening to /cmd_vel — try teleop_twist_keyboard to drive
<span class="pmt">$</span> <span class="com"># in another shell:</span>
<span class="pmt">$</span> ros2 run teleop_twist_keyboard teleop_twist_keyboard
<span class="com"># press i, j, k, l — the listener prints arrows showing direction:</span>
[INFO] vx=+0.50 m/s →→→→→→→→→→ wz=+0.00 rad/s
[INFO] vx=+0.50 m/s →→→→→→→→→→ wz=+1.00 rad/s ⟳⟳⟳⟳⟳</pre>
<div class="callout ok">
<strong>What you've just learned in a 5-minute session</strong>
You stood up the actual Unitree G1 in 3D, inspected its live topic graph, watched all 29 joint angles stream by, queried the TF tree of 41 links, switched its motion pattern at runtime via a parameter, and drove a velocity topic with a separate keyboard process you didn't write — every CLI verb you'll use against the real hardware in lessons 07–09. <em>The verbs don't change; only the source of <code>/joint_states</code> does.</em>
</div>
<h3>What's actually under the hood</h3>
<p>The G1 URDF lives at <code>ws/src/g1_description/g1_29dof.urdf</code> (vendored from upstream by <code>install/06_fetch_g1_assets.sh</code>). The launch file (<code>ws/src/tutorial_sim/launch/g1_sim.launch.py</code>) does three things before passing the URDF to <code>robot_state_publisher</code>:</p>
<ul>
<li>rewrites every relative <code><mesh filename="meshes/X.STL"/></code> to <code>package://g1_description/meshes/X.STL</code> so rviz2 can resolve them via ament;</li>
<li>wraps the resulting XML in <code>ParameterValue(value_type=str)</code> (Jazzy launch quirk);</li>
<li>activates the rviz preset at <code>ws/src/tutorial_sim/rviz/g1.rviz</code> with <code>pelvis</code> as the fixed frame.</li>
</ul>
<figure class="shot">
<img src="screenshots/g1_wave.png" alt="The real G1 in rviz, posed mid-wave">
<figcaption>What "the real G1 URDF" looks like in rviz2: 41 fully-rendered links, the actual hardware kinematics, ready to drive from any ROS 2 node that publishes <code>/joint_states</code>.</figcaption>
</figure>
<p>The animator publishes positions for all 29 G1 joints: the six driven ones (shoulder pitch, elbow, hip pitch — each side) and the other 23 (knees, ankles, wrists, waist, shoulder roll/yaw, hip roll/yaw) held at 0.0. Without publishing the full set, the unspecified joints leave gaps in the TF tree and the body looks collapsed.</p>
<h3>Stop everything</h3>
<p>Ctrl+C in the launch shell. <code>ros2 node list</code> goes empty. No daemons left running.</p>
</section>
<!-- ============================================================ -->
<section class="lesson" id="L01">
<header>
<div class="crumb">lesson 01</div>
<h2>A minimal node — what <code>ros2 run</code> actually runs</h2>
<p class="summary">ROS 2 isn't a framework you import into <code>main()</code>. It's a runtime you join.</p>
</header>
<p>The smallest useful ROS 2 program is a class that extends <code>rclpy.node.Node</code>, a <code>main()</code> that calls <code>rclpy.init()</code>, and a <code>try/finally</code> that cleans up. <strong>Skipping the cleanup leaks a DDS participant</strong> — the robot will discover it again on the next launch and your topic graph fills with ghosts.</p>
<pre><span class="kw">import</span> rclpy
<span class="kw">from</span> rclpy.node <span class="kw">import</span> Node
<span class="kw">class</span> <span class="fn">Hello</span>(Node):
<span class="kw">def</span> <span class="fn">__init__</span>(self):
<span class="kw">super</span>().__init__(<span class="str">"hello"</span>)
self.declare_parameter(<span class="str">"period_s"</span>, <span class="num">1.0</span>)
period = <span class="kw">float</span>(self.get_parameter(<span class="str">"period_s"</span>).value)
self._count = <span class="num">0</span>
self._timer = self.create_timer(period, self._tick)
<span class="kw">def</span> <span class="fn">_tick</span>(self):
self._count += <span class="num">1</span>
self.get_logger().info(<span class="kw">f</span><span class="str">"hello (tick {self._count})"</span>)
<span class="kw">def</span> <span class="fn">main</span>(args=<span class="kw">None</span>):
rclpy.init(args=args)
node = Hello()
<span class="kw">try</span>:
rclpy.spin(node)
<span class="kw">except</span> KeyboardInterrupt:
<span class="kw">pass</span>
<span class="kw">finally</span>:
node.destroy_node()
rclpy.shutdown()</pre>
<p>Run it:</p>
<pre><span class="pmt">$</span> ros2 run tutorial_basics hello
[INFO] node up — name=hello period=1.0s
[INFO] hello, humanoid (tick 1)
[INFO] hello, humanoid (tick 2)
<span class="com">^C</span>
[INFO] interrupted — shutting down cleanly</pre>
<p>Then verify it really is on the topic graph:</p>
<pre><span class="pmt">$</span> ros2 node list
/hello
<span class="pmt">$</span> ros2 node info /hello
<span class="com"># shows Subscribers, Publishers, Service Servers, …</span></pre>
<div class="callout">
<strong>What you just did</strong>
You joined a DDS network. Other ROS 2 nodes anywhere on <code>ROS_DOMAIN_ID=42</code> can see <code>/hello</code>. The Unitree G1 also speaks CycloneDDS — the same runtime you're using here is what talks to the robot in lesson 07.
</div>
</section>
<!-- ============================================================ -->
<section class="lesson" id="L02">
<header>
<div class="crumb">lesson 02</div>
<h2>Pub/sub + QoS — why <code>best_effort</code> is the default for robots</h2>
<p class="summary">A 500 Hz IMU on <code>reliable</code> QoS saturates the network. Pick the right profile or lose the message that mattered.</p>
</header>
<p>The talker publishes a <code>sensor_msgs/JointState</code> at 50 Hz with the <strong>sensor_data</strong> QoS profile: best-effort, volatile, depth 5. A dropped sample is not retransmitted — the next one is already on the way.</p>
<pre><span class="kw">from</span> rclpy.qos <span class="kw">import</span> qos_profile_sensor_data
<span class="kw">from</span> sensor_msgs.msg <span class="kw">import</span> JointState
self._pub = self.create_publisher(
JointState, <span class="str">"/joint_state"</span>, qos_profile_sensor_data)</pre>
<p>The listener uses the matching QoS:</p>
<pre>self._sub = self.create_subscription(
JointState, <span class="str">"/joint_state"</span>, self._on_state, qos_profile_sensor_data)</pre>
<p>Bring up both in two shells:</p>
<pre><span class="pmt">$</span> ros2 run tutorial_pubsub talker
[INFO] publishing /joint_state @ 50.0 Hz, QoS=sensor_data
<span class="pmt">$</span> ros2 run tutorial_pubsub listener
[INFO] subscribed to /joint_state with sensor_data QoS
[INFO] rx# 1 pos=[+0.000, +0.000, +0.500, -0.500]
[INFO] rx# 26 pos=[+0.499, -0.499, +0.026, -0.026]</pre>
<p>And inspect what the network actually carries:</p>
<pre><span class="pmt">$</span> ros2 topic hz /joint_state
average rate: 50.000
<span class="pmt">$</span> ros2 topic info /joint_state -v
<span class="com">Type: sensor_msgs/msg/JointState</span>
<span class="com">Publisher count: 1</span>
<span class="com"> QoS profile:</span>
<span class="com"> Reliability: BEST_EFFORT</span>
<span class="com"> Durability: VOLATILE</span>
<span class="com"> Depth: 5</span></pre>
<div class="callout warn">
<strong>Common gotcha</strong>
If you forget to match QoS profiles, DDS will treat publisher and subscriber as <em>incompatible</em> and the listener gets nothing — <em>no error</em>, just silence. Always check <code>ros2 topic info -v</code> when a subscription seems dead.
</div>
</section>
<!-- ============================================================ -->
<section class="lesson" id="L03">
<header>
<div class="crumb">lesson 03</div>
<h2>Services — the right shape for rare, atomic state changes</h2>
<p class="summary">Topics are for streams. Services are for "go to damping mode, yes or no?"</p>
</header>
<p>The posture server exposes two <code>std_srvs/Trigger</code> services — the simplest possible service type: no request fields, a <code>bool success</code> and <code>string message</code> reply. That's enough for most posture transitions when the <em>intent</em> is the whole request.</p>
<pre><span class="kw">from</span> std_srvs.srv <span class="kw">import</span> Trigger
self._sit = self.create_service(
Trigger, <span class="str">"/posture/sit"</span>, self._on_sit)
<span class="kw">def</span> <span class="fn">_on_sit</span>(self, req, resp):
self._think()
<span class="kw">if</span> self._coinflip():
resp.success = <span class="kw">True</span>
resp.message = <span class="str">"sitting"</span>
<span class="kw">else</span>:
resp.success = <span class="kw">False</span>
resp.message = <span class="str">"controller refused — IMU bias too high"</span>
<span class="kw">return</span> resp</pre>
<p>Two ways to call it:</p>
<pre><span class="com"># quick-and-dirty from the CLI:</span>
<span class="pmt">$</span> ros2 service call /posture/sit std_srvs/srv/Trigger '{}'
response: std_srvs.srv.Trigger_Response(success=True, message=<span class="str">'sitting'</span>)
<span class="com"># from the lesson's client (handles timeout + reachability):</span>
<span class="pmt">$</span> ros2 run tutorial_services client sit
[INFO] sit: OK — sitting</pre>
<p>Why a custom client? Because robot code can't accept "service unreachable" as a silent failure mode. The CLI papers over that; the client doesn't:</p>
<pre><span class="kw">def</span> <span class="fn">call</span>(self, timeout_s=<span class="num">2.0</span>):
<span class="kw">if not</span> self._client.wait_for_service(timeout_sec=timeout_s):
<span class="kw">return</span> <span class="kw">False</span>, <span class="kw">f</span><span class="str">"service /posture/{self._posture} unreachable"</span>
future = self._client.call_async(Trigger.Request())
rclpy.spin_until_future_complete(self, future, timeout_sec=timeout_s)
<span class="kw">if not</span> future.done():
<span class="kw">return</span> <span class="kw">False</span>, <span class="str">"service call timed out"</span>
<span class="kw">return</span> future.result().success, future.result().message</pre>
</section>
<!-- ============================================================ -->
<section class="lesson" id="L04">
<header>
<div class="crumb">lesson 04</div>
<h2>Actions — long-running goals with feedback and cancel</h2>
<p class="summary">The right shape for motion. A walk-to-pose is an action, not a service.</p>
</header>
<p>An action has four moving parts:</p>
<ul>
<li><strong>Goal</strong> — what to achieve ("walk to (1.5, 0)").</li>
<li><strong>Feedback</strong> — published while running ("at (0.8, 0), 0.7 m to go").</li>
<li><strong>Result</strong> — published once, when finished ("arrived").</li>
<li><strong>Cancel</strong> — the caller can withdraw mid-flight; the server stops safely.</li>
</ul>
<p>We use <code>example_interfaces/Fibonacci</code> here as the shape-only demo — the same pattern drives the G1 <code>walk_to_pose</code> action in lesson 09.</p>
<pre><span class="kw">from</span> rclpy.action <span class="kw">import</span> ActionServer, CancelResponse, GoalResponse
<span class="kw">from</span> example_interfaces.action <span class="kw">import</span> Fibonacci
self._action = ActionServer(
self, Fibonacci, <span class="str">"/fibonacci"</span>,
execute_callback=self._execute,
goal_callback=self._on_goal,
cancel_callback=self._on_cancel)
<span class="kw">def</span> <span class="fn">_execute</span>(self, goal_handle):
feedback = Fibonacci.Feedback()
feedback.sequence = [<span class="num">0</span>, <span class="num">1</span>]
<span class="kw">for</span> i <span class="kw">in</span> <span class="kw">range</span>(<span class="num">2</span>, goal_handle.request.order + <span class="num">1</span>):
<span class="kw">if</span> goal_handle.is_cancel_requested:
goal_handle.canceled()
<span class="kw">return</span> Fibonacci.Result()
feedback.sequence.append(
feedback.sequence[i-<span class="num">1</span>] + feedback.sequence[i-<span class="num">2</span>])
goal_handle.publish_feedback(feedback)
time.sleep(<span class="num">0.5</span>)
goal_handle.succeed()
result = Fibonacci.Result()
result.sequence = feedback.sequence
<span class="kw">return</span> result</pre>
<p>Drive it from the CLI:</p>
<pre><span class="pmt">$</span> ros2 action send_goal /fibonacci example_interfaces/action/Fibonacci \
<span class="str">'{order: 10}'</span> --feedback
<span class="com">Sending goal:</span>
<span class="com"> order: 10</span>
<span class="com">Goal accepted with ID: …</span>
<span class="com">Feedback:</span>
<span class="com"> sequence: [0, 1, 1]</span>
<span class="com">Feedback:</span>
<span class="com"> sequence: [0, 1, 1, 2]</span>
<span class="com">…</span>
<span class="com">Result:</span>
<span class="com"> sequence: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]</span></pre>
<div class="callout">