From e39ec7c2b2fe88cb901d1c710f9da2c8df3329d7 Mon Sep 17 00:00:00 2001 From: abdelrahman Date: Tue, 10 Dec 2024 19:18:44 +0200 Subject: [PATCH 1/7] Add turtlesim_rs_msgs package This package provides message and service definitions for the turtlesim_rs package. --- examples/turtlesim_rs_msgs/CMakeLists.txt | 36 +++++++++++++++++++ examples/turtlesim_rs_msgs/msg/Color.msg | 3 ++ examples/turtlesim_rs_msgs/msg/Pose.msg | 6 ++++ examples/turtlesim_rs_msgs/package.xml | 26 ++++++++++++++ examples/turtlesim_rs_msgs/srv/Kill.srv | 2 ++ examples/turtlesim_rs_msgs/srv/SetPen.srv | 6 ++++ examples/turtlesim_rs_msgs/srv/Spawn.srv | 6 ++++ .../srv/TeleportAbsolute.srv | 4 +++ .../srv/TeleportRelative.srv | 3 ++ 9 files changed, 92 insertions(+) create mode 100644 examples/turtlesim_rs_msgs/CMakeLists.txt create mode 100644 examples/turtlesim_rs_msgs/msg/Color.msg create mode 100644 examples/turtlesim_rs_msgs/msg/Pose.msg create mode 100644 examples/turtlesim_rs_msgs/package.xml create mode 100644 examples/turtlesim_rs_msgs/srv/Kill.srv create mode 100644 examples/turtlesim_rs_msgs/srv/SetPen.srv create mode 100644 examples/turtlesim_rs_msgs/srv/Spawn.srv create mode 100644 examples/turtlesim_rs_msgs/srv/TeleportAbsolute.srv create mode 100644 examples/turtlesim_rs_msgs/srv/TeleportRelative.srv diff --git a/examples/turtlesim_rs_msgs/CMakeLists.txt b/examples/turtlesim_rs_msgs/CMakeLists.txt new file mode 100644 index 000000000..644a612d2 --- /dev/null +++ b/examples/turtlesim_rs_msgs/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.5) + +project(turtlesim_rs_msgs) + +# Default to C++14 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 14) +endif() +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +find_package(ament_cmake REQUIRED) +find_package(rosidl_default_generators REQUIRED) + +set(msg_files + "msg/Color.msg" + "msg/Pose.msg" +) + +set(srv_files + "srv/Kill.srv" + "srv/SetPen.srv" + "srv/Spawn.srv" + "srv/TeleportAbsolute.srv" + "srv/TeleportRelative.srv" +) + +rosidl_generate_interfaces(${PROJECT_NAME} + ${msg_files} + ${srv_files} +) + +ament_export_dependencies(rosidl_default_runtime) + +ament_package() diff --git a/examples/turtlesim_rs_msgs/msg/Color.msg b/examples/turtlesim_rs_msgs/msg/Color.msg new file mode 100644 index 000000000..c0af95aab --- /dev/null +++ b/examples/turtlesim_rs_msgs/msg/Color.msg @@ -0,0 +1,3 @@ +uint8 r +uint8 g +uint8 b diff --git a/examples/turtlesim_rs_msgs/msg/Pose.msg b/examples/turtlesim_rs_msgs/msg/Pose.msg new file mode 100644 index 000000000..c1d03a375 --- /dev/null +++ b/examples/turtlesim_rs_msgs/msg/Pose.msg @@ -0,0 +1,6 @@ +float32 x +float32 y +float32 theta + +float32 linear_velocity +float32 angular_velocity \ No newline at end of file diff --git a/examples/turtlesim_rs_msgs/package.xml b/examples/turtlesim_rs_msgs/package.xml new file mode 100644 index 000000000..b0574d042 --- /dev/null +++ b/examples/turtlesim_rs_msgs/package.xml @@ -0,0 +1,26 @@ + + turtlesim_rs_msgs + 0.1.0 + Package containing message and service definitions for the turtlesim_rs package. + user + + Apache License 2.0 + Abdelrahman osama + + rclrs + std_msgs + + ament_cmake + rosidl_default_generators + rosidl_generator_rs + + rosidl_default_runtime + + ament_lint_common + + rosidl_interface_packages + + + ament_cmake + + diff --git a/examples/turtlesim_rs_msgs/srv/Kill.srv b/examples/turtlesim_rs_msgs/srv/Kill.srv new file mode 100644 index 000000000..1da96270a --- /dev/null +++ b/examples/turtlesim_rs_msgs/srv/Kill.srv @@ -0,0 +1,2 @@ +string name +--- \ No newline at end of file diff --git a/examples/turtlesim_rs_msgs/srv/SetPen.srv b/examples/turtlesim_rs_msgs/srv/SetPen.srv new file mode 100644 index 000000000..a1b3d9cc9 --- /dev/null +++ b/examples/turtlesim_rs_msgs/srv/SetPen.srv @@ -0,0 +1,6 @@ +uint8 r +uint8 g +uint8 b +uint8 width +uint8 off +--- diff --git a/examples/turtlesim_rs_msgs/srv/Spawn.srv b/examples/turtlesim_rs_msgs/srv/Spawn.srv new file mode 100644 index 000000000..b8eeaeee0 --- /dev/null +++ b/examples/turtlesim_rs_msgs/srv/Spawn.srv @@ -0,0 +1,6 @@ +float32 x +float32 y +float32 theta +string name # Optional. A unique name will be created and returned if this is empty +--- +string name \ No newline at end of file diff --git a/examples/turtlesim_rs_msgs/srv/TeleportAbsolute.srv b/examples/turtlesim_rs_msgs/srv/TeleportAbsolute.srv new file mode 100644 index 000000000..0dc51b99a --- /dev/null +++ b/examples/turtlesim_rs_msgs/srv/TeleportAbsolute.srv @@ -0,0 +1,4 @@ +float32 x +float32 y +float32 theta +--- diff --git a/examples/turtlesim_rs_msgs/srv/TeleportRelative.srv b/examples/turtlesim_rs_msgs/srv/TeleportRelative.srv new file mode 100644 index 000000000..842dcb1e2 --- /dev/null +++ b/examples/turtlesim_rs_msgs/srv/TeleportRelative.srv @@ -0,0 +1,3 @@ +float32 linear +float32 angular +--- From 4f3a41fd0f1be4d6ae2e15c0758b081adff0e666 Mon Sep 17 00:00:00 2001 From: abdelrahman Date: Tue, 10 Dec 2024 19:29:13 +0200 Subject: [PATCH 2/7] Add turtlesim_rs package as a Rust-based implementation of turtlesim --- examples/turtlesim_rs/Cargo.toml | 40 + examples/turtlesim_rs/images/box-turtle.png | Bin 0 -> 2760 bytes examples/turtlesim_rs/images/diamondback.png | Bin 0 -> 3913 bytes examples/turtlesim_rs/images/electric.png | Bin 0 -> 3694 bytes examples/turtlesim_rs/images/fuerte.png | Bin 0 -> 3923 bytes examples/turtlesim_rs/images/groovy.png | Bin 0 -> 3078 bytes examples/turtlesim_rs/images/hydro.png | Bin 0 -> 22447 bytes examples/turtlesim_rs/images/hydro.svg | 195 +++++ examples/turtlesim_rs/images/indigo.png | Bin 0 -> 21298 bytes examples/turtlesim_rs/images/indigo.svg | 691 ++++++++++++++++++ examples/turtlesim_rs/images/jade.png | Bin 0 -> 2797 bytes examples/turtlesim_rs/images/kinetic.png | Bin 0 -> 3042 bytes examples/turtlesim_rs/images/kinetic.svg | 137 ++++ examples/turtlesim_rs/images/lunar.png | Bin 0 -> 4143 bytes examples/turtlesim_rs/images/lunar.svg | 92 +++ examples/turtlesim_rs/images/melodic.png | Bin 0 -> 1357 bytes examples/turtlesim_rs/images/robot-turtle.png | Bin 0 -> 2886 bytes examples/turtlesim_rs/images/sea-turtle.png | Bin 0 -> 3078 bytes examples/turtlesim_rs/images/turtle.png | Bin 0 -> 5269 bytes examples/turtlesim_rs/package.xml | 24 + examples/turtlesim_rs/src/lib.rs | 2 + examples/turtlesim_rs/src/turtle.rs | 346 +++++++++ examples/turtlesim_rs/src/turtle_frame.rs | 379 ++++++++++ examples/turtlesim_rs/src/turtlesim.rs | 65 ++ .../tutorials/turtle_teleop_key.rs | 97 +++ 25 files changed, 2068 insertions(+) create mode 100644 examples/turtlesim_rs/Cargo.toml create mode 100644 examples/turtlesim_rs/images/box-turtle.png create mode 100644 examples/turtlesim_rs/images/diamondback.png create mode 100644 examples/turtlesim_rs/images/electric.png create mode 100644 examples/turtlesim_rs/images/fuerte.png create mode 100644 examples/turtlesim_rs/images/groovy.png create mode 100644 examples/turtlesim_rs/images/hydro.png create mode 100644 examples/turtlesim_rs/images/hydro.svg create mode 100644 examples/turtlesim_rs/images/indigo.png create mode 100644 examples/turtlesim_rs/images/indigo.svg create mode 100644 examples/turtlesim_rs/images/jade.png create mode 100644 examples/turtlesim_rs/images/kinetic.png create mode 100644 examples/turtlesim_rs/images/kinetic.svg create mode 100644 examples/turtlesim_rs/images/lunar.png create mode 100644 examples/turtlesim_rs/images/lunar.svg create mode 100644 examples/turtlesim_rs/images/melodic.png create mode 100644 examples/turtlesim_rs/images/robot-turtle.png create mode 100644 examples/turtlesim_rs/images/sea-turtle.png create mode 100644 examples/turtlesim_rs/images/turtle.png create mode 100644 examples/turtlesim_rs/package.xml create mode 100644 examples/turtlesim_rs/src/lib.rs create mode 100644 examples/turtlesim_rs/src/turtle.rs create mode 100644 examples/turtlesim_rs/src/turtle_frame.rs create mode 100644 examples/turtlesim_rs/src/turtlesim.rs create mode 100644 examples/turtlesim_rs/tutorials/turtle_teleop_key.rs diff --git a/examples/turtlesim_rs/Cargo.toml b/examples/turtlesim_rs/Cargo.toml new file mode 100644 index 000000000..e931bc9ff --- /dev/null +++ b/examples/turtlesim_rs/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "turtlesim_rs" +version = "0.1.0" +edition = "2024" + +[dependencies] +eframe = "0.29.1" +egui_extras = { version = "0.29.1", features = ["all_loaders"]} +tiny-skia = "0.11.4" +rand = "0.8.5" +termion = "1.5" + +[[bin]] +name = "turtlesim_node" +path = "src/turtlesim.rs" + +[[bin]] +name = "turtle_teleop_key" +path = "tutorials/turtle_teleop_key.rs" + +[dependencies.rclrs] +version = "0.4" + +[dependencies.std_msgs] +version = "*" + +[dependencies.std_srvs] +version = "*" + +[dependencies.geometry_msgs] +version = "*" + +[dependencies.turtlesim_rs_msgs] +version = "0.1.0" + +[dependencies.rosidl_runtime_rs] +version = "0.4" + + + diff --git a/examples/turtlesim_rs/images/box-turtle.png b/examples/turtlesim_rs/images/box-turtle.png new file mode 100644 index 0000000000000000000000000000000000000000..6584fd27e91e8da5a598ecfe310723085f76e220 GIT binary patch literal 2760 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 O0000q$gGRCwBA`2YVu12(_}V>mcCB!H;J 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 literal 0 HcmV?d00001 diff --git a/examples/turtlesim_rs/images/fuerte.png b/examples/turtlesim_rs/images/fuerte.png new file mode 100644 index 0000000000000000000000000000000000000000..b633f4d446ebc55625f1f158432bc79dd976754f GIT binary patch literal 3923 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 literal 0 HcmV?d00001 diff --git a/examples/turtlesim_rs/images/groovy.png b/examples/turtlesim_rs/images/groovy.png new file mode 100644 index 0000000000000000000000000000000000000000..e6932521a01ba2d2c6d4cd52a017a6b0b63b044d GIT binary patch literal 3078 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 N02c==x^@>K@jw6YCMy5{ literal 0 HcmV?d00001 diff --git a/examples/turtlesim_rs/images/hydro.png b/examples/turtlesim_rs/images/hydro.png new file mode 100644 index 0000000000000000000000000000000000000000..58868fd1f9ab0d08980e693a7a6bee8538542fb4 GIT binary patch literal 22447 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 aqQitJuGn}T5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/turtlesim_rs/images/indigo.png b/examples/turtlesim_rs/images/indigo.png new file mode 100644 index 0000000000000000000000000000000000000000..d57670ace58c7ef687eae3e197fdcad2511e10ae GIT binary patch literal 21298 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/turtlesim_rs/images/jade.png b/examples/turtlesim_rs/images/jade.png new file mode 100644 index 0000000000000000000000000000000000000000..f9029198b566a671cc7cbe84ff30ee5f8a8e730b GIT binary patch literal 2797 zcmV@~|T0b>IZDG7lkP)QnLMHjQ6U3PZ0H)&^QHPhQY zJ$*j^=$XBM1e8VkR2|E?Vnn-uVBBMj;W*!wUd&GzkE zSlHUmXI^=fGn3=&`rU4J51nB7s+H&sod8!i!hqet+-m&^??3f?vY1Mq_pXEg4oaaH z&c&SQw;BO>$qf3r{1KF@#(PhIb)jX1>Yo1t82ms0*2XW)aFO`%X;3r#&C*mJ-djKR zj~@s?FPp*7<@{t4 z_LYK(Nr2(Yv9Nua04f|Cm3BaDB(cZn@+{n#li+1D_aU+i0jlwo7o&B6;&Xm3|C9Q) z|1SV%0NY7y9#UX zc1z9&R4Ngnqp4IPDp8ftv8Yt4NI68WSm>zc)W3S!%yYH5jWIX_#0p>?;MJWL7yzDA zOXgjn=X%7)`;oS0!X4Mlii7o)Q_mekjhE3M%E0+aOy9W?6x)F-fR@^olR&qh%XeRh zVRaF3KSim#RC^1!76=dsb< zFekB71w=72Rt9690B)~`uXg~P0qzINOMN3OD?=V5JOr=;RtbKxF8Et7yl!PAV zNH2?AShG0I=&B6WUmwA~e(ki#hz-_tR}#O{{W0f?xsOLf6Qi@eSmYPhHJwR5ohh2m z6u_~F$4b0-aD>7{gdQqlbr0gUHZeI`Oc+usxmoR_lp-nxG%sqy>KVZr7-MuS;OL1l zqOlUVHl}A}w!yj%)~1yp^3*H_y=-PPw)Iu_Z*PLw%yOaV;XPmkeeOKrpIi$q=>*_V z5sE%RN6SSWL_}~8_o9yUBqVLf5N%$GbD$dp%&9@_>(|VVdux{vA9}OhSKTI*`%x`(Zy}b)udlq|@AHM+Am}uKt;wAGC z5uBzJ=@pAmL*q!%N5uyF)EQJ5VKN<{HO}E4+~0JgYN<~zX~(*8JtmA0W02M+WWgM) zx6TqD>A~KzdNzw!E+F1_a+R~RZI|t3Gdt8=589d`s?GqYlK+l{yFP>MrO;YW`~96; z33ok{c6seXbj4?=TsLb`?)i+R;>a^F_hhw3Zwj(3O zKhwZ=o#}w;3~+vh@cyTfD1HUNPP@cmg`il*A1&gK7Qq zqY#RBv5Nr5q%9V?$i25){h>g%V7L0RM#I z%h4lyXK(Y0g%F!AjghO>()MYu{m)mSG&$nu^7kV2Dc7onj@h@4gNV3lK);LLa4o`# zauR|rt^!{7bNR1JSnZ8|wtpJnvUV`CRwFV;wKgT-9(xJ%#_&lX1K=OcpOYZEsLYr= z_{Qw8^!5=%R8Egv@RZY(LMAKtxqQBse^;3Pb4Mx!3e3=rVHG`!aDFeXf-Re%u+R$#@*k2!MfRQNrMr*Y(~6nCRNM7 z8|7T7Cq8r4L%E`MLaqGum|@q8c9~B@~2rY)OPc7?FPZNX=L@(dHGzS1p>B{Emflq!;VNK*F$A z1h;NVek(=#$wT#PAMji}7MeaNFjZI2D@OTa&mp!$a-n6q9GCI|}J14zhZAE2oD8;!+4&9q@Dc z>l?1>yliG2N}WW3NRlp;V-b8JJLR30Yty=ya8N05vFiwqjj-0Wdu=-?p%=U*h71;KXo%= zYn=Vv=(8oXr9nVj7J{J82RJXD1g&s)ew6Tm=Rm7O>j7$>2e7zNfN@=>8m%#nucb`Fq#xaBA z0Nt0vNkp$&gy|jvrLOXG`JUSGKfuTV<+*cN+=hMKa#W@RWn093XE6ugNZN4KcppmL z;pg(hwRyK;F4sfAH~pjiZ_aodg`dkm>18uFNuhKL@Quszn(KXBcei)_N@;)mb28`CH;Z@e zvi!y7H{6?P;9-736n_d!51`dn;H^fxs{p?TPz&ttwbQrU00000NkvXXu0mjfC}3Cv literal 0 HcmV?d00001 diff --git a/examples/turtlesim_rs/images/kinetic.png b/examples/turtlesim_rs/images/kinetic.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8a3b1f9d1b514ab85e461a90435f54c71afb37 GIT binary patch literal 3042 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 literal 0 HcmV?d00001 diff --git a/examples/turtlesim_rs/images/kinetic.svg b/examples/turtlesim_rs/images/kinetic.svg new file mode 100644 index 000000000..ad78b79f6 --- /dev/null +++ b/examples/turtlesim_rs/images/kinetic.svg @@ -0,0 +1,137 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/examples/turtlesim_rs/images/lunar.png b/examples/turtlesim_rs/images/lunar.png new file mode 100644 index 0000000000000000000000000000000000000000..a18f766396e763f41531256019479a5b1f23aa50 GIT binary patch literal 4143 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 literal 0 HcmV?d00001 diff --git a/examples/turtlesim_rs/images/lunar.svg b/examples/turtlesim_rs/images/lunar.svg new file mode 100644 index 000000000..0835959a0 --- /dev/null +++ b/examples/turtlesim_rs/images/lunar.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/turtlesim_rs/images/melodic.png b/examples/turtlesim_rs/images/melodic.png new file mode 100644 index 0000000000000000000000000000000000000000..cab24085428647f2e999725ece41203e708a8262 GIT binary patch literal 1357 zcmV-T1+w~yP)_S4DLZR)V8_B|jU>dUVw-zedNH_AH^Ua&@dhWb4 z_s+e!g8g84bLY;S`R2@-bIzS-%rl7AQ3bs8!c6Pe#}9|T-nBpP*wXW_`?hS^_uKy~ z(w;1cc1ia9+JbA}SXg71D0w?}C?5N)*uIOg`6e1#ydzAqD}&o+x&5{zPt9)^jL#PB z7sKkVEQ)c+IGIec{EthxTy_oj2N zxS!A_h!28AE&BIEwqLyPiTUd0r;XY4kQxO(dvwPPUKuzOLA~?&FC^9q#yL`9d)fPT z6QrkJyPOG?gn=us&c+B2y%v*kV)FIoNPlzx{U*fZAWesOa`;_yq&?mhE-r&_>((F~ z-H9fGOU_3ZAUV(o(hi|%m6Emst_nhZ>GUPx&bd>Cat=0sQi;dp%b=4)8f3ihbOQIS z-^Ic5TkQmU!dqwn;iVUcW`0sXSGNvZX7%?Li343JA6^Fm>CC0ya`I!l64kVHV~lVB ziA(L0Fv&@1?}XOJqA1Tr%nbtSLG)TQ&@cdD5Xwbp=0H+!{_v+Gg@QWT@J_;y9V0B~ zBnJ(ZBO)|`Nq43PS=-!RJJaBS===4ze;H%Ts$XDkj5+0Gk$BmTOYIlBJZe~Oog|D% zw|wU?dw%bY=f`BQ4Smp;(Gky(1)1SIWhnL25TXg<&JwM>?t59Bg=JcG9fa!sjxV=f+r* z;39brLI}4S4MK&%C2;%>u^<9MAOep;GoX3!od)5%i7aoDI0H$F zUk*uxi!AWl-Ga}t4{e$83T&4iL=mcUlfpLfi{}{07~#m+Zh>$9(i6rm3{wwcPljq9 zCj8Pyp|#8pPxEk&o~6yJW36nfZIIg1CP%(5)8Az8Atwc3xAX!e9J5=#!+3ffw-bW+ zQe<9u4qI|oft&+Vm+t%6iPy>5R#DEm4AWG2Ujl!5tm(N&m4 zs-|y(v_?#lVmoXZK$s7`N^Go&xVxi}&P@tZv`a~f$v$(EF!`4$Ee<2{7eV~FR1URV zUQvn(UyMbeXbF^-(t7+|n>0S(!;6wICtyGb-5!Gv2%%`Ez?fjBj;u zDyKmiR5Z<#pzJ5dRy~A36`OVkW$EhAJ%n7%lgJPqYBpS?$OikS!XX5z*mQPrj-uqj zW06u@1PN1)8D(#kuFX>YbneoI;D;^|`sh%*k}wfo(xmVyG!IJ_vwfivmLxKAMh`yP zt`Ly9f6Wo^!2zTYFEfvMBQ8}U=<~yqmo#Ake4lyW-ZcwlI%;lslSbJk752jSDRLDQvUG|`Dt$UgRBVh P00000NkvXXu0mjf$Ult2 literal 0 HcmV?d00001 diff --git a/examples/turtlesim_rs/images/robot-turtle.png b/examples/turtlesim_rs/images/robot-turtle.png new file mode 100644 index 0000000000000000000000000000000000000000..126533b1ffeca45792edd283a70748b425743dff GIT binary patch literal 2886 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 U-{_OCt^fc407*qoM6N<$f{t0(QUCw| literal 0 HcmV?d00001 diff --git a/examples/turtlesim_rs/images/turtle.png b/examples/turtlesim_rs/images/turtle.png new file mode 100644 index 0000000000000000000000000000000000000000..da835ad42dcf65a68b76c00da4fe15b4f1c0b35f GIT binary patch literal 5269 zcmcInX>45875?75ZQksj8G9VZi5=o3q}d<|DN7-N21vR<>L8X<)JA1d)uL3QKPt6C z6|Gc4>92|(Rf}37)Y6JVH;Rf738E!LBm@Y~9y?wVd&aZ9+1|`sd+sx{+0NFgM|yMb zJ?GwY?pg1dJv;8YGwAE_0SIp2wsnxkMf9!qO7#27*ZwBcz&$Z|=WTf9gWe=9l<#hP z;0Yk~4*R(MyI*>kkn;DofBAO#gd%iu3FEK(e+a}^Y~On8J@+eIZ+v{3qb*ki{;+T9 zwRfy}Hry4u)i$l-#PP}B9{tzJA6u5CUoN9766*DN`)+@D^ZN^zcX!qWIJSesZyb5` z!#9R@>bmaK@fRCC7bDK5t>3=pnO1>}&UrYj-m>y5@x{>{v-e+Uec3_^Mb@#gv%C6 z<{i!_Rdv?+#y zd0i+ZzAnq2$2blPl(0xC;sIxJFbocs0obH5$~8|c&aDlGJ&$%o`DnSw@6&bbfR(44tKpL}}veR4qFv-jwJ(K4*5llxBYb1K?yV!-~EFK=FyqQ#4ZxI&zK- z58u3ZEnk34awO44Rda@p4Ou&41Kroje%Zfs+ZTLO=Td)u{<)XNgr|P~#Jz{k9C^0q zszh&MAdb>hsYkTz^<+J8J_)O?SpeBP*9+m0gnY6FkhXOY0|BH`4uZ8bWZqh}ctz;c zntL~ZtOc2P8B@k;+WRQ~rJY;%*a2sw>M4?sEZ}4XsOqT7C3I~N;n4*6+&8Aqx;dUs zr!XOAk+W3#3dodID0~Q}96*4pA`-ArsW=F9N0Bjdh(|g$eC>h34W5rr95XUz&gE1Y zE+Qz=&B=?=H8KSakV64fjhd1^olT`m$j0em2~seH99SV6PPc=Co`**gkwFv|>0nHZ zLQ^&O)U=+(-UIt{o*OrAT;sK67@7fvQr|F57+uANuXtjXra);0lY-|MG2pySf7a=G ztV+Cea*cv)%?p8dkUuf(l7oBzn>Ss*iK9G>Yl7K#TJ-Ez~K zBhgSWJXARho_tXUn=H7@+lNrV%^VkPknjIKb^>N}uGt;a!K!=+(N6;`m&rqtbm);@ zBbT+F{N&9OPrJ33GvEjuADYah+5|EQ5i8s#kl{p;$JJsSuG$RI0SPbQGzT%n2X*v*qkm7B;G$R9rF6)$}<^qh#kSc2`F1Iri2A*Jbf%fsU-14~7U ztQPPGSTPwuD75k>MNwSKx&`0oSJuh0gkXY);x=V7r_8cJ5okjY+`F}~qT5nAT(0yW z5(~q!Ez-t;AsXPb?GF*lQHfsRKp@Z|dxgcChEZ22@A34z9KG$#1;2>Nw1$3p?qf7D zS;WHC2}nviE%`AO470sQqwBbr?CtGZlFt=KT`AP(#7xtIpvtR9M<{P`kQ z-LPo(R(Gw0y+~7msgy`#R#fOS=+faZ2@?c21?sk;Y-aSiDUcge5ntKm<|yOJbWr>f zlB0Qa_jkC0T7Qh11#d72Ur(b;ML9(mAh7M3-q;l?iK$W`z(dt~s2gZ%Tghth#RCn2 zb>^ZXB27L`Q1Gx8v=wq>1%I%a(~MThWl&6=LUNjFB!Me57O#$Ub@8Z7R^YP)m}F{; zl1net5M51nnFsv6VT>G2A=VwFAerfZP0~{0+n+#ol+&w_s2Hfl+F7=Cg;pvywHQ2Z zyrvT?RxV?B7mf^_$DvxOP5U~{q!K4bg3ve;!dyosE^{`IzD09QZk9DhuV^|y%+9;f zJAB4`msO2IE?wv8j;lY@12H?}k2V4mQZy-gn_4k5cl1Y*oNz7J*j}TDgx70iIp=_; zX>JSS3eD$y$BRXKxL8O+*PE>kYdx;(Pr#%UX@yFvj9}E?igM>X6%m~}36s6cpF$;- zqnCz5O?UoL$GK5MH^7o5QfEg8cZ*>th{c;G@={?qbMPaGr91>`d-;M&uZImO)L!oD z3`J^dvJ}~@fvb|1mEm*XEYlj9G9D>w^wJa5QwArKAypJA$fi$W>9T%$PhBJ6@JGiG zjm^wm=aBZ8*rIUX!p z(d}+a4UNF(Xefz(EZnrTub3{K%AHp~W<<|}-`w%o;FfJ&Y$5rnZCTPwH+69C_-Rnb zaO&+h-upHETem%$Ry7B=ZqIfKG1Q7PPf}!UKPEX}$>%YV zIbFu|Xq5`MiCWbrZ5)hesWlbaFJjjW#JLg*XGUSzlxs`+ppXKlZeeZWYP|UTtKTUW zDY*&UK>Gf-_gwe(%kSO$(B?s;k7YkOb71uOL+_qjd-k06Rl&oNgQ?n_G%-D;;M{Z= zl^W$Dy&Pt_2EJ5AxR}9GYC5~Aw^H-^-m#+aJ9;VY0dGI-N&CEHiZ4VX;p0pogSKjY z!_6C(?tkYHD^+Bi8A!x^iG`i=4}4yJmn3=ub-Y^B(Azun$DBP*<+61&m$#liJ;J?M zC=?u?=VMRq*}W^B%DwiBXMS_6evXZ8LR$&5=hWo~maXr3ap6GxZnLPNOa;Ztq+h3Y zWM496yp_(1hg4N3is>&t=vzKHG1 + turtlesim_rs + 0.1.0 + + turtlesim_rs is a ROS2 package implemented in Rust, designed to teach ROS concepts and serve as + an educational tool for developing ROS packages in Rust. + + + user + Apache License 2.0 + Abdelrahman osama + + rclrs + std_msgs + std_srvs + geometry_msgs + turtlesim_rs_msgs + + rosidl_runtime_rs + + + ament_cargo + + diff --git a/examples/turtlesim_rs/src/lib.rs b/examples/turtlesim_rs/src/lib.rs new file mode 100644 index 000000000..de5080c83 --- /dev/null +++ b/examples/turtlesim_rs/src/lib.rs @@ -0,0 +1,2 @@ +pub mod turtle; +pub mod turtle_frame; diff --git a/examples/turtlesim_rs/src/turtle.rs b/examples/turtlesim_rs/src/turtle.rs new file mode 100644 index 000000000..fe6c7d875 --- /dev/null +++ b/examples/turtlesim_rs/src/turtle.rs @@ -0,0 +1,346 @@ +use eframe::egui::{Image, Pos2, Rect, Ui, Vec2}; +use rclrs::{Node, Time, QOS_PROFILE_DEFAULT}; +use std::sync::mpsc::{channel, Receiver}; +use std::time; +use std::{ + f32::consts::{FRAC_PI_2, PI}, + sync::{Arc, Mutex}, +}; +use tiny_skia::{LineCap, Paint, PathBuilder, Pixmap, Stroke, Transform}; + +use crate::turtle_frame::{TURTLE_IMG_HEIGHT, TURTLE_IMG_WIDTH}; + +const DEFAULT_PEN_R: u8 = 0xb3; +const DEFAULT_PEN_G: u8 = 0xb8; +const DEFAULT_PEN_B: u8 = 0xff; +const DEFAULT_PEN_ALPHA: u8 = 255; +const DEFAULT_STROKE_WIDTH: f32 = 3.0; + +enum TurtleSrvs { + SetPen(u8, u8, u8, u8, u8), + TeleportAbsolute(f32, f32, f32), + TeleportRelative(f32, f32), +} + +pub struct TurtleVel { + lin_vel: f64, + ang_vel: f64, + last_command_time: Time, +} + +pub struct Pen<'a> { + paint: Paint<'a>, + stroke: Stroke, +} + +#[allow(unused)] +pub struct Turtle<'a> { + node: Arc, + image: Image<'a>, + pos: Pos2, + orient: f32, + meter: f32, + + pen_: Pen<'a>, + pen_on: bool, + + turtle_vel: Arc>, + + srv_rx: Receiver, + + velocity_sub: Arc>, + pose_pub: Arc>, + color_pub: Arc>, + set_pen_srv: Arc>, + teleport_absolute_srv: Arc>, + teleport_relative_srv: Arc>, +} + +impl<'a> Turtle<'a> { + pub fn new(node: Arc, real_name: &str, image: Image<'a>, pos: Pos2, orient: f32) -> Self { + let meter = TURTLE_IMG_HEIGHT; + + let turtle_vel = Arc::new(Mutex::new(TurtleVel { + lin_vel: 0.0, + ang_vel: 0.0, + last_command_time: node.get_clock().now(), + })); + + let turtle_vel_clone = Arc::clone(&turtle_vel); + let node_clone = Arc::clone(&node); + + let velocity_sub = node + .create_subscription( + &(real_name.to_owned() + "/cmd_vel"), + QOS_PROFILE_DEFAULT, + move |msg: geometry_msgs::msg::Twist| { + let mut vel = turtle_vel_clone.lock().unwrap(); + vel.lin_vel = msg.linear.x; + vel.ang_vel = msg.angular.z; + vel.last_command_time = node_clone.get_clock().now(); + }, + ) + .unwrap(); + + let pose_pub = node + .create_publisher(&(real_name.to_owned() + "/pose"), QOS_PROFILE_DEFAULT) + .unwrap(); + + let color_pub = node + .create_publisher( + &(real_name.to_owned() + "/color_sensor"), + QOS_PROFILE_DEFAULT, + ) + .unwrap(); + + let (srv_tx, srv_rx) = channel(); + + let set_pen_srv_tx = srv_tx.clone(); + let set_pen_srv = node + .create_service( + &(real_name.to_owned() + "/set_pen"), + move |_, srv: turtlesim_rs_msgs::srv::SetPen_Request| { + let (r, g, b, width, off) = (srv.r, srv.g, srv.b, srv.width, srv.off); + set_pen_srv_tx + .send(TurtleSrvs::SetPen(r, g, b, width, off)) + .unwrap(); + turtlesim_rs_msgs::srv::SetPen_Response::default() + }, + ) + .unwrap(); + + let teleport_absolute_srv_tx = srv_tx.clone(); + let teleport_absolute_srv = node + .create_service( + &(real_name.to_owned() + "/teleport_absolute"), + move |_, srv: turtlesim_rs_msgs::srv::TeleportAbsolute_Request| { + let x = srv.x; + let y = srv.y; + let theta = srv.theta; + teleport_absolute_srv_tx + .send(TurtleSrvs::TeleportAbsolute(x, y, theta)) + .unwrap(); + turtlesim_rs_msgs::srv::TeleportAbsolute_Response::default() + }, + ) + .unwrap(); + + let teleport_relative_srv_tx = srv_tx.clone(); + let teleport_relative_srv = node + .create_service( + &(real_name.to_owned() + "/teleport_relative"), + move |_, srv: turtlesim_rs_msgs::srv::TeleportRelative_Request| { + let linear = srv.linear; + let angular = srv.angular; + teleport_relative_srv_tx + .send(TurtleSrvs::TeleportRelative(linear, angular)) + .unwrap(); + turtlesim_rs_msgs::srv::TeleportRelative_Response::default() + }, + ) + .unwrap(); + + let stroke = Stroke { + width: DEFAULT_STROKE_WIDTH, + line_cap: LineCap::Round, + ..Default::default() + }; + + let mut paint = Paint::default(); + paint.set_color_rgba8( + DEFAULT_PEN_R, + DEFAULT_PEN_B, + DEFAULT_PEN_G, + DEFAULT_PEN_ALPHA, + ); + paint.anti_alias = true; + + let pen_ = Pen { paint, stroke }; + let pen_on = true; + + Turtle { + node, + image, + pos, + orient, + meter, + + pen_, + pen_on, + + turtle_vel, + + srv_rx, + + velocity_sub, + pose_pub, + color_pub, + set_pen_srv, + teleport_absolute_srv, + teleport_relative_srv, + } + } + + fn rotate_image(&mut self) { + let image = self.image.clone(); + self.image = image.rotate(-self.orient + FRAC_PI_2, Vec2::splat(0.5)); + } + + pub fn update( + &mut self, + dt: f64, + path_image: &mut Pixmap, + canvas_width: f32, + canvas_height: f32, + ) -> bool { + let mut modified = false; + + let old_orient = self.orient; + let old_pos = self.pos; + + modified |= self.handle_service_requests(path_image, old_pos, canvas_height); + + let mut turtle_vel = self.turtle_vel.lock().unwrap(); + let is_old_command = self + .node + .get_clock() + .now() + .compare_with(&turtle_vel.last_command_time, |now_ns, command_ns| { + let diff_ns = (now_ns - command_ns) as u64; + time::Duration::from_nanos(diff_ns) > time::Duration::from_secs(1) + }) + .unwrap(); + + if is_old_command { + turtle_vel.lin_vel = 0.0; + turtle_vel.ang_vel = 0.0; + } + + let lin_vel_ = turtle_vel.lin_vel; + let ang_vel_ = turtle_vel.ang_vel; + + drop(turtle_vel); + + self.orient += (ang_vel_ * dt) as f32; + // Keep orient between -pi and +pi + self.orient -= 2.0 * PI * ((self.orient + PI) / (2.0 * PI)).floor(); + + self.pos.x += self.orient.cos() * (lin_vel_ * dt) as f32; + self.pos.y += -self.orient.sin() * (lin_vel_ * dt) as f32; + + // Clamp to screen size + if self.pos.x < 0.0 + || self.pos.x > canvas_width + || self.pos.y < 0.0 + || self.pos.y > canvas_height + { + println!( + "Oh no! I hit the wall! (Clamping from [x={}, y={}])", + self.pos.x, self.pos.y + ); + } + + self.pos.x = f32::min(f32::max(self.pos.x, 0.0), canvas_width); + self.pos.y = f32::min(f32::max(self.pos.y, 0.0), canvas_height); + + let pose_msg = turtlesim_rs_msgs::msg::Pose { + x: self.pos.x, + y: canvas_height - self.pos.y, + theta: self.orient, + linear_velocity: lin_vel_ as f32, + angular_velocity: ang_vel_ as f32, + }; + + self.pose_pub.publish(pose_msg).unwrap(); + + let pixel_color = path_image.pixel( + (self.pos.x * self.meter) as u32, + ((canvas_height - self.pos.y) * self.meter) as u32, + ); + + if let Some(color) = pixel_color { + let color_msg = turtlesim_rs_msgs::msg::Color { + r: color.red(), + g: color.green(), + b: color.blue(), + }; + self.color_pub.publish(color_msg).unwrap(); + } + + if self.orient != old_orient { + modified = true; + self.rotate_image(); + } + + if self.pos != old_pos { + modified = true; + + if self.pen_on { + self.draw_line_on_path_image(path_image, old_pos, self.pos); + } + } + + modified + } + + fn handle_service_requests( + &mut self, + path_image: &mut Pixmap, + old_pos: Pos2, + canvas_height: f32, + ) -> bool { + let mut modified = false; + + for srvs in self.srv_rx.try_iter() { + match srvs { + TurtleSrvs::SetPen(r, g, b, width, off) => { + self.pen_.paint.set_color_rgba8(r, g, b, 255); + self.pen_.stroke.width = width as f32; + self.pen_on = off == 0; + } + TurtleSrvs::TeleportAbsolute(x, y, theta) => { + self.pos.x = x; + self.pos.y = canvas_height - y; + self.orient = theta; + self.draw_line_on_path_image(path_image, old_pos, self.pos); + modified = true; + } + TurtleSrvs::TeleportRelative(linear, angular) => { + self.orient += angular; + self.pos.x += self.orient.cos() * linear; + self.pos.y += -self.orient.sin() * linear; + self.draw_line_on_path_image(path_image, old_pos, self.pos); + modified = true; + } + } + } + + modified + } + + pub fn paint(&self, ui: &mut Ui) { + let top_left_pos = Pos2 { + x: self.pos.x * self.meter - TURTLE_IMG_WIDTH / 2.0, + y: self.pos.y * self.meter - TURTLE_IMG_HEIGHT / 2.0, + }; + let image_rect = + Rect::from_min_size(top_left_pos, Vec2::new(TURTLE_IMG_WIDTH, TURTLE_IMG_HEIGHT)); + self.image.paint_at(ui, image_rect); + } + + fn draw_line_on_path_image(&self, path_image: &mut Pixmap, pos1: Pos2, pos2: Pos2) { + let mut path_builder = PathBuilder::new(); + path_builder.move_to(pos1.x * self.meter, pos1.y * self.meter); + path_builder.line_to(pos2.x * self.meter, pos2.y * self.meter); + + if let Some(path) = path_builder.finish() { + path_image.stroke_path( + &path, + &self.pen_.paint, + &self.pen_.stroke, + Transform::identity(), + None, + ); + } + } +} diff --git a/examples/turtlesim_rs/src/turtle_frame.rs b/examples/turtlesim_rs/src/turtle_frame.rs new file mode 100644 index 000000000..c66fa31d2 --- /dev/null +++ b/examples/turtlesim_rs/src/turtle_frame.rs @@ -0,0 +1,379 @@ +use crate::turtle::Turtle; +use core::time; +use eframe::egui::{self, ColorImage, TextureOptions}; +use eframe::egui::{Image, Ui, Vec2}; +use std::collections::BTreeMap; +use std::f32::consts::FRAC_PI_2; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::Arc; +use std::{env, thread}; +use tiny_skia::{Color, Pixmap}; + +pub const FRAME_WIDTH: u32 = 500; +pub const FRAME_HEIGHT: u32 = 500; + +pub const TURTLE_IMG_WIDTH: f32 = 45.0; +pub const TURTLE_IMG_HEIGHT: f32 = 45.0; + +const BACKGROUND_R: u8 = 69; +const BACKGROUND_G: u8 = 86; +const BACKGROUND_B: u8 = 255; +const BACKGROUND_ALPHA: u8 = 255; + +pub const UPDATE_INTERVAL_MS: u64 = 16; + +enum ServiceMsg { + Clear, + Reset, + Kill(String), + Spawn(f32, f32, f32, String, Sender), +} + +#[allow(unused)] +pub struct TurtleFrame<'a> { + ctx: egui::Context, + image_handle: egui::TextureHandle, + turtle_images: Vec>, + path_image: Pixmap, + + turtles: BTreeMap>, + id_count: u32, + frame_count: u64, + + meter: f32, + width_in_meters: f32, + height_in_meters: f32, + + context: rclrs::Context, + nh: Arc, + last_turtle_update: rclrs::Time, + + bg_r_param: rclrs::OptionalParameter, + bg_g_param: rclrs::OptionalParameter, + bg_b_param: rclrs::OptionalParameter, + + srv_rx: Receiver, + + clear_srv: Arc>, + kill_srv: Arc>, + reset_srv: Arc>, + spawn_srv: Arc>, +} + +impl<'a> TurtleFrame<'a> { + pub fn new(ctx: egui::Context) -> Self { + let mut turtle_images = vec![]; + load_turtle_images(&mut turtle_images); + + let turtles = BTreeMap::new(); + let frame_count = 0; + let id_count = 0; + + let meter = TURTLE_IMG_HEIGHT; + let width_in_meters = (FRAME_WIDTH as f32 - 1.0) / meter; + let height_in_meters = (FRAME_HEIGHT as f32 - 1.0) / meter; + let context = rclrs::Context::new(env::args()).unwrap(); + + let nh = rclrs::create_node(&context, "turtlesim_rs").unwrap(); + println!("Starting turtlesim_rs with node name {}", nh.name()); + + let last_turtle_update = nh.get_clock().now(); + + let bg_r_param = nh + .declare_parameter("background_r") + .default(BACKGROUND_R as i64) + .optional() + .unwrap(); + + let bg_g_param = nh + .declare_parameter("background_g") + .default(BACKGROUND_G as i64) + .optional() + .unwrap(); + + let bg_b_param = nh + .declare_parameter("background_b") + .default(BACKGROUND_B as i64) + .optional() + .unwrap(); + + let bg_r = bg_r_param.get().unwrap() as u8; + let bg_g = bg_g_param.get().unwrap() as u8; + let bg_b = bg_b_param.get().unwrap() as u8; + + let mut path_image = Pixmap::new(FRAME_WIDTH, FRAME_HEIGHT).unwrap(); + path_image.fill(Color::from_rgba8(bg_r, bg_g, bg_b, BACKGROUND_ALPHA)); + + let color_image = ColorImage::from_rgba_unmultiplied( + [FRAME_WIDTH as usize, FRAME_HEIGHT as usize], + path_image.data(), + ); + + let image_handle = ctx.load_texture("background", color_image, Default::default()); + + let (srv_tx, srv_rx) = channel(); + + let clear_srv_tx = srv_tx.clone(); + let clear_srv = nh + .create_service("clear", move |_, _| { + clear_srv_tx.send(ServiceMsg::Clear).unwrap(); + std_srvs::srv::Empty_Response::default() + }) + .unwrap(); + + let kill_srv_tx = srv_tx.clone(); + let kill_srv = nh + .create_service( + "kill", + move |_, req: turtlesim_rs_msgs::srv::Kill_Request| { + let turtle_name = req.name; + kill_srv_tx.send(ServiceMsg::Kill(turtle_name)).unwrap(); + turtlesim_rs_msgs::srv::Kill_Response::default() + }, + ) + .unwrap(); + + let reset_srv_tx = srv_tx.clone(); + let reset_srv = nh + .create_service("reset", move |_, _| { + reset_srv_tx.send(ServiceMsg::Reset).unwrap(); + std_srvs::srv::Empty_Response::default() + }) + .unwrap(); + + let spawn_srv_tx = srv_tx.clone(); + let spawn_srv = nh + .create_service( + "spawn", + move |_, req: turtlesim_rs_msgs::srv::Spawn_Request| { + let (name_tx, name_rx) = channel(); + let (x, y, theta, turtle_name) = (req.x, req.y, req.theta, req.name); + + spawn_srv_tx + .send(ServiceMsg::Spawn(x, y, theta, turtle_name, name_tx)) + .unwrap(); + let turtle_realname = name_rx.recv().unwrap(); + + turtlesim_rs_msgs::srv::Spawn_Response { + name: turtle_realname, + } + }, + ) + .unwrap(); + + let nh_clone = Arc::clone(&nh); + thread::spawn(move || loop { + std::thread::sleep(time::Duration::from_millis(UPDATE_INTERVAL_MS / 2)); + let _v = rclrs::spin_once(nh_clone.clone(), Some(time::Duration::ZERO)); + }); + + TurtleFrame { + ctx, + image_handle, + turtle_images, + path_image, + + turtles, + id_count, + frame_count, + + context, + nh, + last_turtle_update, + + meter, + width_in_meters, + height_in_meters, + + bg_r_param, + bg_g_param, + bg_b_param, + + srv_rx, + + clear_srv, + kill_srv, + reset_srv, + spawn_srv, + } + } + + pub fn get_frame_center(&self) -> (f32, f32) { + (self.width_in_meters / 2.0, self.height_in_meters / 2.0) + } + pub fn spawn(&mut self, name: &str, x: f32, y: f32, angle: f32) -> String { + let rand_usize = rand::random::() % self.turtle_images.len(); + let turtle_img = self.turtle_images[rand_usize].clone(); + self.spawn_internal(name, x, y, angle, turtle_img) + } + + fn spawn_internal( + &mut self, + name: &str, + x: f32, + y: f32, + angle: f32, + image: egui::Image<'a>, + ) -> String { + let mut real_name = name.to_owned(); + if name.is_empty() { + self.id_count += 1; + let mut new_name = format!("turtle{}", self.id_count); + + while self.has_turtle(&new_name) { + self.id_count += 1; + new_name = format!("turtle{}", self.id_count); + } + real_name = new_name; + } else if self.has_turtle(name) { + return String::new(); + } + + let turtle = Turtle::new( + self.nh.clone(), + &real_name.clone(), + image, + egui::Pos2::new(x, self.height_in_meters - y), + angle, + ); + self.turtles.insert(real_name.clone(), turtle); + + self.ctx.request_repaint(); + println!( + "Spawning turtle [{}] at x=[{}], y=[{}], theta=[{}]", + real_name, x, y, angle + ); + + real_name + } + + pub fn has_turtle(&self, name: &str) -> bool { + self.turtles.contains_key(name) + } + + pub fn update(&mut self, ui: &mut Ui) { + self.image_handle + .set(self.get_color_image(), TextureOptions::default()); + ui.image((self.image_handle.id(), self.image_handle.size_vec2())); + for turtle in self.turtles.values() { + turtle.paint(ui); + } + } + + pub fn update_turtles(&mut self) { + let mut modified = false; + + for turtle in self.turtles.values_mut() { + modified |= turtle.update( + UPDATE_INTERVAL_MS as f64 * 0.001, + &mut self.path_image, + self.width_in_meters, + self.height_in_meters, + ); + } + + if modified { + self.ctx.request_repaint(); + } + + self.frame_count += 1; + } + + pub fn handle_service_requests(&mut self) { + let service_requests = self.srv_rx.try_iter().collect::>(); + + if service_requests.is_empty() { + return; + } + + for srv_req in service_requests { + match srv_req { + ServiceMsg::Clear => self.clear_callback(), + ServiceMsg::Reset => self.reset_callback(), + ServiceMsg::Kill(turtle_name) => self.kill_callback(turtle_name), + ServiceMsg::Spawn(x, y, theta, turtle_name, name_tx) => { + self.spawn_callback(x, y, theta, turtle_name, name_tx) + } + } + } + + self.ctx.request_repaint(); + } + + fn get_color_image(&self) -> ColorImage { + ColorImage::from_rgba_unmultiplied( + [FRAME_WIDTH as usize, FRAME_HEIGHT as usize], + self.path_image.data(), + ) + } + + fn clear_callback(&mut self) { + let bg_r = self.bg_r_param.get().unwrap() as u8; + let bg_g = self.bg_g_param.get().unwrap() as u8; + let bg_b = self.bg_b_param.get().unwrap() as u8; + + self.path_image + .fill(Color::from_rgba8(bg_r, bg_g, bg_b, BACKGROUND_ALPHA)) + } + + fn reset_callback(&mut self) { + self.turtles.clear(); + self.id_count = 0; + self.spawn( + "", + self.width_in_meters / 2.0, + self.height_in_meters / 2.0, + 0.0, + ); + self.clear_callback(); + } + + fn kill_callback(&mut self, turtle_name: String) { + let has_turtle = self.has_turtle(&turtle_name); + if has_turtle { + self.turtles.remove(&turtle_name); + } else { + println!("Tried to kill turtle {}, which does not exist", turtle_name); + } + } + + fn spawn_callback( + &mut self, + x: f32, + y: f32, + theta: f32, + turtle_name: String, + name_tx: Sender, + ) { + let turtle_realname = self.spawn(&turtle_name, x, y, theta); + name_tx.send(turtle_realname).unwrap(); + } +} + +fn load_turtle_images(turtle_images: &mut Vec>) { + let turtles = vec![ + egui::include_image!("../images/box-turtle.png"), + egui::include_image!("../images/robot-turtle.png"), + egui::include_image!("../images/sea-turtle.png"), + egui::include_image!("../images/diamondback.png"), + egui::include_image!("../images/electric.png"), + egui::include_image!("../images/fuerte.png"), + egui::include_image!("../images/groovy.png"), + egui::include_image!("../images/hydro.svg"), + egui::include_image!("../images/indigo.svg"), + egui::include_image!("../images/jade.png"), + egui::include_image!("../images/kinetic.png"), + egui::include_image!("../images/lunar.png"), + egui::include_image!("../images/melodic.png"), + ]; + + turtle_images.reserve(turtles.len()); + for img in turtles { + let image = egui::Image::new(img); + turtle_images.push( + image + .max_size(Vec2::new(TURTLE_IMG_WIDTH, TURTLE_IMG_HEIGHT)) + .rotate(FRAC_PI_2, Vec2::splat(0.5)), + ); + } +} diff --git a/examples/turtlesim_rs/src/turtlesim.rs b/examples/turtlesim_rs/src/turtlesim.rs new file mode 100644 index 000000000..8d0c1c4ed --- /dev/null +++ b/examples/turtlesim_rs/src/turtlesim.rs @@ -0,0 +1,65 @@ +use core::time; +use eframe::egui::{self, CentralPanel, Frame, ViewportBuilder}; +use std::sync::{Arc, Mutex}; +use std::thread; + +use egui_extras::install_image_loaders; +use turtlesim_rs::turtle_frame::{TurtleFrame, FRAME_HEIGHT, FRAME_WIDTH, UPDATE_INTERVAL_MS}; + +fn main() { + let viewport = ViewportBuilder::default() + .with_resizable(false) + .with_inner_size((FRAME_WIDTH as f32, FRAME_HEIGHT as f32)); + + let native_options = eframe::NativeOptions { + viewport, + ..Default::default() + }; + + let _ = eframe::run_native( + "TurtleSim_rs", + native_options, + Box::new(|cc| { + install_image_loaders(&cc.egui_ctx); + Ok(Box::new(MyEguiApp::new(cc))) + }), + ); +} + +struct MyEguiApp { + turtle_frame: Arc>>, +} + +impl MyEguiApp { + fn new(cc: &eframe::CreationContext<'_>) -> Self { + let mut turtle_frame = TurtleFrame::new(cc.egui_ctx.clone()); + + let (x, y) = turtle_frame.get_frame_center(); + let theta = 0.0; + let turtle_name = ""; + turtle_frame.spawn(turtle_name, x, y, theta); + + let turtle_frame = Arc::new(Mutex::new(turtle_frame)); + + let turtle_frame_clone = Arc::clone(&turtle_frame); + thread::spawn(move || loop { + std::thread::sleep(time::Duration::from_millis(UPDATE_INTERVAL_MS)); + let mut frame = turtle_frame_clone.lock().unwrap(); + frame.update_turtles(); + frame.handle_service_requests(); + }); + + MyEguiApp { turtle_frame } + } +} + +impl eframe::App for MyEguiApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + CentralPanel::default() + .frame(Frame::default()) + .show(ctx, |ui| { + let mut frame = self.turtle_frame.lock().unwrap(); + frame.update(ui) + }); + } +} diff --git a/examples/turtlesim_rs/tutorials/turtle_teleop_key.rs b/examples/turtlesim_rs/tutorials/turtle_teleop_key.rs new file mode 100644 index 000000000..4b6502ce1 --- /dev/null +++ b/examples/turtlesim_rs/tutorials/turtle_teleop_key.rs @@ -0,0 +1,97 @@ +use rclrs::{Node, Publisher}; +use std::env; +use std::io; +use std::sync::Arc; +use termion::event::Key; +use termion::input::TermRead; +use termion::raw::IntoRawMode; + +struct TeleopTurtle { + _nh: Arc, + linear: f64, + angular: f64, + l_scale: f64, + a_scale: f64, + twist_pub: Arc>, +} + +impl TeleopTurtle { + pub fn new(context: &rclrs::Context) -> Self { + let _nh = rclrs::create_node(context, "teleop_turtle").unwrap(); + + let l_scale_param = _nh + .declare_parameter("scale_linear") + .default(2.0) + .optional() + .unwrap(); + + let a_scale_param = _nh + .declare_parameter("scale_angular") + .default(2.0) + .optional() + .unwrap(); + + let l_scale = l_scale_param.get().unwrap(); + let a_scale = a_scale_param.get().unwrap(); + + let twist_pub = _nh + .create_publisher("/turtle1/cmd_vel", rclrs::QOS_PROFILE_DEFAULT) + .unwrap(); + + Self { + _nh, + linear: 0.0, + angular: 0.0, + l_scale, + a_scale, + twist_pub, + } + } + + pub fn key_loop(&mut self) { + println!("Reading from keyboard"); + println!("---------------------------"); + println!("Use arrow keys to move the turtle."); + println!("'q' to quit."); + + let _stdout = io::stdout().into_raw_mode().unwrap(); + let stdin = io::stdin(); + for key in stdin.keys() { + self.linear = 0.0; + self.angular = 0.0; + + match key.unwrap() { + Key::Left => { + self.angular = 1.0; + } + Key::Right => { + self.angular = -1.0; + } + Key::Up => { + self.linear = 1.0; + } + Key::Down => { + self.linear = -1.0; + } + Key::Char('q') | Key::Ctrl('c') => { + break; + } + _ => {} + } + let mut twist_msg = geometry_msgs::msg::Twist::default(); + twist_msg.angular.z = self.angular * self.a_scale; + twist_msg.linear.x = self.linear * self.l_scale; + self.twist_pub.publish(twist_msg).unwrap(); + } + } +} + +fn main() -> Result<(), rclrs::RclrsError> { + let context = rclrs::Context::new(env::args()).unwrap(); + + let mut teleop_turtle = TeleopTurtle::new(&context); + + teleop_turtle.key_loop(); + + Ok(()) +} From 9c6fe3d5da35505594ec27ddb0cc25c6c1ec27a5 Mon Sep 17 00:00:00 2001 From: abdelrahman Date: Tue, 10 Dec 2024 21:48:39 +0200 Subject: [PATCH 3/7] Fix typo in Cargo.toml edition configuration --- examples/turtlesim_rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/turtlesim_rs/Cargo.toml b/examples/turtlesim_rs/Cargo.toml index e931bc9ff..ba162b09b 100644 --- a/examples/turtlesim_rs/Cargo.toml +++ b/examples/turtlesim_rs/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "turtlesim_rs" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] eframe = "0.29.1" From 6180c0a9154ed7860ffbc72476c43c92c2e1a593 Mon Sep 17 00:00:00 2001 From: abdelrahman Date: Tue, 10 Dec 2024 23:05:26 +0200 Subject: [PATCH 4/7] Fix compatibility with older Rust version (CI issue) - Downgraded dependencies to work with rust lower than 1.75. --- examples/turtlesim_rs/Cargo.toml | 4 ++-- examples/turtlesim_rs/src/turtlesim.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/turtlesim_rs/Cargo.toml b/examples/turtlesim_rs/Cargo.toml index ba162b09b..3e9ebbed4 100644 --- a/examples/turtlesim_rs/Cargo.toml +++ b/examples/turtlesim_rs/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -eframe = "0.29.1" -egui_extras = { version = "0.29.1", features = ["all_loaders"]} +eframe = "0.27.0" +egui_extras = { version = "0.27.0", features = ["all_loaders"]} tiny-skia = "0.11.4" rand = "0.8.5" termion = "1.5" diff --git a/examples/turtlesim_rs/src/turtlesim.rs b/examples/turtlesim_rs/src/turtlesim.rs index 8d0c1c4ed..fb18edcc2 100644 --- a/examples/turtlesim_rs/src/turtlesim.rs +++ b/examples/turtlesim_rs/src/turtlesim.rs @@ -21,7 +21,7 @@ fn main() { native_options, Box::new(|cc| { install_image_loaders(&cc.egui_ctx); - Ok(Box::new(MyEguiApp::new(cc))) + Box::new(MyEguiApp::new(cc)) }), ); } From 91aa1a3299c67b9ef83bccc0ca3a86eb027727f4 Mon Sep 17 00:00:00 2001 From: abdelrahman Date: Wed, 11 Dec 2024 16:06:54 +0200 Subject: [PATCH 5/7] Add mimic tutorial --- examples/turtlesim_rs/Cargo.toml | 4 ++ examples/turtlesim_rs/tutorials/mimic.rs | 51 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100755 examples/turtlesim_rs/tutorials/mimic.rs diff --git a/examples/turtlesim_rs/Cargo.toml b/examples/turtlesim_rs/Cargo.toml index 3e9ebbed4..5c54fa73e 100644 --- a/examples/turtlesim_rs/Cargo.toml +++ b/examples/turtlesim_rs/Cargo.toml @@ -18,6 +18,10 @@ path = "src/turtlesim.rs" name = "turtle_teleop_key" path = "tutorials/turtle_teleop_key.rs" +[[bin]] +name = "mimic" +path = "tutorials/mimic.rs" + [dependencies.rclrs] version = "0.4" diff --git a/examples/turtlesim_rs/tutorials/mimic.rs b/examples/turtlesim_rs/tutorials/mimic.rs new file mode 100755 index 000000000..60486b556 --- /dev/null +++ b/examples/turtlesim_rs/tutorials/mimic.rs @@ -0,0 +1,51 @@ +use rclrs::{Context, Node, Publisher, Subscription, QOS_PROFILE_DEFAULT}; +use std::env; +use std::sync::Arc; + +#[allow(unused)] +struct Mimic { + output_nh: Arc, + input_nh: Arc, + twist_pub: Arc>, + pose_sub: Arc>, +} + +impl Mimic { + fn new() -> Self { + let context = Context::new(env::args()).unwrap(); + let output_nh = rclrs::create_node(&context, "output").unwrap(); + + let twist_pub = output_nh + .create_publisher("/output/cmd_vel", QOS_PROFILE_DEFAULT) + .unwrap(); + + let input_nh = rclrs::create_node(&context, "input").unwrap(); + + let twist_pub_clone = Arc::clone(&twist_pub); + let pose_sub = input_nh + .create_subscription( + "/input/pose", + QOS_PROFILE_DEFAULT, + move |pose_msg: turtlesim_rs_msgs::msg::Pose| { + let mut twist_msg = geometry_msgs::msg::Twist::default(); + twist_msg.linear.x = pose_msg.linear_velocity as f64; + twist_msg.angular.z = pose_msg.angular_velocity as f64; + twist_pub_clone.publish(twist_msg).unwrap(); + }, + ) + .unwrap(); + + Self { + output_nh, + input_nh, + twist_pub, + pose_sub, + } + } +} + +fn main() -> Result<(), rclrs::RclrsError> { + let mimic = Mimic::new(); + rclrs::spin(mimic.input_nh.clone())?; + Ok(()) +} From 46330610babca218540a51c3f2d6ee32698f1231 Mon Sep 17 00:00:00 2001 From: abdelrahman Date: Sun, 15 Dec 2024 19:36:47 +0200 Subject: [PATCH 6/7] Fix incorrect pixel color being published --- examples/turtlesim_rs/src/turtle.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/turtlesim_rs/src/turtle.rs b/examples/turtlesim_rs/src/turtle.rs index fe6c7d875..5e07ce9bf 100644 --- a/examples/turtlesim_rs/src/turtle.rs +++ b/examples/turtlesim_rs/src/turtle.rs @@ -149,8 +149,8 @@ impl<'a> Turtle<'a> { let mut paint = Paint::default(); paint.set_color_rgba8( DEFAULT_PEN_R, - DEFAULT_PEN_B, DEFAULT_PEN_G, + DEFAULT_PEN_B, DEFAULT_PEN_ALPHA, ); paint.anti_alias = true; @@ -255,7 +255,7 @@ impl<'a> Turtle<'a> { let pixel_color = path_image.pixel( (self.pos.x * self.meter) as u32, - ((canvas_height - self.pos.y) * self.meter) as u32, + (self.pos.y * self.meter) as u32, ); if let Some(color) = pixel_color { From 19687bc0535d5f13f6e70a6f84aca07847b30389 Mon Sep 17 00:00:00 2001 From: abdelrahman Date: Mon, 16 Dec 2024 12:19:17 +0200 Subject: [PATCH 7/7] Refactor threads to use Weak references for graceful termination - Replaced strong Arc clones with Weak references in thread spawns - Ensured threads terminate when the associated Arc is dropped --- examples/turtlesim_rs/src/turtle_frame.rs | 8 ++++++-- examples/turtlesim_rs/src/turtlesim.rs | 13 +++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/examples/turtlesim_rs/src/turtle_frame.rs b/examples/turtlesim_rs/src/turtle_frame.rs index c66fa31d2..054d7e7ea 100644 --- a/examples/turtlesim_rs/src/turtle_frame.rs +++ b/examples/turtlesim_rs/src/turtle_frame.rs @@ -161,10 +161,14 @@ impl<'a> TurtleFrame<'a> { ) .unwrap(); - let nh_clone = Arc::clone(&nh); + let nh_weak = Arc::downgrade(&nh); thread::spawn(move || loop { std::thread::sleep(time::Duration::from_millis(UPDATE_INTERVAL_MS / 2)); - let _v = rclrs::spin_once(nh_clone.clone(), Some(time::Duration::ZERO)); + if let Some(nh_clone) = nh_weak.upgrade() { + let _ = rclrs::spin_once(nh_clone, Some(time::Duration::ZERO)); + } else { + break; + } }); TurtleFrame { diff --git a/examples/turtlesim_rs/src/turtlesim.rs b/examples/turtlesim_rs/src/turtlesim.rs index fb18edcc2..a6aadeac4 100644 --- a/examples/turtlesim_rs/src/turtlesim.rs +++ b/examples/turtlesim_rs/src/turtlesim.rs @@ -41,12 +41,17 @@ impl MyEguiApp { let turtle_frame = Arc::new(Mutex::new(turtle_frame)); - let turtle_frame_clone = Arc::clone(&turtle_frame); + let turtle_frame_weak = Arc::downgrade(&turtle_frame); + thread::spawn(move || loop { std::thread::sleep(time::Duration::from_millis(UPDATE_INTERVAL_MS)); - let mut frame = turtle_frame_clone.lock().unwrap(); - frame.update_turtles(); - frame.handle_service_requests(); + if let Some(turtle_frame_clone) = turtle_frame_weak.upgrade() { + let mut frame = turtle_frame_clone.lock().unwrap(); + frame.update_turtles(); + frame.handle_service_requests(); + } else { + break; + } }); MyEguiApp { turtle_frame }