351 Commits

Author SHA1 Message Date
Sem van der Hoeven
dcf45bff26 add pdf docs 2023-07-10 15:56:21 +02:00
Sem van der Hoeven
f0abeaacf7 add correct land service 2023-07-10 15:18:24 +02:00
Sem van der Hoeven
195ddc6ab9 add optical flow enabled and emergency force stop 2023-07-10 15:13:57 +02:00
Sem van der Hoeven
f0cbebf874 add optical flow 2023-07-10 15:00:51 +02:00
Sem van der Hoeven
378d63a026 update readme with good values 2023-07-10 13:47:58 +02:00
Sem van der Hoeven
3030421efc add new sections to github 2023-07-10 13:08:14 +02:00
Sem van der Hoeven
743c0ce540 add readme 2023-07-10 10:41:52 +02:00
Sem van der Hoeven
9fee669f44 add relative links 2023-07-10 09:59:26 +02:00
Sem van der Hoeven
c9690c376f change readme 2023-07-10 09:53:17 +02:00
Sem van der Hoeven
303a3f0dbd Report is handed in 2023-06-17 00:04:53 +02:00
Sem van der Hoeven
67df381251 logs 2023-06-14 09:53:59 +02:00
Sem van der Hoeven
6ddd24cfd5 try print error 2023-06-14 09:52:28 +02:00
Sem van der Hoeven
49d62f894a change move check 2023-06-14 09:47:00 +02:00
Sem van der Hoeven
dc96e3a86c change move check 2023-06-14 09:46:30 +02:00
Sem van der Hoeven
cc14b4a3eb change arm check 2023-06-14 09:45:21 +02:00
Sem van der Hoeven
6d74605582 clean up apilistener 2023-06-13 22:02:30 +02:00
SemvdH
9a81018c1d Merge pull request #12 from SemvdH/api
Merge API branch into main
2023-06-13 21:54:43 +02:00
Sem van der Hoeven
93ac0ad866 update code for report 2023-06-13 21:53:44 +02:00
Sem van der Hoeven
953c7d6884 remove select 2023-06-13 20:41:17 +02:00
Sem van der Hoeven
f671fa0535 add land test 2023-06-13 11:17:50 +02:00
Sem van der Hoeven
30fe217df0 substring pos and vel values 2023-06-12 17:16:33 +02:00
Sem van der Hoeven
95b19be4be add sleep to back 2023-06-12 16:57:59 +02:00
Sem van der Hoeven
3d51438fa1 change values 2023-06-12 16:57:01 +02:00
Sem van der Hoeven
eece400e32 increase back time 2023-06-12 16:51:33 +02:00
Sem van der Hoeven
85188015e2 add still test 2023-06-12 16:49:15 +02:00
Sem van der Hoeven
0eef99b793 fix checking msg for back test 2023-06-12 16:47:34 +02:00
Sem van der Hoeven
e55a2cf53a add left and back tests 2023-06-12 16:46:07 +02:00
Sem van der Hoeven
b377155baa fix test number 2023-06-12 16:42:47 +02:00
Sem van der Hoeven
887d36a229 extra stop 2023-06-12 16:42:34 +02:00
Sem van der Hoeven
ac3c6860ce add right test 2023-06-12 16:40:38 +02:00
Sem van der Hoeven
f3e84d5382 check only front 2023-06-12 16:38:17 +02:00
Sem van der Hoeven
6074fb52c1 the drone should always move away from objects, not only if it's moving in that direction 2023-06-12 16:27:12 +02:00
Sem van der Hoeven
3629424089 check all directions 2023-06-12 16:13:16 +02:00
Sem van der Hoeven
6dcaaef638 add switching lidar direction 2023-06-12 16:08:36 +02:00
Sem van der Hoeven
8034702db0 change msg 2023-06-12 16:05:59 +02:00
Sem van der Hoeven
9cf5eae78f whoops 2023-06-12 15:53:55 +02:00
Sem van der Hoeven
8eea606db3 add test for back and more for front 2023-06-12 15:52:59 +02:00
Sem van der Hoeven
5b7f239f8f remove log 2023-06-12 15:44:49 +02:00
Sem van der Hoeven
62c7e67e82 change node name 2023-06-12 15:43:49 +02:00
Sem van der Hoeven
d451b78e96 extra log msg 2023-06-12 15:36:37 +02:00
Sem van der Hoeven
21683cb92c fix text 2023-06-12 15:34:03 +02:00
Sem van der Hoeven
f23e56a9a0 log 2023-06-12 15:32:38 +02:00
Sem van der Hoeven
8d2bb7446f update test text 2023-06-12 15:32:17 +02:00
Sem van der Hoeven
f56bd7f735 ; 2023-06-12 15:18:04 +02:00
Sem van der Hoeven
8cb03ec4e7 fix collision prevention 2023-06-12 15:16:09 +02:00
Sem van der Hoeven
b7cba3e2ec collision log msgs 2023-06-12 12:03:59 +02:00
Sem van der Hoeven
d0d27552cf add launch_testing 2023-06-12 12:00:22 +02:00
Sem van der Hoeven
d986b96faf add type of proc_output 2023-06-12 11:56:36 +02:00
Sem van der Hoeven
5c46b857e0 add type of proc_output 2023-06-12 11:55:26 +02:00
Sem van der Hoeven
3d18a14dcf fix 2023-06-12 11:51:11 +02:00
Sem van der Hoeven
100c19db79 add validate output? 2023-06-12 11:49:52 +02:00
Sem van der Hoeven
2155e4a8e0 move assertwaitfor statement 2023-06-12 11:43:06 +02:00
Sem van der Hoeven
1f3a2754ea move position callback 2023-06-12 11:39:45 +02:00
Sem van der Hoeven
7cee4c1543 move move away test to different file 2023-06-12 11:36:54 +02:00
Sem van der Hoeven
e706a38ea4 add test for moving away with lidar 2023-06-12 11:31:22 +02:00
Sem van der Hoeven
bcc14c9e0d make node publish on status topic 2023-06-09 19:40:38 +02:00
Sem van der Hoeven
8cc120c10f add qos profile 2023-06-09 19:37:14 +02:00
Sem van der Hoeven
2f9d45c79d status 2023-06-09 19:32:01 +02:00
Sem van der Hoeven
2aff2bcf12 logs 2023-06-09 19:30:15 +02:00
Sem van der Hoeven
f0bb159c4b add wait on nodes 2023-06-09 19:27:57 +02:00
Sem van der Hoeven
e139d111f6 add heartbeat node 2023-06-09 19:25:27 +02:00
Sem van der Hoeven
89473e4612 add qos to publisher 2023-06-09 19:24:37 +02:00
Sem van der Hoeven
87608ff637 fix logging 2023-06-09 18:28:08 +02:00
Sem van der Hoeven
da0a35f98f fix logging 2023-06-09 18:26:04 +02:00
Sem van der Hoeven
bf48ee04c3 fix typos 2023-06-09 18:23:08 +02:00
Sem van der Hoeven
473de62451 fix test name 2023-06-09 18:19:48 +02:00
Sem van der Hoeven
defd91b473 add land service 2023-06-09 18:13:02 +02:00
Sem van der Hoeven
a3fb87ead3 add API battery test 2023-06-09 17:54:16 +02:00
Sem van der Hoeven
cc78321421 add landing functionality with height meter 2023-06-08 21:44:27 +02:00
Sem van der Hoeven
4daf79cdc0 add land service 2023-06-08 21:21:12 +02:00
Sem van der Hoeven
ca2049b0db add height to drone status 2023-06-08 20:43:19 +02:00
Sem van der Hoeven
d1e2b5960e fix? 2023-06-08 19:48:51 +02:00
Sem van der Hoeven
fcf9f15fb3 fix? 2023-06-08 19:44:09 +02:00
Sem van der Hoeven
17bfcc323c fix? 2023-06-08 19:42:03 +02:00
Sem van der Hoeven
e3babad383 temp no timeout 2023-06-08 13:20:56 +02:00
Sem van der Hoeven
e31b0346bc boolean to set_timeout 2023-06-08 13:19:33 +02:00
Sem van der Hoeven
032dc1dea9 increase timeout time 2023-06-08 13:17:53 +02:00
Sem van der Hoeven
6b21465a35 timeout in status receive 2023-06-08 13:16:57 +02:00
Sem van der Hoeven
dd38be123f log type of error 2023-06-08 13:10:23 +02:00
Sem van der Hoeven
4138673893 add checking timeout 2023-06-08 13:04:09 +02:00
Sem van der Hoeven
ba0fdc7863 add timeout 2023-06-08 12:36:59 +02:00
Sem van der Hoeven
a7c22aa362 add camera error 2023-06-08 12:24:17 +02:00
Sem van der Hoeven
545119633c try 2023-06-08 12:18:56 +02:00
Sem van der Hoeven
74353bb128 try 2023-06-08 12:17:39 +02:00
Sem van der Hoeven
f50e2d0d57 add continuous checking 2023-06-08 12:15:48 +02:00
Sem van der Hoeven
b22d8228aa add log 2023-06-08 12:08:43 +02:00
Sem van der Hoeven
9826afbd49 add checking for connection 2023-06-08 11:50:35 +02:00
Sem van der Hoeven
07f2320e91 extra connection loss alert 2023-06-08 11:42:21 +02:00
Sem van der Hoeven
abbd8717a8 change turn_left_right to yaw 2023-06-08 11:18:46 +02:00
Sem van der Hoeven
6569261ab2 add label 2023-06-08 11:14:26 +02:00
Sem van der Hoeven
d246d0b5f9 add control mode select 2023-06-08 11:13:32 +02:00
Sem van der Hoeven
8a85857155 add camera timer to check if it should exit 2023-06-08 11:09:12 +02:00
Sem van der Hoeven
e48efb93e1 remove extra parse 2023-06-08 11:05:53 +02:00
Sem van der Hoeven
19787aa2ca pass message.data 2023-06-08 11:04:07 +02:00
Sem van der Hoeven
35331e3714 pass message.data 2023-06-08 11:02:59 +02:00
Sem van der Hoeven
bc55fbc757 print message 2023-06-08 11:01:59 +02:00
Sem van der Hoeven
94eae1d8a9 add log error 2023-06-08 11:00:33 +02:00
Sem van der Hoeven
9a207dabcc remove checking for strings 2023-06-08 00:09:35 +02:00
Sem van der Hoeven
328f439270 change strings 2023-06-08 00:06:09 +02:00
Sem van der Hoeven
970d093a31 add spin timeout 2023-06-08 00:01:02 +02:00
Sem van der Hoeven
5707bb74c7 try sending only 1 msg 2023-06-07 23:59:30 +02:00
Sem van der Hoeven
39929b6d58 fix other bug 2023-06-07 23:57:34 +02:00
Sem van der Hoeven
4686a90e0a fix bug 2023-06-07 23:55:34 +02:00
Sem van der Hoeven
9dbd747295 logs 2023-06-07 23:53:44 +02:00
Sem van der Hoeven
a76b99789c logs 2023-06-07 23:53:01 +02:00
Sem van der Hoeven
f63814c6f2 add sleep 2023-06-07 23:52:10 +02:00
Sem van der Hoeven
e55da2375e logs 2023-06-07 23:50:54 +02:00
Sem van der Hoeven
4a82a3d602 logs 2023-06-07 23:49:32 +02:00
Sem van der Hoeven
b3c2505c0a add heartbeat node 2023-06-07 23:43:54 +02:00
Sem van der Hoeven
7ca1452908 add heartbeat node 2023-06-07 23:42:59 +02:00
Sem van der Hoeven
4ec2c8c79b add px4controller node 2023-06-07 23:41:27 +02:00
Sem van der Hoeven
56a6c579af change yaw to angle 2023-06-07 23:40:31 +02:00
Sem van der Hoeven
7dfa1e77df function arguments 2023-06-07 23:38:42 +02:00
Sem van der Hoeven
d0b8f3dafa fix name 2023-06-07 23:36:58 +02:00
Sem van der Hoeven
a740fa7c45 add positiionchanger tests 2023-06-07 23:19:26 +02:00
Sem van der Hoeven
f846d31ac2 fix name 2023-06-07 18:39:12 +02:00
Sem van der Hoeven
0fa5751c29 typo 2023-06-07 18:38:28 +02:00
Sem van der Hoeven
e141326733 add heartbeat control mode test 2023-06-07 18:37:36 +02:00
Sem van der Hoeven
7e8da56382 fix service name 2023-06-07 18:06:16 +02:00
Sem van der Hoeven
49bb33b275 fix name 2023-06-07 18:05:03 +02:00
Sem van der Hoeven
e673846113 add camera unit test 2023-06-07 18:01:58 +02:00
Sem van der Hoeven
35ee9611e4 remove lint dependencies for testing 2023-06-07 17:36:18 +02:00
Sem van der Hoeven
47b4ed15f8 remove lint and license test dependencies 2023-06-07 17:34:15 +02:00
Sem van der Hoeven
7966bdec9e add called attitude service 2023-06-07 17:31:54 +02:00
Sem van der Hoeven
745b5bc4e9 float values 2023-06-07 17:30:03 +02:00
Sem van der Hoeven
4c61e4e1e2 add failsafe node to launchdescription 2023-06-07 17:28:37 +02:00
Sem van der Hoeven
cc9f4e7f5c show which service is waiting 2023-06-07 17:27:31 +02:00
Sem van der Hoeven
a8facbf521 launch failsafe node 2023-06-07 17:26:22 +02:00
Sem van der Hoeven
91d1762687 use launch_ros.actions node 2023-06-07 17:24:50 +02:00
Sem van der Hoeven
909504d305 typo 2023-06-07 17:21:31 +02:00
Sem van der Hoeven
ebf4264ae3 add tests for px4 failsafe 2023-06-07 17:16:20 +02:00
Sem van der Hoeven
9b05ac0fa0 add test file for px4controller 2023-06-07 16:25:58 +02:00
Sem van der Hoeven
a2ffd58069 add waiting for service call 2023-06-07 16:16:45 +02:00
Sem van der Hoeven
6f08d3ff32 remove test assert 2023-06-07 16:14:35 +02:00
Sem van der Hoeven
4bbbaa2183 add pytest 2023-06-07 16:07:58 +02:00
Sem van der Hoeven
3ab0304473 add failsafe unit test 2023-06-07 15:54:28 +02:00
Sem van der Hoeven
1535e9e480 remove br 2023-06-07 11:08:06 +02:00
Sem van der Hoeven
6f4a9cd7b2 big imgs 2023-06-07 11:07:13 +02:00
Sem van der Hoeven
355d31d97c move imgs 2023-06-07 11:06:44 +02:00
Sem van der Hoeven
8c6219965f move imgs 2023-06-07 11:06:06 +02:00
Sem van der Hoeven
c4451eede0 float right 2023-06-07 11:05:10 +02:00
Sem van der Hoeven
6550713780 br 2023-06-07 11:04:19 +02:00
Sem van der Hoeven
d6f9f15205 change img size 2023-06-07 11:03:33 +02:00
Sem van der Hoeven
d6020cf904 add images 2023-06-07 11:02:59 +02:00
Sem van der Hoeven
d3ac417f6f add close event 2023-06-07 10:56:58 +02:00
Sem van der Hoeven
3012e611cd fix websocket event listeners 2023-06-07 10:54:36 +02:00
Sem van der Hoeven
258e5cf289 ADD LOCAL CONNECT 2023-06-07 10:52:35 +02:00
Sem van der Hoeven
1c855540ef fix bat perc 2023-06-06 17:49:00 +02:00
Sem van der Hoeven
3dea3e6d5d fix bat perc 2023-06-06 17:46:27 +02:00
Sem van der Hoeven
60d2aeaa7f change qos for drone status 2023-06-06 17:30:15 +02:00
Sem van der Hoeven
d876a9ba24 add logging of publishing arm status 2023-06-06 17:24:05 +02:00
Sem van der Hoeven
ee79147c8c print drone status arm 2023-06-06 17:17:43 +02:00
Sem van der Hoeven
4998acc7d2 typo 2023-06-06 16:52:05 +02:00
Sem van der Hoeven
07e9357a1f add readydrone to cmakelists.txt 2023-06-06 16:46:02 +02:00
Sem van der Hoeven
b9270f7745 add ready drone to api listener 2023-06-06 16:42:22 +02:00
Sem van der Hoeven
41209ceab0 add setting drone ready 2023-06-06 16:38:07 +02:00
Sem van der Hoeven
231556683d Add ready drone request 2023-06-06 16:14:02 +02:00
Sem van der Hoeven
842e3e4534 add readydrone service 2023-06-06 15:58:36 +02:00
Sem van der Hoeven
e808e93cf3 try closing camera 2023-06-05 21:59:19 +02:00
Sem van der Hoeven
edde183c7b change all spin_until_complte into callbacks 2023-06-05 21:52:55 +02:00
Sem van der Hoeven
5ca202e8c0 log enabling failsafe 2023-06-05 21:46:34 +02:00
Sem van der Hoeven
902adb42f3 made failsafe enable when battery level too low 2023-06-05 21:41:14 +02:00
Sem van der Hoeven
bcd4f891d2 made camera controller destroy 2023-06-05 21:33:48 +02:00
Sem van der Hoeven
8b2dfd27e2 make camera controller exit on many errors 2023-06-05 21:23:05 +02:00
Sem van der Hoeven
096cee40be change floats to ints 2023-06-05 21:21:03 +02:00
Sem van der Hoeven
56b8f147c4 convert pos and vel data to floats 2023-06-05 21:17:23 +02:00
Sem van der Hoeven
ef0c6222ea remove spamming log 2023-06-05 21:14:14 +02:00
Sem van der Hoeven
a725c593ff try except with parsing status message 2023-06-05 21:12:08 +02:00
Sem van der Hoeven
49be78f16c logs 2023-06-05 21:03:43 +02:00
Sem van der Hoeven
0aff792254 the fuck 2023-06-05 20:58:37 +02:00
Sem van der Hoeven
b4ae183829 the fuck 2023-06-05 20:57:22 +02:00
Sem van der Hoeven
7669514ff6 change connected status to h2 2023-06-05 20:41:56 +02:00
Sem van der Hoeven
ab3a1725a3 fix checking for first lidar message 2023-06-05 20:38:11 +02:00
Sem van der Hoeven
45426bea1e only send failsafe msg once 2023-06-05 20:36:37 +02:00
Sem van der Hoeven
b7037a3e6f try fix lidar taking long to send messages 2023-06-05 17:51:09 +02:00
Sem van der Hoeven
cbf01d7875 better displaying info to user 2023-06-05 17:17:19 +02:00
Sem van der Hoeven
0548955969 fix error on error log 2023-06-05 17:14:44 +02:00
Sem van der Hoeven
a6a57b9689 connect logs 2023-06-05 17:13:10 +02:00
Sem van der Hoeven
582e65126e add longer waiting on LIDAR 2023-06-05 17:09:57 +02:00
Sem van der Hoeven
c967eea3f2 add waiting on first lidar message 2023-06-05 17:04:57 +02:00
Sem van der Hoeven
4916584e13 add showing of failsafe message in failsafe node 2023-06-05 16:57:13 +02:00
Sem van der Hoeven
561daf035c add positionchanger wait 10 seconds before checking lidar health 2023-06-05 16:50:46 +02:00
Sem van der Hoeven
d80b20e05a update status message definition 2023-06-05 16:10:23 +02:00
Sem van der Hoeven
b565832773 try resetting connection on loss 2023-06-05 16:06:19 +02:00
Sem van der Hoeven
2b36a9a383 comments 2023-06-05 15:00:18 +02:00
Sem van der Hoeven
273d979beb add remaining functionality and comments 2023-06-05 13:36:44 +02:00
Sem van der Hoeven
267e818d66 add velocity and position display 2023-06-05 12:43:01 +02:00
Sem van der Hoeven
508cbfe720 change battery info 2023-06-03 16:02:12 +02:00
Sem van der Hoeven
f4d57025a4 change width 2023-06-03 15:58:15 +02:00
Sem van der Hoeven
4684b7adb2 typo 2023-06-03 15:57:23 +02:00
Sem van der Hoeven
57f60fe44a convert canvas to img 2023-06-03 15:56:09 +02:00
Sem van der Hoeven
98efc65b86 convert canvas to img 2023-06-03 15:53:27 +02:00
Sem van der Hoeven
4ce6ff67da convert canvas to img 2023-06-03 15:52:36 +02:00
Sem van der Hoeven
402fcbab96 fix 2023-06-03 15:46:55 +02:00
Sem van der Hoeven
9a255bad12 add asyncio new loop 2023-06-03 15:45:22 +02:00
Sem van der Hoeven
ce65da2ed2 add asyncio new loop 2023-06-03 15:43:32 +02:00
Sem van der Hoeven
e0ab883144 add asyncio new loop 2023-06-03 15:42:14 +02:00
Sem van der Hoeven
f29509e16d fix asyncio 2023-06-03 15:40:19 +02:00
Sem van der Hoeven
38c9266ceb add websockets to camera controller 2023-06-03 15:38:08 +02:00
Sem van der Hoeven
2ac3f22ea7 change width 2023-06-03 15:02:00 +02:00
Sem van der Hoeven
5df44d0fb4 change width 2023-06-03 15:00:56 +02:00
Sem van der Hoeven
8db7fec055 change width 2023-06-03 14:59:29 +02:00
Sem van der Hoeven
0397c23a62 add error msg 2023-06-03 14:38:35 +02:00
Sem van der Hoeven
b2b8c2081f add error msg 2023-06-03 14:37:48 +02:00
Sem van der Hoeven
822f63aaa1 change width 2023-06-03 14:36:40 +02:00
Sem van der Hoeven
331d543300 change width 2023-06-03 14:35:40 +02:00
Sem van der Hoeven
fe086b2ef9 open socket on load 2023-06-03 14:23:15 +02:00
Sem van der Hoeven
36a262dcb5 open socket on load 2023-06-03 14:22:15 +02:00
Sem van der Hoeven
2c9b12cd8d try streaming with websockets 2023-06-03 14:19:03 +02:00
Sem van der Hoeven
8fca2086ac try streaming with websockets 2023-06-03 14:13:34 +02:00
Sem van der Hoeven
f30a51ca68 try changing resolution 2023-06-02 15:40:58 +02:00
Sem van der Hoeven
1a7efcfa23 change from asyncio to normal thread 2023-06-02 15:33:22 +02:00
Sem van der Hoeven
50eafe6720 fix overflow 2023-06-02 13:12:08 +02:00
Sem van der Hoeven
e2f4cde884 fix overflow 2023-06-02 13:10:21 +02:00
Sem van der Hoeven
ca3fdcf760 fix overflow 2023-06-02 13:09:52 +02:00
Sem van der Hoeven
49456cca65 fix overflow 2023-06-02 13:09:05 +02:00
Sem van der Hoeven
86ec261e81 fix overflow 2023-06-02 13:07:52 +02:00
Sem van der Hoeven
4182be2872 fix overflow 2023-06-02 13:07:24 +02:00
Sem van der Hoeven
140aced268 fix overflow 2023-06-02 13:06:47 +02:00
Sem van der Hoeven
0144fa9a51 fix overflow 2023-06-02 13:06:06 +02:00
Sem van der Hoeven
e4a5a3aeac fix overflow 2023-06-02 13:05:38 +02:00
Sem van der Hoeven
53ab71036f fix overflow 2023-06-02 13:04:41 +02:00
Sem van der Hoeven
1237bc4c78 take_picture 2023-06-02 13:03:47 +02:00
Sem van der Hoeven
1850a490f1 take_picture 2023-06-02 13:01:52 +02:00
Sem van der Hoeven
dd364da5be decode base64 image 2023-06-02 12:58:10 +02:00
Sem van der Hoeven
823a8ead89 log image 2023-06-02 12:57:04 +02:00
Sem van der Hoeven
c5fbefa497 convert base64 image 2023-06-02 12:55:55 +02:00
Sem van der Hoeven
7c0333a18f remove connect to video stream 2023-06-02 12:52:49 +02:00
Sem van der Hoeven
1e1b9c0e93 try with http post video 2023-06-02 12:47:11 +02:00
Sem van der Hoeven
f197f595c5 camera sleep every second 2023-06-02 12:22:31 +02:00
Sem van der Hoeven
99dbd03edf fix ws address 2023-06-02 12:14:00 +02:00
Sem van der Hoeven
8045c0c96e camera controler 2023-06-02 12:10:33 +02:00
Sem van der Hoeven
9cbd7f1be8 format cpu usage 2023-06-02 12:07:48 +02:00
Sem van der Hoeven
575950a945 format cpu usage 2023-06-02 12:06:38 +02:00
Sem van der Hoeven
79a9e2d853 display data from status 2023-06-02 12:05:29 +02:00
Sem van der Hoeven
cd653fc026 logs 2023-06-02 12:01:43 +02:00
Sem van der Hoeven
f76943b36c send messages async 2023-06-02 11:59:10 +02:00
Sem van der Hoeven
8ed216785c logging sse 2023-06-02 11:57:04 +02:00
Sem van der Hoeven
0a70283df0 log status data 2023-06-02 11:52:49 +02:00
Sem van der Hoeven
ae4d2a980d remove ensure future 2023-06-02 11:46:14 +02:00
Sem van der Hoeven
d9a4e87756 wait until websocket is active 2023-06-02 11:45:02 +02:00
Sem van der Hoeven
13e3ebc900 wait for event loop 2023-06-02 11:43:16 +02:00
Sem van der Hoeven
109827b897 fix camera coroutines not being awaited 2023-06-02 11:40:26 +02:00
Sem van der Hoeven
28b4309fbf Merge branch 'api' of github.com:SemvdH/5g_drone_ROS2 into api 2023-06-02 11:40:23 +02:00
Sem van der Hoeven
43b39f4002 fix camera coroutines not being awaited 2023-06-02 11:40:13 +02:00
Sem van der Hoeven
72123d0f10 scripts 2023-06-02 11:37:19 +02:00
Sem van der Hoeven
560d6ca866 change send video thread to use asyncio.run because webserver thread does not use asyncio 2023-06-02 11:22:28 +02:00
Sem van der Hoeven
baa0f0c35f add connecting to websocket on client side 2023-06-02 11:19:52 +02:00
Sem van der Hoeven
5031dd9aa8 add websocket to cameracontroller for video stream 2023-06-02 11:11:38 +02:00
Sem van der Hoeven
d1b51dd7f4 try 2023-05-31 22:50:50 +02:00
Sem van der Hoeven
49a6991bb2 try setting resolution 2023-05-31 22:45:01 +02:00
Sem van der Hoeven
e1c3864811 try to use same event loop 2023-05-31 22:07:42 +02:00
Sem van der Hoeven
ea5d9429d8 remove updating status through get request 2023-05-31 21:41:27 +02:00
Sem van der Hoeven
05abff6c67 publish video on same event loop as websocket 2023-05-31 21:35:55 +02:00
Sem van der Hoeven
3d5b593941 change to nodejs buffer and base64 2023-05-31 21:15:25 +02:00
Sem van der Hoeven
df8f391c9f add displaying video stream using sse 2023-05-31 21:03:40 +02:00
Sem van der Hoeven
bdcdb19d96 log 2023-05-31 20:35:42 +02:00
Sem van der Hoeven
5fdc4d6642 add log 2023-05-31 20:34:37 +02:00
Sem van der Hoeven
caa8c98afd add other code 2023-05-31 20:32:52 +02:00
Sem van der Hoeven
209828d35a move websockets to html 2023-05-31 20:30:24 +02:00
Sem van der Hoeven
2839e0cfcf attempt to log image size in bytes 2023-05-31 20:27:06 +02:00
Sem van der Hoeven
f117ead9b1 add sending video test 2023-05-31 20:05:10 +02:00
Sem van der Hoeven
1c8229ad98 comments 2023-05-30 22:33:15 +02:00
Sem van der Hoeven
7a7d0c4f01 fix sse? 2023-05-30 22:30:25 +02:00
Sem van der Hoeven
294e45b9b7 pass data through 2023-05-30 22:27:15 +02:00
Sem van der Hoeven
4163113a52 pass data through 2023-05-30 22:24:52 +02:00
Sem van der Hoeven
f115d3e60c pass data through 2023-05-30 22:22:55 +02:00
Sem van der Hoeven
6c0aa9e15f sse stuff 2023-05-30 22:20:42 +02:00
Sem van der Hoeven
e6e1b11369 add SSE for receiving failsafe status 2023-05-30 22:02:02 +02:00
Sem van der Hoeven
48e1ce8f5b try 2023-05-30 16:18:22 +02:00
Sem van der Hoeven
30c6c8499d try sending image as binary data 2023-05-30 16:09:56 +02:00
Sem van der Hoeven
c3ab03b617 remove logging image 2023-05-30 16:02:14 +02:00
Sem van der Hoeven
213d753168 try decode base64 2023-05-30 15:56:07 +02:00
Sem van der Hoeven
a58556a8a5 add base64 image 2023-05-30 15:50:38 +02:00
Sem van der Hoeven
59e9ff84f8 typo 2023-05-30 15:40:17 +02:00
Sem van der Hoeven
0cc0d16091 fix filename 2023-05-30 14:58:26 +02:00
Sem van der Hoeven
23619a9f72 add filename of result 2023-05-30 14:57:02 +02:00
Sem van der Hoeven
00f6307909 change location of camera image 2023-05-30 14:50:51 +02:00
Sem van der Hoeven
bc1bc0dae6 add callback for service response 2023-05-30 13:04:52 +02:00
Sem van der Hoeven
5843575649 more picture logging 2023-05-30 12:48:17 +02:00
Sem van der Hoeven
02875313f6 fix pciture client 2023-05-30 12:41:54 +02:00
Sem van der Hoeven
753760302e fix filename typo 2023-05-30 12:37:45 +02:00
Sem van der Hoeven
e4f06440fb fix bug 2023-05-30 12:35:43 +02:00
Sem van der Hoeven
c871da0641 try getting picture to work 2023-05-30 12:14:24 +02:00
Sem van der Hoeven
bd0d12cf67 fix camera error 2023-05-30 12:09:42 +02:00
Sem van der Hoeven
e3b2e067f6 remove onclicks 2023-05-30 11:58:23 +02:00
Sem van der Hoeven
ad2ee62ead add mousedown eventlisteners 2023-05-30 11:57:05 +02:00
Sem van der Hoeven
b2798a8c8a button mouseup 2023-05-30 11:53:56 +02:00
Sem van der Hoeven
87b951060d made api listener exit after waiting for services too long 2023-05-30 11:23:50 +02:00
Sem van der Hoeven
c8c7723ad7 fix message in failsafe 2023-05-30 11:07:03 +02:00
Sem van der Hoeven
8c8abba2b4 fix failsafe spin 2023-05-30 11:03:30 +02:00
Sem van der Hoeven
9ce69bf93e fix failsafe init 2023-05-30 11:02:25 +02:00
Sem van der Hoeven
91c02d2abe add describe service position changer is waiting for 2023-05-30 10:57:20 +02:00
Sem van der Hoeven
cb37cbc02c fix button disappearing 2023-05-30 10:52:17 +02:00
Sem van der Hoeven
62f5dfe06b button dissappears? 2023-05-30 10:50:10 +02:00
Sem van der Hoeven
02f278e61c only alert once 2023-05-30 10:48:28 +02:00
Sem van der Hoeven
c481260638 remove status log spam 2023-05-30 10:46:49 +02:00
Sem van der Hoeven
ea87fbc98c try with settimeout 2023-05-30 10:43:06 +02:00
Sem van der Hoeven
fe958e7e05 add connect function 2023-05-30 10:40:19 +02:00
Sem van der Hoeven
a6226ef99a add API error handling 2023-05-30 10:36:50 +02:00
Sem van der Hoeven
e8fc4e9275 add connect button 2023-05-30 10:34:02 +02:00
Sem van der Hoeven
338ed03004 add waiting on websockets 2023-05-30 10:29:27 +02:00
Sem van der Hoeven
3f3ee06925 add land and takeoff 2023-05-29 19:04:37 +02:00
Sem van der Hoeven
6010f903cb change to multithreaded executor 2023-05-29 18:38:52 +02:00
Sem van der Hoeven
b502c5b285 add try except for sending move request 2023-05-29 18:36:02 +02:00
Sem van der Hoeven
b5271fe5f3 remove log spam 2023-05-29 18:28:54 +02:00
Sem van der Hoeven
a3cfc5cd97 logs 2023-05-29 18:25:12 +02:00
Sem van der Hoeven
c12f016545 handle image 2023-05-29 18:19:45 +02:00
Sem van der Hoeven
6e3e3dc022 change move ints to floats 2023-05-29 15:57:29 +02:00
Sem van der Hoeven
ba31680b67 use json and body 2023-05-29 15:54:22 +02:00
Sem van der Hoeven
56d39487a6 print req before sending 2023-05-29 15:49:30 +02:00
Sem van der Hoeven
4b3eb9a8ce print query before sending 2023-05-29 15:48:50 +02:00
Sem van der Hoeven
6e0b0da412 change data to query 2023-05-29 15:45:59 +02:00
Sem van der Hoeven
c76b66e8b2 set xhr request header after opening 2023-05-29 15:43:08 +02:00
Sem van der Hoeven
bf9dc4de72 send move requests 2023-05-29 15:00:44 +02:00
Sem van der Hoeven
cdc6fecce7 check for empty result 2023-05-29 14:47:41 +02:00
Sem van der Hoeven
98a8808bed more logs 2023-05-29 14:43:14 +02:00
Sem van der Hoeven
a7e836593a sending would be useful 2023-05-29 14:40:29 +02:00
Sem van der Hoeven
084fa9dc77 change if statement for xhr result 2023-05-29 14:39:26 +02:00
Sem van der Hoeven
a0c2c8e05f updating status log 2023-05-29 14:32:02 +02:00
Sem van der Hoeven
0054ba6500 parse status request 2023-05-29 14:30:58 +02:00
Sem van der Hoeven
b69bc88b66 receive status 2023-05-29 14:27:24 +02:00
Sem van der Hoeven
8e4e42cf0a log type and data 2023-05-29 14:17:34 +02:00
Sem van der Hoeven
f98b44917c add more websocket connected checks 2023-05-29 14:14:55 +02:00
Sem van der Hoeven
73709a257f add up request 2023-05-29 14:08:06 +02:00
Sem van der Hoeven
1f4fbb2246 button callbacks 2023-05-29 13:33:16 +02:00
Sem van der Hoeven
369a4c2b75 add buttons 2023-05-29 13:19:56 +02:00
Sem van der Hoeven
d656338993 typo 2023-05-29 13:07:16 +02:00
Sem van der Hoeven
98133c8815 css? 2023-05-29 13:05:10 +02:00
Sem van der Hoeven
7c1bb248e7 typo 2023-05-29 13:01:02 +02:00
Sem van der Hoeven
340419a187 update gui 2023-05-29 12:57:23 +02:00
Sem van der Hoeven
3b97c44ed8 publish status? 2023-05-29 12:47:40 +02:00
Sem van der Hoeven
c67d4ac73a add labels 2023-05-29 12:41:57 +02:00
Sem van der Hoeven
0ac76f788a add labels 2023-05-29 12:41:12 +02:00
Sem van der Hoeven
8d021678da how to position two divs nexto to eachoter 2023-05-29 12:40:02 +02:00
Sem van der Hoeven
a539dff7ea how to position two divs nexto to eachoter 2023-05-29 12:39:16 +02:00
Sem van der Hoeven
133c2a04ab how to position two divs nexto to eachoter 2023-05-29 12:38:04 +02:00
Sem van der Hoeven
37437f9a53 border 2023-05-29 12:35:58 +02:00
Sem van der Hoeven
0c3f561c38 change heights 2023-05-29 12:35:21 +02:00
Sem van der Hoeven
a1643f9a5c video elements 2023-05-29 12:32:51 +02:00
Sem van der Hoeven
78421ccf6f update style 2023-05-29 12:29:18 +02:00
Sem van der Hoeven
4b18669bb3 update api 2023-05-29 12:25:29 +02:00
Sem van der Hoeven
34e4748737 add u16 [prefix 2023-05-29 12:07:21 +02:00
Sem van der Hoeven
bb8182baa0 add handling of velocity 2023-05-29 12:02:18 +02:00
Sem van der Hoeven
641dd746c4 change failsafe wstring (char16) to string (char) 2023-05-29 10:56:17 +02:00
Sem van der Hoeven
437ea4f536 change string in enable_failsafe to be same type of string as in failsafe message request 2023-05-29 10:48:05 +02:00
Sem van der Hoeven
d5ce727c3d add failsafe to msg interfaces 2023-05-29 10:40:03 +02:00
Sem van der Hoeven
7fc72064c1 Merge branch 'api' of github.com:SemvdH/5g_drone_ROS2 into api 2023-05-29 10:40:00 +02:00
Sem van der Hoeven
581f53735b add failsafe to msg interfaces 2023-05-29 10:39:52 +02:00
SemvdH
14b94a2ec4 Merge pull request #11 from SemvdH/position_changer
merge Position changer into main for report
2023-05-26 23:15:20 +02:00
SemvdH
3573c2532c Merge pull request #10 from SemvdH/position_changer
merge Position changer into main to use files in report
2023-05-26 18:04:25 +02:00
53 changed files with 2463 additions and 390 deletions

151
README.md
View File

@@ -1 +1,152 @@
# 5G RCID
## 5G Remote Controlled Inspection Drone
By Sem van der Hoeven
## Table of contents
- [Introduction](#introduction)
- [EMERGENCY FORCE STOP](#emergency-force-stop)
- [Pin layout](#pin-layout)
- [Connecting to API](#connecting-to-api)
- [Installing API](#installing-api)
- [Installation on new flight computer](#installation-on-new-flight-computer)
- [Building PX4 firmware](#building-px4-firmware)
- [ROS2 nodes overview](#ros2-nodes-overview)
- [PX4 parameters](#px4-parameters)
- [Optical flow](#optical-flow)
## Introduction
This is the code for the 5G RCID of the 5G Hub. All ROS2 nodes and the API code can be found here. The flight computer currently already contains the latest version of the software.
- The code for the ROS2 nodes can be found in the [src folder (src/)](/src/)
- The code for the API can be found in the [api folder (api/)](/api/)
- Further information about how the drone is built and how it works can be found in my report in the [docs folder (doc/)](/doc/)
To add new ROS2 functionality, you can edit the code, push to the repository, pull on the flight computer (ubuntu@10.100.0.40, password is `raspberrypi`) and build the ROS2 workspace again. An example of how to do this is shown below:
```bash
# (On your computer) commit changes
git commit -a -m "Added new functionality"
git push
# (On the flight computer) pull changes
cd /home/ubuntu/ros2_ws
git fetch
git pull
# (On the flight computer) build workspace
colcon build --allow-overriding drone_services
# (On the flight computer) source workspace
source install/setup.bash
# (On the flight computer) restart ROS2 nodes
drone_scripts/restart_services.sh
```
To add new API functionality, you can do the same, but for the code in the api folder. Then, log in on the edge computer and pull the changes. The API is automatically restarted when the code is changed. If you want to manually restart the API, you can do so using the following command:
```bash
# (On the edge computer) restart API
sudo systemctl restart webserver
```
## EMERGENCY FORCE STOP
If, for whatever reason, the drone does not respond to the API, you can force stop all nodes on the drone and force it to land through the pixhawk using the following commands:
```bash
# on your computer, ssh into the drone
ssh ubuntu@10.100.0.40
# cd into the ROS2 workspace
cd ros2_ws
# stop all ROS2 nodes
drone_scripts/stop_services.sh
```
## Pin layout
A pinout of the raspberry pi can be found [here](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html). The connections are visible in the table below:
|Raspberry Pi pin|Function|Connected to|
|---|---|---|
|2|5V|Power input from PDB|
|4|5V|Power to relais|
|6|GND|Ground from PDB|
|8|UART TX|To RX of Pixhawk TELEM 1|
|9|GND|Ground to relais|
|10|UART RX|To TX of Pixhawk TELEM 1|
|11|GPIO 17|Relais|
|13|GPIO 27|Relais|
## Connecting to API
To connect to the API, make sure you are connected to the 5G Hub network. Then, the API is located at http://10.1.1.41:8080/. When the drone is finished booting (the relais is switched on), you can connect to the drone using the `Connect` button.
## Installing API
To be able to run the API, npm and nodejs must be installed:
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
nvm install node
# to be able to run npm and nodejs as sudo, run the following commands:
n=$(which node)
n=${n%/bin/node}
chmod -R 755 $n/bin/*
sudo cp -r $n/{bin,lib,share} /usr/local
```
To install the API on a new edge computer, you must first clone this repository. Then you can run the NodeJS webserver using npm. You can do that using the following commands:
```bash
git clone https://github.com/etmeddi/5g_drone_ROS2.git
cd 5g_drone_ROS2/api
npm start
```
## Installation on new flight computer
The drone currently has a Raspberry Pi that contains a ROS2 installation. The Raspberry Pi runs Ubuntu 20.04 and ROS 2 Foxy. If you want to install this on a new Pi, you should [get Ubuntu Server 20.04](https://ubuntu.com/download/server#downloads), and [install ROS2 Foxy](https://docs.ros.org/en/foxy/Installation/Ubuntu-Install-Debians.html) on it. Then, you should clone this repository into a `ros2_ws` folder. You can do that using the following commands:
```bash
git clone git@github.com:etmeddi/5g_drone_ROS2.git
mv 5g_drone_ROS2 ros2_ws
```
After that, to make sure the connection to PX4 works, you should follow the [PX4 ROS2 User Guide](https://docs.px4.io/main/en/ros/ros2_comm.html#installation-setup) provided by PX4.
## Building PX4 firmware
The RCID uses a custom version of the PX4 firmware to include the battery percentage and CPU load of the flight controller. Before building the firmware, make sure the [developer toolchain](https://docs.px4.io/main/en/dev_setup/dev_env.html) is set up correctly. To build the firmware, first clone the repository (from [the PX4 docs](https://docs.px4.io/main/en/dev_setup/building_px4.html)):
```bash
# enter home directory
cd
# clone git repository
git clone https://github.com/PX4/PX4-Autopilot.git --recursive
```
In the PX4 directory, edit the file `src/modules/uxrce_dds_client/dds_topics.yaml`. Above the line that says `subscriptions:`, add the following:
```yaml
- topic: /fmu/out/battery_status
type: px4_msgs::msg::BatteryStatus
- topic: /fmu/out/cpuload
type: px4_msgs::msg::Cpuload
```
The file can also be found in this repository at [dds_topics.yaml](dds_topics.yaml). After changing the file, you can build the firmware using the following command:
```bash
# navigate to root of PX4 directory
cd ~/PX4-Autopilot
# build firmware
make px4_fmu-v5_default
```
The built firmware file will be located at `PX4-Autopilot/build/px4_fmu-v4_default/px4_fmu-v4_default.px4`. You can then flash this to the flight controller using [QGroundControl](http://qgroundcontrol.com/). Make sure to select the [custom firmware](https://docs.px4.io/main/en/config/firmware.html#installing-px4-master-beta-or-custom-firmware) option when flashing, and select the built firmware file.
## ROS2 nodes overview
An overview of all the ROS2 nodes, services and topics can be found below (also visible in my report)
![ROS2 nodes overview](/doc/ROSNodes.jpg)
## PX4 parameters
To enable communication with the flight computer, the following parameters should be set in QGroundControl:
|Parameter|Value|Function|
|---|---|---|
|UXRCE_DDS_CFG|101|run microxrce-dds on TELEM 1|
|MAV_0_CONFIG|TELEM 4|run mavlink on TELEM 4 because XRCE-DDS runs on TELEM 1|
|SER_TEL1_BAUD|921600|high baud rate because serial|
|COM_RC_IN_MODE|4 (Stick input disabled)|allow arming without GPS|
|COM_RCL_EXCEPT|5|Ignore loss of RC remote signal|
## Optical flow
The optical flow sensor is a [Hex HereFlow PMW3901](https://docs.px4.io/main/en/sensor/pmw3901.html#hex-hereflow-pmw3901-optical-flow-sensor) and it's connected to the CAN port of the pixhawk. The parameters to enable the optical flow sensor are:
|Parameter|Value|Function|
|---|---|---|
|EKF2_OF_CTRL |1 (Enabled)|Enable optical flow sensor fusion|
|UAVCAN_ENABLE|2 (Sensors automatic config)|Enable UAVCAN for sensors|
|UAVCAN_SUB_FLOW|Enabled|subscribe to optical flow messages|
|UAVCAN_SUB_GPS|Disabled|subscribe to GPS messages|
To reverse this, and be able to use GPS, the parameters should be:
|Parameter|Value|Function|
|---|---|---|
|EKF2_OF_CTRL |0 (Disabled)|Disable optical flow sensor fusion|
|UAVCAN_ENABLE|0 (Disabled)|Disable UAVCAN for sensors|
|UAVCAN_SUB_FLOW|Disabled|don't subscribe to optical flow messages|
|UAVCAN_SUB_GPS|Enabled|subscribe to GPS messages|

View File

@@ -1,19 +1,10 @@
var express = require("express");
var app = express();
const WebSocket = require("ws");
var ws = new WebSocket("ws://10.100.0.40:9001/");
let sse_clients = [];
ws.on('open', function open() {
console.log("connected");
});
ws.on("message", function message(message) {
var msg = JSON.parse(message);
console.log("RECEIVED: " + msg);
});
ws.on("error", console.error);
app.use(express.static("public"));
app.use(express.json());
// set the view engine to ejs
app.set("view engine", "ejs");
@@ -22,8 +13,10 @@ app.set("view engine", "ejs");
// index page
app.get("/", function (req, res) {
res.render("index");
res.render("index", { api_connected: api_connected });
});
app.get("/events", handle_sse_client);
app.listen(8080);
console.log("Server is listening on port 8080");

View File

@@ -0,0 +1,66 @@
body {
background-color: azure;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
html, body {
overflow: auto;
}
.header {
color: black;
text-align: center;
}
.video {
width: 100%;
overflow: auto;
}
.mainvideo {
width: 50%;
float: left;
height: 100%;
border: 1px solid blue;
margin-right: 10px;
padding: 10px;
overflow: visible;
}
.lastpicture {
width: 40%;
float: right;
/* height: 400px; */
border: 1px solid red;
padding: 10px;
}
#connectedbuttons {
overflow: auto;
}
#buttons {
float: left;
}
#connectedstatus {
float:left;
width: 80%;
}
#take_picture {
float:right;
}
#arm_disarm {
float: right;
}
#button_estop {
background-color: red;
color: white;
}
.headerimg {
height: 200px;
/* float: right; */
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

View File

@@ -1,12 +1,275 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>5G drone API</title>
</head>
<body>
<div>Hello World!</div>
</body>
</html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel='stylesheet' href='/css/stylesheet.css' />
<title>5G drone API</title>
</head>
<body style="height: 100%;">
<div>
<h1 class="header">5G Drone API</h1>
</div>
<div class="mainvideo">
<p id="cameraview">Camera view: Not connected</p>
<canvas id="result-video" style="border: 1px solid blue;" width="800" height="600"></canvas>
<div id="connectedbuttons">
<div id="connectedstatus">
<h2 id="connectedlabel">Not connected to drone</h2>
<button id="connectbutton" onclick="local_connect()">Connect</button>
</div>
<div id="buttons">
<button id="take_picture" onclick="take_picture()">Take picture</button>
<br>
<button id="arm_disarm" onclick="arm_disarm()">Arm/Disarm</button>
</div>
</div>
<div id="controls">
<h2>Controls</h2>
<div id="buttons">
<button class="movebutton" id="button_turnleft">Turn left</button>
<button class="movebutton" id="button_turnright">Turn right</button>
<button class="movebutton" id="button_up">Up</button>
<button class="movebutton" id="button_down">Down</button>
<button class="movebutton" id="button_forward">Forward</button>
<button class="movebutton" id="button_backward">Backward</button>
<button class="movebutton" id="button_left">Left</button>
<button class="movebutton" id="button_right">Right</button>
<button id="button_land" onclick="land()">Land</button>
<button id="button_estop" onclick="estop()"><strong>Emergency Stop</strong></button>
</div>
</div>
</div>
<div class="lastpicture">
<p>Last picture:</p>
<div id="lastimg">
<img id="picture" style="width: 400px;">
</div>
<h2>Drone status</h2>
<p id="batterypercentage">Battery percentage</p>
<p id="cpuload">CPU load</p>
<p id="armed"></p>
<p id="control_mode"></p>
<p id="speed">Current speed</p>
<p id="position">Current position</p>
<p id="height">Height</p>
<p id="failsafe">Failsafe not activated</p>
<img class="headerimg"
src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Ericsson_logo.svg/2341px-Ericsson_logo.svg.png"
alt="ericsson logo">
<img class="headerimg" src="https://hightechcampus.com/storage/1069/5ghub-logo.png" alt="5g hub logo">
</div>
</body>
<script>
var ws;
assign_button_callbacks();
openSocket = () => {
document.getElementById("cameraview").innerHTML = "Camera view: Connecting...";
socket = new WebSocket("ws://10.100.0.40:9002/");
let msg = document.getElementById("result-video");
socket.addEventListener('open', (e) => {
console.log("Connected to video")
document.getElementById("cameraview").innerHTML = "Camera view: Connected";
});
socket.addEventListener('message', (e) => {
let ctx = msg.getContext("2d");
let image = new Image();
image.src = URL.createObjectURL(e.data);
image.addEventListener("load", (e) => {
ctx.drawImage(image, 0, 0, 800, 600);
});
});
socket.addEventListener('close', (e) => {
console.log("Disconnected from video")
document.getElementById("cameraview").innerHTML = "Camera view: Disconnected. Reload the page to reconnect";
});
socket.addEventListener('error', (e) => {
console.log("Error in video connection")
document.getElementById("cameraview").innerHTML = "Camera view: Error. Reload the page to reconnect";
});
}
function assign_button_callbacks() {
var buttons = document.getElementsByClassName("movebutton");
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("mouseup", function () {
stop();
});
}
document.getElementById("button_forward").addEventListener("mousedown", function () { forward(); });
document.getElementById("button_backward").addEventListener("mousedown", function () { backward(); });
document.getElementById("button_left").addEventListener("mousedown", function () { left(); });
document.getElementById("button_right").addEventListener("mousedown", function () { right(); });
document.getElementById("button_turnleft").addEventListener("mousedown", function () { turn_left(); });
document.getElementById("button_turnright").addEventListener("mousedown", function () { turn_right(); });
document.getElementById("button_up").addEventListener("mousedown", function () { up(); });
document.getElementById("button_down").addEventListener("mousedown", function () { down(); });
}
function send_move_request(data) {
console.log("sending move request");
if (ws != null)
ws.send(data);
}
function turn_left() {
console.log("turnleft");
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 0.0, "left_right": 0.0, "yaw": -10.0 }));
}
function turn_right() {
console.log("turnright");
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 0.0, "left_right": 0.0, "yaw": 10.0 }));
}
function up() {
console.log("up");
send_move_request(JSON.stringify({ "command": 3, "up_down": 1.0, "forward_backward": 0.0, "left_right": 0.0, "yaw": 0.0 }));
}
function down() {
console.log("down");
send_move_request(JSON.stringify({ "command": 3, "up_down": -1.0, "forward_backward": 0.0, "left_right": 0.0, "yaw": 0.0 }));
}
function forward() {
console.log("forward"); send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 1.0, "left_right": 0.0, "yaw": 0.0 }));
}
function backward() {
console.log("backward");
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": -1.0, "left_right": 0.0, "yaw": 0.0 }));
}
function left() {
console.log("left");
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 0.0, "left_right": -1.0, "yaw": 0.0 }));
}
function right() {
console.log("right");
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 0.0, "left_right": 1.0, "yaw": 0.0 }));
}
function stop() {
console.log("stop");
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 0.0, "left_right": 0.0, "yaw": 0.0 }));
}
function land() {
console.log("land");
var request = JSON.stringify({ command: 0 });
ws.send(request);
}
function estop() {
console.log("estop");
var request = JSON.stringify({
command: 6,
});
ws.send(request);
}
function take_picture() {
console.log("take picture");
var image = new Image();
image.src = document.getElementById("result-video").toDataURL();
image.width = 400;
document.getElementById("lastimg").innerHTML = "";
document.getElementById("lastimg").appendChild(image);
}
function arm_disarm() {
console.log("arm/disarm");
var request = JSON.stringify({ command: 1 });
ws.send(request);
}
function handle_ws_message(data) {
set_timout = false;
if (data.type == "STATUS") {
document.getElementById("batterypercentage").innerHTML = "Battery percentage: " + data.data.battery_percentage.toString().substring(0, 4) + "%";
document.getElementById("cpuload").innerHTML = "CPU load: " + data.data.cpu_usage.toString().substring(0, 6).substring(2, 4) + "%";
document.getElementById("armed").innerHTML = "Armed: " + data.data.armed;
document.getElementById("control_mode").innerHTML = "Control mode: " + data.data.control_mode;
document.getElementById("speed").innerHTML = "Current speed (m/s): x: " + data.data.velocity[0].toString().substring(0,4) + " y: " + data.data.velocity[1].toString().substring(0,4) + " z: " + data.data.velocity[2].toString().substring(0,4);
document.getElementById("position").innerHTML = "Current position (m): x: " + data.data.position[0].toString().substring(0,4) + " y: " + data.data.position[1].toString().substring(0,4) + " z: " + data.data.position[2].toString().substring(0,4);
} else if (data.type == "FAILSAFE") {
document.getElementById("failsafe").innerHTML = "Failsafe: ACTIVATED";
document.getElementById("failsafe").style.backgroundColor = "red";
document.getElementById("failsafe").style.color = "white";
document.getElementById("failsafe").style.textDecoration = "bold";
alert("Failsafe enabled! Drone is landing. The failsafe message is:\n" + data.message);
} else if (data.type == "API_HEARTBEAT") {
concole.log("Got heartbeat from API")
clearTimeout(api_timout);
}
}
function local_connect() {
console.log("Connecting to API");
ws = new WebSocket("ws://10.100.0.40:9001/");
ws.addEventListener("open", function open() {
console.log("connected with websockets to API!");
document.getElementById("connectedlabel").innerHTML = "Connected to drone";
document.getElementById("connectbutton").disabled = true;
openSocket();
});
ws.addEventListener("message", function message(message) {
try {
var msg = JSON.parse(message.data);
handle_ws_message(msg);
} catch (error) {
console.log("could not parse as json");
console.error(error)
}
});
ws.addEventListener("error", function error(err) {
console.log("there was an error! " + err);
console.log("Showing alert!");
alert("There was an error connecting to the API!");
document.getElementById("connectedlabel").innerHTML = "Not connected to drone";
document.getElementById("connectbutton").disabled = false;
});
ws.addEventListener("close", function close(event) {
console.log("connection closed");
alert("Connection to API closed!")
document.getElementById("connectedlabel").innerHTML = "Not connected to drone";
document.getElementById("connectbutton").disabled = false;
});
}
function connect() {
var received = false;
var xhr = new XMLHttpRequest();
xhr.open("GET", "/connect", true);
xhr.onreadystatechange = function () {
if (this.status == 200) {
console.log(this.responseText);
if (this.responseText.length > 0) {
var status = JSON.parse(this.responseText);
document.getElementById("connectedlabel").innerHTML = "Connected to drone";
document.getElementById("connectbutton").disabled = true;
}
} else {
console.log("error");
document.getElementById("connectedlabel").innerHTML = "Not connected to drone";
if (!received) {
alert("Could not connect to API!");
received = true;
}
}
};
xhr.send();
}
</script>
</html>

37
api/views/test.ejs Normal file
View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<title>Python_Websocket_Live_Streaming</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body onload="openSocket()">
<div id="status">
Connection failed. Somebody may be using the socket.
</div>
<div style="text-align: center">
<canvas id="msg" width="960" height="720" style="display:inline-block" />
</div>
</body>
<script>
openSocket = () => {
socket = new WebSocket("ws://10.100.0.40:9001/");
let msg = document.getElementById("msg");
socket.addEventListener('open', (e) => {
document.getElementById("status").innerHTML = "Opened";
});
socket.addEventListener('message', (e) => {
let ctx = msg.getContext("2d");
let image = new Image();
image.src = URL.createObjectURL(e.data);
image.addEventListener("load", (e) => {
ctx.drawImage(image, 0, 0, msg.width, msg.height);
});
});
}
</script>
</html>

BIN
doc/5G RCID Presentatie.pdf Normal file

Binary file not shown.

BIN
doc/ROSNodes.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

View File

@@ -0,0 +1,14 @@
echo "waiting 5 secs before restarting..."
sleep 5
echo "restarting"
sudo systemctl restart drone_api.service
sudo systemctl restart drone_camera.service
sudo systemctl restart drone_failsafe.service
sudo systemctl restart drone_find_usb_devices.service
sudo systemctl restart drone_height_sensor.service
sudo systemctl restart drone_lidar.service
sudo systemctl restart drone_positionchanger.service
sudo systemctl restart drone_px4_connection.service
sudo systemctl restart drone_relais.service
sudo systemctl restart drone_status.service

7
drone_scripts/start_api.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
. /home/ubuntu/source_ros2.sh
ros2 run api_communication api_listener

View File

@@ -0,0 +1,7 @@
#!/bin/bash
. /home/ubuntu/source_ros2.sh
ros2 run failsafe failsafe

View File

@@ -0,0 +1,7 @@
#!/bin/bash
. /home/ubuntu/source_ros2.sh
ros2 run drone_controls position_changer

View File

@@ -0,0 +1,7 @@
#!/bin/bash
. /home/ubuntu/source_ros2.sh
ros2 launch px4_connection px4_controller_heardbeat_launch.py

10
drone_scripts/stop_services.sh Executable file
View File

@@ -0,0 +1,10 @@
sudo systemctl stop drone_api.service
sudo systemctl stop drone_camera.service
sudo systemctl stop drone_failsafe.service
sudo systemctl stop drone_find_usb_devices.service
sudo systemctl stop drone_height_sensor.service
sudo systemctl stop drone_lidar.service
sudo systemctl stop drone_positionchanger.service
sudo systemctl stop drone_px4_connection.service
sudo systemctl stop drone_relais.service
sudo systemctl stop drone_status.service

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

10
restart_services.sh Executable file
View File

@@ -0,0 +1,10 @@
sudo systemctl restart drone_api.service
sudo systemctl restart drone_camera.service
sudo systemctl restart drone_failsafe.service
sudo systemctl restart drone_find_usb_devices.service
sudo systemctl restart drone_height_sensor.service
sudo systemctl restart drone_lidar.service
sudo systemctl restart drone_positionchanger.service
sudo systemctl restart drone_px4_connection.service
sudo systemctl restart drone_relais.service
sudo systemctl restart drone_status.service

View File

@@ -2,46 +2,84 @@ import rclpy
from rclpy.node import Node
from drone_services.msg import DroneStatus
from drone_services.msg import FailsafeMsg
from drone_services.srv import TakePicture
from drone_services.srv import MovePosition
from drone_services.srv import EnableFailsafe
from drone_services.srv import ArmDrone
from drone_services.srv import DisarmDrone
from drone_services.srv import ReadyDrone
from drone_services.srv import Land
import asyncio
import websockets.server
import threading
import json
from enum import Enum
# communication: client always sends commands that have a command id.
# server always sends messages back that have a message type
from functools import partial
from functools import partial
class RequestCommand(Enum):
GET_COMMANDS_TYPES = -1 # to get the available commands and types
TAKEOFF = 0
LAND = 1
"""
Enum for the commands that can be sent to the API
"""
GET_COMMANDS_TYPES = -1
LAND = 0
ARM_DISARM = 1
MOVE_POSITION = 2
MOVE_DIRECTION = 3
TAKE_PICTURE = 5
EMERGENCY_STOP = 6
class ResponseMessage(Enum):
"""
Enum for the messages that can be sent to the client
"""
ALL_REQUESTS_RESPONSES = -1
STATUS = 0
IMAGE = 1
MOVE_DIRECTION_RESULT = 2
FAILSAFE = 3
class ApiListener(Node):
"""
Node that listens to the client and sends the commands to the drone
"""
def __init__(self):
super().__init__('api_listener')
self.get_logger().info('ApiListener node started')
self.drone_status_subscriber = self.create_subscription(
DroneStatus, '/drone/status', self.drone_status_callback, 10)
self.drone_status_subscriber = self.create_subscription(DroneStatus, '/drone/status', self.drone_status_callback, 10)
self.failsafe_subscriber = self.create_subscription(FailsafeMsg, "/drone/failsafe", self.failsafe_callback, 10)
self.timer = self.create_timer(1, self.publish_status)
self.take_picture_client = self.create_client(
TakePicture, '/drone/picture')
while not self.take_picture_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Take picture service not available, waiting again...')
self.take_picture_client = self.create_client(TakePicture, '/drone/picture')
self.wait_for_service(self.take_picture_client, "Take picture")
self.take_picture_request = TakePicture.Request()
self.move_position_client = self.create_client(MovePosition, '/drone/move_position')
self.wait_for_service(self.move_position_client, "Move position")
self.move_position_request = MovePosition.Request()
self.enable_failsafe_client = self.create_client(EnableFailsafe, "/drone/enable_failsafe")
self.wait_for_service(self.enable_failsafe_client, "Enable failsafe")
self.enable_failsafe_request = EnableFailsafe.Request()
self.arm_drone_client = self.create_client(ArmDrone, "/drone/arm")
self.wait_for_service(self.arm_drone_client, "Arm drone")
self.arm_drone_request = ArmDrone.Request()
self.disarm_drone_client = self.create_client(DisarmDrone, "/drone/disarm")
self.wait_for_service(self.disarm_drone_client, "Disarm drone")
self.disarm_drone_request = DisarmDrone.Request()
self.ready_drone_client = self.create_client(ReadyDrone, "/drone/ready")
self.wait_for_service(self.ready_drone_client, "Ready drone")
self.ready_drone_request = ReadyDrone.Request()
self.land_client = self.create_client(Land, "/drone/land")
self.wait_for_service(self.land_client, "Land drone")
self.land_request = Land.Request()
self.status_data = {}
self.status_data_received = False
self.last_message = ""
@@ -57,53 +95,114 @@ class ApiListener(Node):
target=self.handle_responses, daemon=True)
self.response_thread.start()
def drone_status_callback(self, msg):
self.status_data_received = True
self.status_data['battery_percentage'] = msg.battery_percentage
self.status_data['cpu_usage'] = msg.cpu_usage
self.status_data['armed'] = msg.armed
self.status_data['control_mode'] = msg.control_mode
self.status_data['route_setpoint'] = msg.route_setpoint
self.event_loop = None
self.armed = False
self.failsafe_enabled = False
self.has_sent_failsafe_msg = False
def publish_message(self, message):
self.get_logger().info(f'Publishing message: {message}')
asyncio.run(self.websocket.send(message))
def wait_for_service(self, client, service_name):
"""Waits for a client service to be available
Args:
client (ROS2 service client): The client to use to wait fo the service
service_name (str): The client name for logging
"""
waiting = 0
while not client.wait_for_service(timeout_sec=1.0):
if (waiting > 10):
self.get_logger().error(service_name + ' service not available, exiting...')
exit(-1)
self.get_logger().info(service_name + 'service not available, waiting again...')
waiting = waiting + 1
def drone_status_callback(self, msg):
"""Callback for when a drone_status message is received
Args:
msg (DroneStatus): The received message
"""
try:
self.status_data_received = True
self.status_data['battery_percentage'] = msg.battery_percentage
if msg.battery_percentage < 15:
self.enable_failsafe("Battery level too low! Failsafe enabled to prevent damage to battery (" + str(msg.battery_percentage ) + "%)")
self.status_data['cpu_usage'] = msg.cpu_usage
self.status_data['armed'] = msg.armed
self.armed = msg.armed
self.status_data['control_mode'] = msg.control_mode
self.status_data['route_setpoint'] = msg.route_setpoint
self.status_data['velocity'] = [float(msg.velocity[0]), float(
msg.velocity[1]), float(msg.velocity[2])]
self.status_data['position'] = [float(msg.position[0]), float(
msg.position[1]), float(msg.position[2])]
self.status_data['failsafe'] = msg.failsafe
self.status_data['height'] = msg.height
except Exception as e:
self.get_logger().error(f'Error while parsing drone status message: {e}')
def failsafe_callback(self, msg):
"""Callback for when the failsafe gets enabled. Queues a FAILSAFE message to the client
Args:
msg (FailSAfe): The message that was received
"""
if self.failsafe_enabled:
return
if not self.has_sent_failsafe_msg:
self.has_sent_failsafe_msg = True
self.status_data['failsafe'] = msg.enabled
self.message_queue.append(json.dumps({'type': ResponseMessage.FAILSAFE.name, 'message': msg.msg}))
async def publish_message(self, message):
"""publishes a message to the NodeJS client
Args:
message (JSON object): the message to send
"""
# self.get_logger().info(f'Publishing message: {message}')
if self.websocket is not None:
try:
await self.websocket.send(message)
except Exception as e:
self.get_logger().error('Something went wrong while sending a message to the websocket: ' + str(e))
else:
self.get_logger().error('Trying to publish message but no websocket connection')
def publish_status(self):
"""publishes the current status to the client by queueing a STATUS message
"""
if self.status_data_received:
self.status_data_received = False
if self.websocket is not None:
self.message_queue.append(json.dumps(
{'type': ResponseMessage.STATUS.name, 'data': self.status_data}))
try:
self.message_queue.append(json.dumps({'type': ResponseMessage.STATUS.name, 'data': self.status_data}))
except Exception as e:
self.get_logger().error('Something went wrong while publishing the status: ' + str(e))
def handle_responses(self):
"""Thread to handle responses to send to the client
"""
while True:
if len(self.message_queue) > 0:
self.publish_message(self.message_queue.pop(0))
if len(self.message_queue) > 0 and self.websocket is not None:
# self.get_logger().info("sending message")
asyncio.run(self.publish_message(self.message_queue.pop(0)))
def start_api_thread(self):
"""Starts the API thread"""
asyncio.run(self.handle_api())
async def handle_api(self):
"""Handles the API requests and starts the websockets server"""
self.get_logger().info('Starting API')
self.event_loop = asyncio.get_event_loop()
self.server = await websockets.serve(self.api_handler, '0.0.0.0', 9001)
self.get_logger().info('API started on port 9001')
await self.server.wait_closed()
def process_image_request(self, message_json):
self.get_logger().info('Take picture command received')
if message_json['filename']:
self.get_logger().info(
f'Filename: {message_json["filename"]}')
self.take_picture_request.input_name = message_json['filename']
self.future = self.cli.call_async(self.take_picture_request)
rclpy.spin_until_future_complete(self, self.future)
result_filename = self.future.result()
with open(result_filename, 'rb') as f:
image_data = f.read()
self.message_queue.append(json.dumps(
{'type': ResponseMessage.IMAGE.name, 'image': image_data}))
def send_available_commands(self):
"""Sends the available commands to the client
"""
print('Sending available commands')
requestcommands = {}
messagetypes = {}
@@ -114,10 +213,146 @@ class ApiListener(Node):
messagetypes[message_type.name] = message_type.value
result['request_commands'] = requestcommands
result['response_messages'] = messagetypes
self.message_queue.append(json.dumps(
{'type': ResponseMessage.ALL_REQUESTS_RESPONSES.name, 'data': result}))
self.message_queue.append(json.dumps({'type': ResponseMessage.ALL_REQUESTS_RESPONSES.name, 'data': result}))
def handle_direction_message(self, message):
"""Calls the move position service with the given values
Args:
message (JSON object): the message containing the direction values
"""
self.move_position_request.up_down = float(message['up_down'])
self.move_position_request.left_right = float(message['left_right'])
self.move_position_request.front_back = float(
message['forward_backward'])
self.move_position_request.angle = float(message['yaw'])
self.get_logger().info(f'Calling move position service with request: {str(self.move_position_request)}')
self.send_move_position_request()
def send_move_position_request(self):
"""Sends the move position request to the move position service"""
future = self.move_position_client.call_async(
self.move_position_request)
future.add_done_callback(partial(self.move_position_service_callback))
def move_position_service_callback(self, future):
"""Callback for the move position service
Args:
future (Future): Future object that holds the result
"""
try:
result = future.result()
message_result = {}
message_result['type'] = ResponseMessage.MOVE_DIRECTION_RESULT.name
self.get_logger().info(f'Move position service call result: {str(result)}')
if result.success == True:
self.get_logger().info('Move position service call success')
message_result['success'] = True
else:
self.get_logger().error('Move position service call failed')
message_result['success'] = False
self.message_queue.append(json.dumps(message_result))
except Exception as e:
self.get_logger().error('Something went wrong while sending a move position request and waiting for the response: %r' % (e))
def enable_failsafe(self, message):
self.get_logger().info("Enabling failsafe")
self.enable_failsafe_request.message = message
future = self.enable_failsafe_client.call_async(
self.enable_failsafe_request)
future.add_done_callback(partial(self.enable_failsafe_callback))
def enable_failsafe_callback(self, future):
try:
result = future.result()
if (result.enabled == True):
self.get_logger().info("Failsafe activated")
except Exception as e:
self.get_logger().error("Something went wrong while trying to enable failsafe!\n" + str(e))
def emergency_stop(self):
"""Sends an emergency stop request to the failsafe service"""
self.get_logger().info('Activating emergency stop')
self.enable_failsafe("Emergency stop activated")
def takeoff(self):
"""Sends a takeoff request to the move position service"""
self.get_logger().info('Takeoff command received')
self.move_position_request.up_down = 0.1
self.move_position_request.left_right = 0.0
self.move_position_request.front_back = 0.0
self.move_position_request.angle = 0.0
self.send_move_position_request()
def land(self):
"""Sends a land request to the move position service"""
self.get_logger().info('Land command received')
self.move_position_request.up_down = -0.1
self.move_position_request.left_right = 0.0
self.move_position_request.front_back = 0.0
self.move_position_request.angle = 0.0
self.send_move_position_request()
future = self.land_client.call_async(self.land_request)
future.add_done_callback(partial(self.land_service_callback))
def arm_disarm(self):
"""Sends an arm or disarm request to the PX4Controller"""
if self.armed:
self.get_logger().info('Disarm command received')
future = self.disarm_drone_client.call_async(
self.disarm_drone_request)
future.add_done_callback(partial(self.disarm_service_callback))
else:
self.get_logger().info('Arm command received')
future = self.ready_drone_client.call_async(
self.ready_drone_request)
future.add_done_callback(partial(self.ready_drone_callback))
def ready_drone_callback(self, future):
try:
result = future.result()
if result.success:
self.get_logger().info('Ready service call success')
else:
self.get_logger().error('Ready service call failed')
except Exception as e:
self.get_logger().error('Something went wrong while calling the ready service!\n' + str(e))
def arm_service_callback(self, future):
try:
result = future.result()
if result.success:
self.get_logger().info('Arm service call success')
else:
self.get_logger().error('Arm service call failed')
except Exception as e:
self.get_logger().error('Something went wrong while calling the arm service!\n' + str(e))
def disarm_service_callback(self, future):
try:
result = future.result()
if result.success:
self.get_logger().info('Disarm service call success')
self.armed = False
else:
self.get_logger().error('Disarm service call failed')
except Exception as e:
self.get_logger().error('Something went wrong while calling the disarm service!\n' + str(e))
def land_service_callback(self, future):
try:
result = future.result()
if result.is_landing:
self.get_logger().info('Land service call success')
else:
self.get_logger().error('Land service call failed')
except Exception as e:
self.get_logger().error('Something went wrong while calling the land service!\n' + str(e))
def consume_message(self, message):
"""Consumes a message from the client"""
self.get_logger().info(f'Consuming message: {message}')
try:
message_json = json.loads(str(message))
@@ -129,37 +364,41 @@ class ApiListener(Node):
else:
self.get_logger().info(
f'Received command: {message_json["command"]}')
if message_json['command'] == RequestCommand.TAKEOFF.value:
self.get_logger().info('Takeoff command received')
elif message_json['command'] == RequestCommand.LAND.value:
if message_json['command'] == RequestCommand.LAND.value:
self.get_logger().info('Land command received')
elif message_json['command'] == RequestCommand.MOVE_POSITION.value:
self.get_logger().info('Move position command received')
self.land()
elif message_json['command'] == RequestCommand.ARM_DISARM.value:
self.get_logger().info('Arm/disarm command received')
self.arm_disarm()
elif message_json['command'] == RequestCommand.MOVE_DIRECTION.value:
self.get_logger().info('Move direction command received')
elif message_json['command'] == RequestCommand.TAKE_PICTURE.value:
self.process_image_request(message_json)
elif message_json['command'] == RequestCommand.GET.value:
self.handle_direction_message(message_json)
elif message_json['command'] == RequestCommand.GET_COMMANDS_TYPES.value:
self.get_logger().info('Get command received')
self.send_available_commands()
elif message_json['command'] == RequestCommand.EMERGENCY_STOP.value:
self.get_logger().info('Emergency stop command received')
self.emergency_stop()
else:
self.get_logger().error('Received unknown command')
self.get_logger().error('Received unknown command ' + str(message_json['command']))
self.send_available_commands()
except TypeError:
self.get_logger().error('Received unknown command')
self.get_logger().error('Received unknown type: ' + str(type(message)) + " " + str(message))
self.send_available_commands()
except json.JSONDecodeError:
self.get_logger().error('Received invalid JSON')
self.send_available_commands()
except Exception as e:
self.get_logger().error('Something went wrong!')
self.get_logger().error(str(e))
self.get_logger().error(str(type(e)))
self.get_logger().error(e)
async def api_handler(self, websocket):
"""Handles the websocket connection
Args:
websocket (websockets object): the websocket connection
"""
self.get_logger().info('New connection')
# if self.websocket is not None:
# self.get_logger().error('Got a new websocket connection but I am already connected!')
# return
self.websocket = websocket
try:
@@ -175,17 +414,17 @@ class ApiListener(Node):
self.get_logger().error(str(e))
self.websocket = None
def main(args=None):
"""Main function"""
rclpy.init(args=args)
api_listener = ApiListener()
rclpy.spin(api_listener)
multithreaded_executor = rclpy.executors.MultiThreadedExecutor()
multithreaded_executor.add_node(api_listener)
multithreaded_executor.spin()
api_listener.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()

View File

@@ -8,10 +8,6 @@
<license>Apache License 2.0</license>
<depend>rclpy</depend>
<depend>drone_services</depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>

View File

@@ -0,0 +1,110 @@
import os
import sys
import unittest
import time
import launch
import launch_ros
import launch_ros.actions
import launch_testing.actions
import pytest
import rclpy
from drone_services.msg import FailsafeMsg
from drone_services.msg import DroneStatus
@pytest.mark.rostest
def generate_test_description():
file_path = os.path.dirname(__file__)
api_listener_node = launch_ros.actions.Node(
executable=sys.executable,
arguments=[os.path.join(
file_path, '..', 'api_communication', 'api_listener.py')],
additional_env={'PYTHONUNBUFFERED': '1'}
)
failsafe_node = launch_ros.actions.Node(
package='failsafe', executable='failsafe')
camera_node = launch_ros.actions.Node(
package='camera', executable='camera_controller')
position_changer_node = launch_ros.actions.Node(
package='drone_controls', executable='position_changer')
px4_controller_node = launch_ros.actions.Node(
package='px4_connection', executable='px4_controller')
heartbeat_node = launch_ros.actions.Node(
package='px4_connection', executable='heartbeat')
return (
launch.LaunchDescription([
api_listener_node,
failsafe_node,
camera_node,
position_changer_node,
px4_controller_node,
heartbeat_node,
launch_testing.actions.ReadyToTest(),
]),
{
'api_listener_node': api_listener_node,
'failsafe_node': failsafe_node,
'camera_node': camera_node,
'position_changer_node': position_changer_node,
'px4_controller_node': px4_controller_node,
'heartbeat_node': heartbeat_node
}
)
class ApiListenerTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
rclpy.init()
@classmethod
def tearDownClass(cls):
rclpy.shutdown()
def setUp(self):
self.node = rclpy.create_node('test_api_listener')
self.published_battery_status = False
self.received_failsafe_callback = False
def tearDown(self):
self.node.destroy_node()
def failsafe_callback(self,msg):
self.assertTrue(msg.enabled, "Failsafe was not enabled!")
self.assertTrue("Battery level too low! Failsafe enabled to prevent damage to battery" in msg.msg, "Failsafe message was not correct!")
self.received_failsafe_callback = True
def status_callback(self,msg):
self.node.get_logger().info("Received status callback " + str(msg))
def test_api_listener_battery(self, api_listener_node, proc_output):
failsafe_subscriber = self.node.create_subscription(FailsafeMsg, '/drone/failsafe', self.failsafe_callback, 10)
drone_status_publisher = self.node.create_publisher(DroneStatus, '/drone/status', 10)
msg = DroneStatus()
msg.battery_percentage = 10.0
msg.armed = True
msg.height = 10.0
msg.control_mode = "attitude"
msg.cpu_usage = 10.0
msg.route_setpoint = 0
msg.position = [0.0,0.0,0.0]
msg.velocity = [0.0,0.0,0.0]
time.sleep(5) # wait for nodes to start
self.node.get_logger().info("Starting publishing")
end_time = time.time() + 10.0
try:
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
# self.node.get_logger().info("publishing message")
drone_status_publisher.publish(msg)
if self.received_failsafe_callback:
break
self.assertTrue(self.received_failsafe_callback, "Failsafe was not enabled!")
finally:
self.node.destroy_subscription(failsafe_subscriber)
self.node.destroy_publisher(drone_status_publisher)

View File

@@ -1,23 +0,0 @@
# Copyright 2015 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ament_copyright.main import main
import pytest
@pytest.mark.copyright
@pytest.mark.linter
def test_copyright():
rc = main(argv=['.', 'test'])
assert rc == 0, 'Found errors'

View File

@@ -1,25 +0,0 @@
# Copyright 2017 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ament_flake8.main import main_with_errors
import pytest
@pytest.mark.flake8
@pytest.mark.linter
def test_flake8():
rc, errors = main_with_errors(argv=[])
assert rc == 0, \
'Found %d code style errors / warnings:\n' % len(errors) + \
'\n'.join(errors)

View File

@@ -1,23 +0,0 @@
# Copyright 2015 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ament_pep257.main import main
import pytest
@pytest.mark.linter
@pytest.mark.pep257
def test_pep257():
rc = main(argv=['.', 'test'])
assert rc == 0, 'Found code style errors / warnings'

View File

@@ -4,10 +4,7 @@
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
#include "beacon_positioning/msg/tracker_position.hpp"
#include "rtls_driver/rtls_driver.hpp"
// from the example: https://github.com/Terabee/positioning_systems_api/blob/master/examples/rtls_tracker_example.cpp
#include "serial_communication/serial.hpp"
#define TRACKER_0_PORT "/dev/ttyUSB0"

View File

@@ -5,19 +5,54 @@ from sensor_msgs.msg import Image
import os
from datetime import datetime
import asyncio
import websockets.server
import threading
import cv2
import sys
import requests
#resolution of the camera
RES_4K_H = 3496
RES_4K_W = 4656
class CameraController(Node):
def __init__(self):
"""Initialize the camera controller node.
Initializes the service and the video stream for the websocket connection.
"""
super().__init__('camera_controller')
self.get_logger().info("Camera controller started. Waiting for service call...")
self.srv = self.create_service(
TakePicture, 'drone/picture', self.take_picture_callback)
TakePicture, '/drone/picture', self.take_picture_callback)
self.websocket = None
self.server = None
self.event_loop = None
self.should_exit = False
self.timer = self.create_timer(1, self.timer_callback)
self.video_thread = threading.Thread(target=self.setup_websocket)
self.video_thread.start()
def timer_callback(self):
"""Callback function for shutting down if an error occurred from a different thread."""
if self.should_exit:
self.get_logger().info("Shutting down...")
self.destroy_node()
sys.exit(-1)
def take_picture_callback(self, request, response):
"""Callback function for the service call to the /drone/picture service.
takes a picture with the given filename and saves it to the drone_img folder.
Sends the filename back to the caller.
"""
if (request.input_name == "default"):
self.get_logger().info("Taking picture with default filename")
now = datetime.now().strftime("droneimage_%Y-%m-%d_%H-%M-%S")
@@ -25,11 +60,57 @@ class CameraController(Node):
response.filename = imagename
else:
response.filename = request.input_name
os.system('fswebcam -r ' + RES_4K_W + 'x' + RES_4K_H + ' ' + response.filename)
os.system('fswebcam -r ' + str(RES_4K_W) + 'x' + str(RES_4K_H) + ' ' + response.filename)
self.get_logger().info("Picture saved as " + response.filename)
return response
def setup_websocket(self):
"""Sets up the websocket connection for the video stream on port 9002."""
loop = asyncio.new_event_loop()
connected = False
while not connected:
try:
start_server = websockets.serve(self.websocket_video, "0.0.0.0", 9002,loop=loop)
connected = True
except Exception as e:
self.get_logger().error("error " + str(e))
connected = False
loop.run_until_complete(start_server)
loop.run_forever()
try:
self.destroy_node()
except Exception as e:
self.get_logger().error("error " + str(e))
sys.exit(-1)
async def websocket_video(self,websocket,path):
"""Function for the websocket connection on port 9002.
This continuously captures frames from the video and sends them to the websocket client.
A resolution of 1920x1080 is chosen for performance reasons."""
vid = cv2.VideoCapture(0,cv2.CAP_V4L)
vid.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
vid.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
error_amount = 0
while True:
try:
while(vid.isOpened()):
img, frame = vid.read()
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 100]
man = cv2.imencode('.jpg', frame)[1]
await websocket.send(man.tobytes())
self.get_logger().error("Not opened")
error_amount += 1
except Exception as e:
self.get_logger().error("error " + str(e))
error_amount += 1
if error_amount > 20:
self.get_logger().error("Too many errors, closing node")
self.should_exit = True
sys.exit(-1)
def main(args=None):
rclpy.init(args=args)

View File

@@ -11,9 +11,6 @@
<exec_depend>drone_services</exec_depend>
<exec_depend>sensor_msgs</exec_depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>

View File

@@ -0,0 +1,72 @@
import os
import sys
import unittest
import time
import launch
import launch_ros
import launch_ros.actions
import launch_testing.actions
import pytest
import rclpy
from drone_services.srv import TakePicture
@pytest.mark.rostest
def generate_test_description():
file_path = os.path.dirname(__file__)
camera_node = launch_ros.actions.Node(
executable=sys.executable,
arguments=[os.path.join(file_path, '..', 'camera', 'camera_controller.py')],
additional_env={'PYTHONUNBUFFERED': '1'}
)
return (
launch.LaunchDescription([
camera_node,
launch_testing.actions.ReadyToTest(),
]),
{
'camera_node': camera_node,
}
)
class CameraUnitTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
rclpy.init()
@classmethod
def tearDownClass(cls):
rclpy.shutdown()
def setUp(self):
self.node = rclpy.create_node('test_camera')
self.service_called = False
def tearDown(self):
self.node.destroy_node()
def service_call_callback(self,future):
self.assertIsNotNone(future.result())
self.assertEqual(future.result().filename, "/home/ubuntu/test_image.jpg")
self.assertTrue(os.path.exists("/home/ubuntu/test_image.jpg"))
self.service_called = True
def test_camera_save_image(self,camera_node,proc_output):
# call camera service
camera_client = self.node.create_client(TakePicture, '/drone/picture')
while not camera_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('camera service not available, waiting again...')
request = TakePicture.Request()
request.input_name = "/home/ubuntu/test_image.jpg"
try:
while True:
rclpy.spin_once(self.node,timeout_sec=0.1)
if not self.service_called:
camera_client.call_async(request).add_done_callback(self.service_call_callback)
else:
break
finally:
self.node.destroy_client(camera_client)

View File

@@ -1,23 +0,0 @@
# Copyright 2015 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ament_copyright.main import main
import pytest
@pytest.mark.copyright
@pytest.mark.linter
def test_copyright():
rc = main(argv=['.', 'test'])
assert rc == 0, 'Found errors'

View File

@@ -1,25 +0,0 @@
# Copyright 2017 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ament_flake8.main import main_with_errors
import pytest
@pytest.mark.flake8
@pytest.mark.linter
def test_flake8():
rc, errors = main_with_errors(argv=[])
assert rc == 0, \
'Found %d code style errors / warnings:\n' % len(errors) + \
'\n'.join(errors)

View File

@@ -1,23 +0,0 @@
# Copyright 2015 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ament_pep257.main import main
import pytest
@pytest.mark.linter
@pytest.mark.pep257
def test_pep257():
rc = main(argv=['.', 'test'])
assert rc == 0, 'Found code style errors / warnings'

View File

@@ -2,11 +2,12 @@ cmake_minimum_required(VERSION 3.8)
project(drone_controls)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# find dependencies
find_package(ament_cmake REQUIRED)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)
@@ -25,15 +26,18 @@ install(TARGETS position_changer
DESTINATION lib/${PROJECT_NAME})
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# comment the line when a copyright and license is added to all source files
set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# comment the line when this package is in a git repo and when
# a copyright and license is added to all source files
set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
# find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# comment the line when a copyright and license is added to all source files
# set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# comment the line when this package is in a git repo and when
# a copyright and license is added to all source files
# set(ament_cmake_cpplint_FOUND TRUE)
# ament_lint_auto_find_test_dependencies()
find_package(launch_testing_ament_cmake REQUIRED)
add_launch_test(test/test_positionchanger.py)
endif()
ament_package()

View File

@@ -5,7 +5,11 @@
#include <drone_services/msg/height_data.hpp>
#include <drone_services/srv/set_vehicle_control.hpp>
#include <drone_services/srv/set_trajectory.hpp>
#include <drone_services/srv/set_attitude.hpp>
#include <drone_services/srv/move_position.hpp>
#include <drone_services/srv/ready_drone.hpp>
#include <drone_services/srv/arm_drone.hpp>
#include <drone_services/srv/land.hpp>
#include <drone_services/srv/enable_failsafe.hpp>
#include <drone_services/msg/failsafe_msg.hpp>
@@ -25,10 +29,13 @@
#define MIN_DISTANCE 1.0 // in meters
#define CONTROL_MODE_ATTITUDE 4 // attitude control mode bitmask
#define DEFAULT_CONTROL_MODE 16 // default velocity control bitmask
// converts bitmask control mode to control mode used by PX4Controller
#define PX4_CONTROLLER_CONTROL_MODE(x) ((x) == 4 ? 1 : ((x) == 16 ? 2 : ((x) == 32 ? 3 : -1)))
#define HEIGHT_DELTA 0.1
using namespace std::chrono_literals;
struct Quaternion
@@ -45,26 +52,36 @@ public:
auto qos = rclcpp::QoS(rclcpp::QoSInitialization(qos_profile.history, 5), qos_profile);
this->lidar_subscription = this->create_subscription<drone_services::msg::LidarReading>("/drone/object_detection", qos, std::bind(&PositionChanger::handle_lidar_message, this, std::placeholders::_1));
// vehicle_local_position_subscription_ = this->create_subscription<px4_msgs::msg::VehicleLocalPosition>("/fmu/out/vehicle_local_position", qos, std::bind(&PX4Controller::on_local_position_receive, this, std::placeholders::_1));
this->height_subscription = this->create_subscription<drone_services::msg::HeightData>("/drone/height", qos, std::bind(&PositionChanger::handle_height_message, this, std::placeholders::_1));
this->odometry_subscription = this->create_subscription<px4_msgs::msg::VehicleOdometry>("/fmu/out/vehicle_odometry", qos, std::bind(&PositionChanger::handle_odometry_message, this, std::placeholders::_1));
this->move_position_service = this->create_service<drone_services::srv::MovePosition>("/drone/move_position", std::bind(&PositionChanger::handle_move_position_request, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
this->ready_drone_service = this->create_service<drone_services::srv::ReadyDrone>("/drone/ready", std::bind(&PositionChanger::handle_ready_drone_request, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
this->land_service = this->create_service<drone_services::srv::Land>("/drone/land", std::bind(&PositionChanger::handle_land_request, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
this->failsafe_publisher = this->create_publisher<drone_services::msg::FailsafeMsg>("/drone/failsafe", 10);
this->trajectory_client = this->create_client<drone_services::srv::SetTrajectory>("/drone/set_trajectory");
this->attitude_client = this->create_client<drone_services::srv::SetAttitude>("/drone/set_attitude");
this->vehicle_control_client = this->create_client<drone_services::srv::SetVehicleControl>("/drone/set_vehicle_control");
this->failsafe_client = this->create_client<drone_services::srv::EnableFailsafe>("/drone/enable_failsafe");
this->arm_drone_client = this->create_client<drone_services::srv::ArmDrone>("/drone/arm");
RCLCPP_INFO(this->get_logger(), "waiting for trajectory service...");
wait_for_service<rclcpp::Client<drone_services::srv::SetTrajectory>::SharedPtr>(this->trajectory_client);
wait_for_service<rclcpp::Client<drone_services::srv::SetTrajectory>::SharedPtr>(this->trajectory_client, "/drone/set_trajectory");
RCLCPP_INFO(this->get_logger(), "waiting for attitude service...");
wait_for_service<rclcpp::Client<drone_services::srv::SetAttitude>::SharedPtr>(this->attitude_client, "/drone/set_attitude");
RCLCPP_INFO(this->get_logger(), "waiting for vehicle control service...");
wait_for_service<rclcpp::Client<drone_services::srv::SetVehicleControl>::SharedPtr>(this->vehicle_control_client);
wait_for_service<rclcpp::Client<drone_services::srv::SetVehicleControl>::SharedPtr>(this->vehicle_control_client, "/drone/set_vehicle_control");
RCLCPP_INFO(this->get_logger(), "waiting for failsafe service...");
wait_for_service<rclcpp::Client<drone_services::srv::EnableFailsafe>::SharedPtr>(this->failsafe_client);
wait_for_service<rclcpp::Client<drone_services::srv::EnableFailsafe>::SharedPtr>(this->failsafe_client, "/drone/enable_failsafe");
RCLCPP_INFO(this->get_logger(), "waiting for arm service...");
wait_for_service<rclcpp::Client<drone_services::srv::ArmDrone>::SharedPtr>(this->arm_drone_client, "/drone/arm");
this->trajectory_request = std::make_shared<drone_services::srv::SetTrajectory::Request>();
this->attitude_request = std::make_shared<drone_services::srv::SetAttitude::Request>();
this->vehicle_control_request = std::make_shared<drone_services::srv::SetVehicleControl::Request>();
this->failsafe_request = std::make_shared<drone_services::srv::EnableFailsafe::Request>();
this->arm_drone_request = std::make_shared<drone_services::srv::ArmDrone::Request>();
lidar_health_timer = this->create_wall_timer(1s, std::bind(&PositionChanger::check_lidar_health, this));
@@ -84,6 +101,57 @@ public:
if (status == std::future_status::ready)
{
RCLCPP_INFO(this->get_logger(), "Vehicle control mode set result: %d", future.get()->success);
if (this->got_ready_drone_request && future.get()->success)
{
auto arm_response = this->arm_drone_client->async_send_request(this->arm_drone_request, std::bind(&PositionChanger::arm_drone_service_callback, this, std::placeholders::_1));
}
}
else
{
RCLCPP_INFO(this->get_logger(), "Service In-Progress...");
}
}
/**
* @brief callback function for the /drone/arm service
*
* @param future the future object that holds the result of the service call
*/
void arm_drone_service_callback(rclcpp::Client<drone_services::srv::ArmDrone>::SharedFuture future)
{
auto status = future.wait_for(1s);
if (status == std::future_status::ready)
{
RCLCPP_INFO(this->get_logger(), "Arm result: %d", future.get()->success);
if (this->got_ready_drone_request)
{
this->got_ready_drone_request = false;
this->attitude_request->pitch = 0.0;
this->attitude_request->yaw = 0.0;
this->attitude_request->roll = 0.0;
this->attitude_request->thrust = 0.15;
auto attitude_response = this->attitude_client->async_send_request(this->attitude_request, std::bind(&PositionChanger::attitude_message_callback, this, std::placeholders::_1));
}
}
else
{
RCLCPP_INFO(this->get_logger(), "Service In-Progress...");
}
}
/**
* @brief callback function for the /drone/set_attitude service
*
* @param future the future object that holds the result of the service call
*/
void attitude_message_callback(rclcpp::Client<drone_services::srv::SetAttitude>::SharedFuture future)
{
auto status = future.wait_for(1s);
if (status == std::future_status::ready)
{
RCLCPP_INFO(this->get_logger(), "Attitude set result: %d", future.get()->success);
}
else
{
@@ -133,7 +201,6 @@ public:
*/
void send_trajectory_message()
{
check_move_direction_allowed();
this->trajectory_request->values[0] = this->current_speed_x;
this->trajectory_request->values[1] = this->current_speed_y;
this->trajectory_request->values[2] = this->current_speed_z;
@@ -145,10 +212,10 @@ public:
/**
* @brief Enables the failsafe with the specified message
*
*
* @param message the message indicating the cause of the failsafe
*/
void enable_failsafe(std::string message)
void enable_failsafe(std::u16string message)
{
this->failsafe_enabled = true;
this->failsafe_request->message = message;
@@ -162,33 +229,25 @@ public:
*/
void apply_collision_weights()
{
if (this->current_speed_x > 0) // if moving forward
if (!this->move_direction_allowed[MOVE_DIRECTION_FRONT])
{
if (!this->move_direction_allowed[MOVE_DIRECTION_FRONT])
{
this->current_speed_x = collision_prevention_weights[MOVE_DIRECTION_FRONT];
}
RCLCPP_INFO(this->get_logger(), "Collision prevention front: %f", collision_prevention_weights[MOVE_DIRECTION_FRONT]);
get_x_y_with_rotation(collision_prevention_weights[MOVE_DIRECTION_FRONT], 0, this->current_yaw, &this->current_speed_x, &this->current_speed_y);
}
else // moving backward
if (!this->move_direction_allowed[MOVE_DIRECTION_BACK])
{
if (!this->move_direction_allowed[MOVE_DIRECTION_BACK])
{
this->current_speed_x = collision_prevention_weights[MOVE_DIRECTION_BACK];
}
RCLCPP_INFO(this->get_logger(), "Collision prevention back: %f", collision_prevention_weights[MOVE_DIRECTION_BACK]);
get_x_y_with_rotation(collision_prevention_weights[MOVE_DIRECTION_BACK], 0, this->current_yaw, &this->current_speed_x, &this->current_speed_y);
}
if (this->current_speed_y > 0) // moving right
if (!this->move_direction_allowed[MOVE_DIRECTION_RIGHT])
{
if (!this->move_direction_allowed[MOVE_DIRECTION_RIGHT])
{
this->current_speed_y = collision_prevention_weights[MOVE_DIRECTION_RIGHT];
}
RCLCPP_INFO(this->get_logger(), "Collision prevention right: %f", collision_prevention_weights[MOVE_DIRECTION_RIGHT]);
get_x_y_with_rotation(0, collision_prevention_weights[MOVE_DIRECTION_RIGHT], this->current_yaw, &this->current_speed_x, &this->current_speed_y);
}
else // moving left
if (!this->move_direction_allowed[MOVE_DIRECTION_LEFT])
{
if (!this->move_direction_allowed[MOVE_DIRECTION_LEFT])
{
this->current_speed_y = collision_prevention_weights[MOVE_DIRECTION_LEFT];
}
RCLCPP_INFO(this->get_logger(), "Collision prevention left: %f", collision_prevention_weights[MOVE_DIRECTION_LEFT]);
get_x_y_with_rotation(0, collision_prevention_weights[MOVE_DIRECTION_LEFT], this->current_yaw, &this->current_speed_x, &this->current_speed_y);
}
}
@@ -212,10 +271,80 @@ public:
: (MIN_DISTANCE - std::min(this->lidar_sensor_values[LIDAR_SENSOR_RL], this->lidar_sensor_values[LIDAR_SENSOR_RR]));
collision_prevention_weights[MOVE_DIRECTION_RIGHT] = this->move_direction_allowed[MOVE_DIRECTION_RIGHT] ? 0
: -(MIN_DISTANCE - std::min(this->lidar_sensor_values[LIDAR_SENSOR_RR], this->lidar_sensor_values[LIDAR_SENSOR_FR]));
apply_collision_weights();
}
/**
* @brief Callback for when a set_attitude request is made while landing
*
* @param future the future object containing the result of the request.
*/
void attitude_land_callback(rclcpp::Client<drone_services::srv::SetAttitude>::SharedFuture future)
{
auto status = future.wait_for(1s);
if (status == std::future_status::ready)
{
if (future.get()->success)
{
RCLCPP_INFO(this->get_logger(), "Attitude set to 0 for landing, landing done");
this->has_landed = true;
}
}
else
{
RCLCPP_INFO(this->get_logger(), "Service In-Progress...");
}
}
/**
* @brief Callback for when a set_vehicle_control request is made while landing
*
* @param future the future object containing the result of the request.
*/
void vehicle_control_land_request_callback(rclcpp::Client<drone_services::srv::SetVehicleControl>::SharedFuture future)
{
auto status = future.wait_for(1s);
if (status == std::future_status::ready)
{
if (future.get()->success)
{
RCLCPP_INFO(this->get_logger(), "Vehicle Control mode set to attitude for landing");
this->attitude_request->pitch = 0;
this->attitude_request->roll = 0;
this->attitude_request->yaw = 0;
this->attitude_request->thrust = 0;
auto attitude_response = this->attitude_client->async_send_request(this->attitude_request, std::bind(&PositionChanger::attitude_land_callback, this, std::placeholders::_1));
}
}
else
{
RCLCPP_INFO(this->get_logger(), "Service In-Progress...");
}
}
/**
* @brief Callback method for receiving new height data
*
* @param message the message containing the height data
*/
void handle_height_message(const drone_services::msg::HeightData::SharedPtr message)
{
this->current_height = message->min_height;
if (!this->got_ready_drone_request)
{
this->start_height = message->min_height;
}
if (this->is_landing)
{
if (this->current_height <= this->start_height + HEIGHT_DELTA)
{
this->vehicle_control_request->control = 4;
auto control_mode_response = this->vehicle_control_client->async_send_request(this->vehicle_control_request,
std::bind(&PositionChanger::vehicle_control_land_request_callback, this, std::placeholders::_1));
}
}
}
/**
* @brief Callback function for receiving new LIDAR data
*
@@ -223,6 +352,7 @@ public:
*/
void handle_lidar_message(const drone_services::msg::LidarReading::SharedPtr message)
{
this->has_received_first_lidar_message = true;
this->received_lidar_message = true;
if (message->sensor_1 > 0)
@@ -252,14 +382,17 @@ public:
/**
* @brief Checks if the LIDAR is still sending messages. If not, enable failsafe
*
*
*/
void check_lidar_health()
{
if (!this->received_lidar_message)
if (this->has_received_first_lidar_message)
{
RCLCPP_WARN(this->get_logger(), "Lidar not sending messages, enabling failsafe");
enable_failsafe("No healthy connection to LIDAR!");
if (!this->received_lidar_message)
{
RCLCPP_WARN(this->get_logger(), "Lidar not sending messages, enabling failsafe");
enable_failsafe(u"No healthy connection to LIDAR! Check the LIDAR USB cable and restart the drone.");
}
}
this->received_lidar_message = false;
}
@@ -291,6 +424,11 @@ public:
{
if (!this->failsafe_enabled)
{
if (!this->has_received_first_lidar_message)
{
this->enable_failsafe(u"Waiting for LIDAR timed out! Check the LIDAR USB connection and consider restarting the drone.");
return;
}
RCLCPP_INFO(this->get_logger(), "Incoming request\nfront_back: %f\nleft_right: %f\nup_down: %f\nangle: %f", request->front_back, request->left_right, request->up_down, request->angle);
if (request->angle > 360 || request->angle < -360)
{
@@ -298,17 +436,72 @@ public:
response->success = false;
return;
}
if (this->vehicle_control_request->control != DEFAULT_CONTROL_MODE)
{
this->vehicle_control_request->control = DEFAULT_CONTROL_MODE;
auto control_mode_response = this->vehicle_control_client->async_send_request(this->vehicle_control_request,
std::bind(&PositionChanger::vehicle_control_service_callback, this, std::placeholders::_1));
}
this->current_yaw += request->angle * (M_PI / 180.0); // get the angle in radians
this->current_speed_z = request->up_down;
get_x_y_with_rotation(request->front_back, request->left_right, this->current_yaw, &this->current_speed_x, &this->current_speed_y);
check_move_direction_allowed();
RCLCPP_INFO(this->get_logger(), "Calculated speed x: %f, y: %f", this->current_speed_x, this->current_speed_y);
send_trajectory_message();
response->success = true;
} else {
}
else
{
response->success = false;
}
}
/**
* @brief Callback function for when the /drone/land service is called
*
* @param request_header the header of the request
* @param request the request to land
* @param response the response to send back.
*/
void handle_land_request(
const std::shared_ptr<rmw_request_id_t> request_header,
const std::shared_ptr<drone_services::srv::Land::Request> request,
const std::shared_ptr<drone_services::srv::Land::Response> response)
{
this->is_landing = true;
response->is_landing = this->is_landing;
}
/**
* @brief Callback function for when the /drone/ready_drone service is called
*
* @param request_header the header of the request
* @param request the request to ready the drone
* @param response the response to send back. Contains true if the request was successful, false otherwise
*/
void handle_ready_drone_request(
const std::shared_ptr<rmw_request_id_t> request_header,
const std::shared_ptr<drone_services::srv::ReadyDrone::Request> request,
const std::shared_ptr<drone_services::srv::ReadyDrone::Response> response)
{
if (this->failsafe_enabled)
{
response->success = false;
return;
}
if (!this->has_received_first_lidar_message)
{
this->enable_failsafe(u"Waiting for LIDAR timed out! Check the LIDAR USB connection and consider restarting the drone.");
response->success = false;
return;
}
this->got_ready_drone_request = true;
this->vehicle_control_request->control = CONTROL_MODE_ATTITUDE;
auto control_mode_response = this->vehicle_control_client->async_send_request(this->vehicle_control_request,
std::bind(&PositionChanger::vehicle_control_service_callback, this, std::placeholders::_1));
response->success = true;
}
/**
* @brief Get the yaw angle from a specified normalized quaternion.
* Uses the theory from https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles
@@ -343,18 +536,24 @@ private:
rclcpp::Publisher<drone_services::msg::FailsafeMsg>::SharedPtr failsafe_publisher;
rclcpp::Subscription<drone_services::msg::LidarReading>::SharedPtr lidar_subscription;
rclcpp::Subscription<px4_msgs::msg::VehicleOdometry>::SharedPtr odometry_subscription;
rclcpp::Subscription<drone_services::msg::HeightData>::SharedPtr height_subscription;
rclcpp::Client<drone_services::srv::SetTrajectory>::SharedPtr trajectory_client;
rclcpp::Client<drone_services::srv::SetAttitude>::SharedPtr attitude_client;
rclcpp::Client<drone_services::srv::SetVehicleControl>::SharedPtr vehicle_control_client;
rclcpp::Client<drone_services::srv::EnableFailsafe>::SharedPtr failsafe_client;
rclcpp::Client<drone_services::srv::ArmDrone>::SharedPtr arm_drone_client;
rclcpp::Service<drone_services::srv::MovePosition>::SharedPtr move_position_service;
rclcpp::Service<drone_services::srv::ReadyDrone>::SharedPtr ready_drone_service;
rclcpp::Service<drone_services::srv::Land>::SharedPtr land_service;
rclcpp::TimerBase::SharedPtr lidar_health_timer;
drone_services::srv::SetTrajectory::Request::SharedPtr trajectory_request;
drone_services::srv::SetAttitude::Request::SharedPtr attitude_request;
drone_services::srv::SetVehicleControl::Request::SharedPtr vehicle_control_request;
drone_services::srv::EnableFailsafe::Request::SharedPtr failsafe_request;
drone_services::srv::ArmDrone::Request::SharedPtr arm_drone_request;
float lidar_sensor_values[4]{MIN_DISTANCE}; // last distance measured by the lidar sensors
float lidar_imu_readings[4]; // last imu readings from the lidar sensors
@@ -362,10 +561,17 @@ private:
float current_speed_x = 0;
float current_speed_y = 0;
float current_speed_z = 0;
float current_height = 0;
float start_height = -1;
bool is_landing = false;
bool has_landed = false;
bool move_direction_allowed[4] = {true}; // whether the drone can move in a certain direction
float collision_prevention_weights[4] = {0}; // the amount to move away from an object in a certain direction if the drone is too close
bool failsafe_enabled = false;
bool received_lidar_message = false;
int lidar_health_checks = 0;
bool has_received_first_lidar_message = false;
bool got_ready_drone_request = false;
/**
* @brief waits for a service to be available
@@ -374,7 +580,7 @@ private:
* @param client the client object to wait for the service
*/
template <class T>
void wait_for_service(T client)
void wait_for_service(T client, std::string service_name)
{
while (!client->wait_for_service(1s))
{
@@ -383,7 +589,7 @@ private:
RCLCPP_ERROR(this->get_logger(), "Interrupted while waiting for the service. Exiting.");
return;
}
RCLCPP_INFO(this->get_logger(), "service not available, waiting again...");
RCLCPP_INFO(this->get_logger(), "service not available, waiting again on service %s", service_name.c_str());
}
}
};

View File

@@ -0,0 +1,164 @@
import unittest
import launch
import launch_ros
import launch_ros.actions
import launch_testing.actions
import pytest
import rclpy
import time
from drone_services.srv import MovePosition
from drone_services.msg import FailsafeMsg
from drone_services.msg import LidarReading
@pytest.mark.rostest
def generate_test_description():
positionchanger_node = launch_ros.actions.Node(
package='drone_controls', executable='position_changer')
failsafe_node = launch_ros.actions.Node(
package='failsafe', executable='failsafe')
px4_controller_node = launch_ros.actions.Node(
package='px4_connection', executable='px4_controller')
heartbeat_node = launch_ros.actions.Node(
package='px4_connection', executable='heartbeat')
return (
launch.LaunchDescription([
positionchanger_node,
failsafe_node,
px4_controller_node,
heartbeat_node,
launch_testing.actions.ReadyToTest(),
]),
{
'positionchanger_node': positionchanger_node,
'failsafe_node': failsafe_node,
'px4_controller_node': px4_controller_node,
'heartbeat_node': heartbeat_node
}
)
class TestPositionChanger(unittest.TestCase):
@classmethod
def setUpClass(cls):
rclpy.init()
@classmethod
def tearDownClass(cls):
rclpy.shutdown()
def setUp(self):
self.node = rclpy.create_node('test_positionchanger')
self.called_positionchanger_service = False
self.received_failsafe_callback = False
def tearDown(self):
self.node.destroy_node()
def failsafe_callback(self, msg):
self.assertTrue(msg.enabled, "Failsafe was not enabled!")
self.received_failsafe_callback = True
def move_position_callback(self, future):
self.assertFalse(future.result(
).success, "MovePosition service call was successful, but should have failed!")
self.called_positionchanger_service = True
def test_positionchanger_no_lidar_data(self, positionchanger_node, proc_output):
self.received_failsafe_callback = False
self.called_positionchanger_service = False
failsafe_subscriber = self.node.create_subscription(
FailsafeMsg, '/drone/failsafe', self.failsafe_callback, 10)
move_position_client = self.node.create_client(
MovePosition, '/drone/move_position')
while not move_position_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('move_position service not available, waiting again...')
request = MovePosition.Request()
request.front_back = 1.0
request.left_right = 1.0
request.up_down = 1.0
request.angle = 1.0
end_time = time.time() + 10.0
try:
while time.time() < end_time:
rclpy.spin_once(self.node)
if not self.called_positionchanger_service:
future = move_position_client.call_async(request)
future.add_done_callback(self.move_position_callback)
elif not self.received_failsafe_callback:
continue
else:
break
self.assertTrue(self.received_failsafe_callback,
"Failsafe callback was not received!")
self.assertTrue(self.called_positionchanger_service,
"MovePosition service was not called!")
finally:
self.node.destroy_client(move_position_client)
self.node.destroy_subscription(failsafe_subscriber)
def test_positionchanger_lidar_stops(self, positionchanger_node, proc_output):
self.node.get_logger().info("STARTING TEST test_positionchanger_lidar_stops")
self.received_failsafe_callback = False
self.called_positionchanger_service = False
failsafe_subscriber = self.node.create_subscription(
FailsafeMsg, '/drone/failsafe', self.failsafe_callback, 10)
lidar_publisher = self.node.create_publisher(
LidarReading, '/drone/object_detection', 10)
move_position_client = self.node.create_client(
MovePosition, '/drone/move_position')
while not move_position_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('move_position service not available, waiting again...')
request = MovePosition.Request()
request.front_back = 1.0
request.left_right = 1.0
request.up_down = 1.0
request.angle = 1.0
lidar_msg = LidarReading()
lidar_msg.sensor_1 = 2.0
lidar_msg.sensor_2 = 2.0
lidar_msg.sensor_3 = 2.0
lidar_msg.sensor_4 = 2.0
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
sent_lidar_msg = False
# wait for nodes to become active
time.sleep(3)
# wait 5 seconds for the failsafe to trigger
wait_time = time.time() + 5.0
end_time = time.time() + 10.0
try:
self.node.get_logger().info('STARTING WHILE LOOP')
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
if not sent_lidar_msg:
lidar_publisher.publish(lidar_msg)
sent_lidar_msg = True
# wait 5 seconds before sending the move_position request
if not self.called_positionchanger_service and time.time() > wait_time:
self.node.get_logger().info('Sending move_position request')
future = move_position_client.call_async(request)
future.add_done_callback(self.move_position_callback)
elif not self.received_failsafe_callback:
continue
self.node.get_logger().info('END OF WHILE LOOP')
self.assertTrue(self.called_positionchanger_service,
"MovePosition service was not called!")
self.assertTrue(self.received_failsafe_callback,
"Failsafe was not activated!")
finally:
self.node.get_logger().info('Cleaning up')
self.node.destroy_client(move_position_client)
self.node.destroy_subscription(failsafe_subscriber)
self.node.destroy_publisher(lidar_publisher)

View File

@@ -0,0 +1,158 @@
import os
import sys
import unittest
import launch
import launch_ros
import launch_ros.actions
import launch_testing.actions
import pytest
import rclpy
import time
from drone_services.srv import MovePosition
from drone_services.srv import ArmDrone
from drone_services.srv import Land
from drone_services.msg import HeightData
@pytest.mark.rostest
def generate_test_description():
file_path = os.path.dirname(__file__)
# device under test
positionchanger_node = launch_ros.actions.Node(
package='drone_controls', executable='position_changer')
failsafe_node = launch_ros.actions.Node(
package='failsafe', executable='failsafe')
px4_controller_node = launch_ros.actions.Node(
package='px4_connection', executable='px4_controller')
heartbeat_node = launch_ros.actions.Node(
package='px4_connection', executable='heartbeat')
return (
launch.LaunchDescription([
positionchanger_node,
failsafe_node,
px4_controller_node,
heartbeat_node,
launch_testing.actions.ReadyToTest(),
]),
{
'positionchanger_node': positionchanger_node,
'failsafe_node': failsafe_node,
'px4_controller_node': px4_controller_node,
'heartbeat_node': heartbeat_node
}
)
class TestPositionChanger(unittest.TestCase):
@classmethod
def setUpClass(cls):
rclpy.init()
@classmethod
def tearDownClass(cls):
rclpy.shutdown()
def setUp(self):
self.node = rclpy.create_node('test_positionchanger')
self.called_arm_service = False
self.called_land_service = False
self.called_move_service_up = False
self.called_move_service_stop = False
self.moved_up = False
def tearDown(self):
self.node.destroy_node()
def move_position_callback_up(self, future):
self.node.get_logger().info("Callback called")
# self.assertTrue(future.result().success, "Move service failed")
self.called_move_service_up = True
def move_position_callback_stop(self, future):
self.node.get_logger().info("Callback called")
# self.assertTrue(future.result().success, "Move service failed")
self.called_move_service_stop = True
def arm_callback(self, future):
self.node.get_logger().info("Arm Callback called")
# self.assertTrue(future.result().success, "Arm service failed")
self.called_arm_service = True
def land_callback(self,future):
self.node.get_logger().info("Land Callback called")
self.assertTrue(future.result().is_landing, "Drone is not landing")
self.called_land_service = True
def test_positionchanger_land(self, proc_output):
self.node.get_logger().info("starting land test")
height_data_publisher = self.node.create_publisher(HeightData, '/drone/height', 10)
height_msg = HeightData()
height_msg.heights = [0.2,0.2,0.2,0.2]
height_msg.min_height = 0.2
arm_client = self.node.create_client(ArmDrone, '/drone/arm')
while not arm_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('arm service not available, waiting again...')
land_client = self.node.create_client(Land, '/drone/land')
while not land_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('land service not available, waiting again...')
move_client = self.node.create_client(MovePosition, '/drone/move_position')
while not move_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('move_position service not available, waiting again...')
arm_request = ArmDrone.Request()
land_request = Land.Request()
move_request = MovePosition.Request()
move_request.front_back = 0.0
move_request.left_right = 0.0
move_request.up_down = 5.0
move_request.angle = 0.0
start_height_msgs_published = 0
moving_height_msgs_published = 0
landing_height_msgs_published = 0
end_time = time.time() + 20.0
try:
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
if start_height_msgs_published < 10:
height_data_publisher.publish(height_msg)
start_height_msgs_published += 1
elif not self.called_arm_service:
arm_future = arm_client.call_async(arm_request)
arm_future.add_done_callback(self.arm_callback)
elif not self.called_move_service_up:
move_future = move_client.call_async(move_request)
move_future.add_done_callback(self.move_position_callback_up)
elif moving_height_msgs_published < 10:
height_msg.min_height += 0.1
height_data_publisher.publish(height_msg)
moving_height_msgs_published += 1
elif not self.called_move_service_stop:
move_request.up_down = 0.0
move_future = move_client.call_async(move_request)
move_future.add_done_callback(self.move_position_callback_stop)
elif not self.called_land_service:
land_future = land_client.call_async(land_request)
land_future.add_done_callback(self.land_callback)
elif landing_height_msgs_published < 10:
height_msg.min_height -= 0.15
height_data_publisher.publish(height_msg)
landing_height_msgs_published += 1
launch_testing.asserts.assertInStderr(proc_output, "Attitude set to 0 for landing, landing done", 'position_changer-1')
launch_testing.asserts.assertInStderr(proc_output, "Vehicle Control mode set to attitude for landing", 'position_changer-1')
finally:
self.node.get_logger().info("shutting down")
self.node.destroy_client(arm_client)
self.node.destroy_client(land_client)
self.node.destroy_client(move_client)
self.node.destroy_publisher(height_data_publisher)
#publish height data as start value
#arm
#move up
#hang still
#land
#check if landed

View File

@@ -0,0 +1,240 @@
import os
import unittest
import launch
import launch_ros
import launch_ros.actions
import launch_testing.actions
import pytest
import rclpy
import time
from drone_services.srv import MovePosition
from drone_services.msg import LidarReading
@pytest.mark.rostest
def generate_test_description():
file_path = os.path.dirname(__file__)
# device under test
positionchanger_node = launch_ros.actions.Node(
package='drone_controls', executable='position_changer')
failsafe_node = launch_ros.actions.Node(
package='failsafe', executable='failsafe')
px4_controller_node = launch_ros.actions.Node(
package='px4_connection', executable='px4_controller')
heartbeat_node = launch_ros.actions.Node(
package='px4_connection', executable='heartbeat')
return (
launch.LaunchDescription([
positionchanger_node,
failsafe_node,
px4_controller_node,
heartbeat_node,
launch_testing.actions.ReadyToTest(),
]),
{
'positionchanger_node': positionchanger_node,
'failsafe_node': failsafe_node,
'px4_controller_node': px4_controller_node,
'heartbeat_node': heartbeat_node
}
)
class TestPositionChanger(unittest.TestCase):
@classmethod
def setUpClass(cls):
rclpy.init()
@classmethod
def tearDownClass(cls):
rclpy.shutdown()
def setUp(self):
self.node = rclpy.create_node('test_positionchanger')
self.called_positionchanger_service = False
self.received_failsafe_callback = False
self.lidar_publisher = self.node.create_publisher(
LidarReading, '/drone/object_detection', 10)
self.move_position_client = self.node.create_client(
MovePosition, '/drone/move_position')
while not self.move_position_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('move_position service not available, waiting again...')
self.request = MovePosition.Request()
def tearDown(self):
self.node.destroy_client(self.move_position_client)
self.node.destroy_publisher(self.lidar_publisher)
self.node.destroy_node()
def toRadians(self, degrees) -> float:
return degrees * 3.14159265359 / 180
def move_position_callback(self, future):
self.node.get_logger().info("Callback called")
self.called_positionchanger_service = True
def test_positionchanger_lidar_moves_away_front(self, proc_output):
self.node.get_logger().info("starting front test")
self.request.front_back = 1.0
self.request.left_right = 0.0
self.request.up_down = 0.0
self.request.angle = 0.0
lidar_msgs_sent = 0
lidar_msg = LidarReading()
lidar_msg.sensor_1 = 0.5 # front right
lidar_msg.sensor_2 = 2.0 # front left
lidar_msg.sensor_3 = 2.0 # rear left
lidar_msg.sensor_4 = 2.0 # rear right
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
end_time = time.time() + 10.0
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
self.lidar_publisher.publish(lidar_msg)
lidar_msgs_sent += 1
if (lidar_msgs_sent == 10):
lidar_msg.sensor_1 = 1.0
lidar_msg.sensor_2 = 0.3
elif (lidar_msgs_sent == 20):
break
if not self.called_positionchanger_service:
future = self.move_position_client.call_async(self.request)
future.add_done_callback(self.move_position_callback)
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention front: -0.5", 'position_changer-1')
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention front: -0.7", 'position_changer-1')
def test_positionchanger_lidar_moves_away_right(self, proc_output):
self.node.get_logger().info("starting right test")
self.request.front_back = 0.0
self.request.left_right = 1.0
self.request.up_down = 0.0
self.request.angle = 0.0
lidar_msgs_sent = 0
lidar_msg = LidarReading()
lidar_msg.sensor_1 = 0.4 # front right
lidar_msg.sensor_2 = 2.0 # front left
lidar_msg.sensor_3 = 2.0 # rear left
lidar_msg.sensor_4 = 2.0 # rear right
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
end_time = time.time() + 10.0
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
self.lidar_publisher.publish(lidar_msg)
lidar_msgs_sent += 1
if (lidar_msgs_sent == 10):
lidar_msg.sensor_1 = 2.0
lidar_msg.sensor_4 = 0.29
elif (lidar_msgs_sent == 20):
break
if not self.called_positionchanger_service:
future = self.move_position_client.call_async(self.request)
future.add_done_callback(self.move_position_callback)
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention right: -0.6", 'position_changer-1')
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention right: -0.71", 'position_changer-1')
def test_positionchanger_lidar_moves_away_left(self, proc_output):
self.node.get_logger().info("starting left test")
self.request.front_back = 0.0
self.request.left_right = -1.0
self.request.up_down = 0.0
self.request.angle = 0.0
lidar_msgs_sent = 0
lidar_msg = LidarReading()
lidar_msg.sensor_1 = 2.0 # front right
lidar_msg.sensor_2 = 0.11 # front left
lidar_msg.sensor_3 = 2.0 # rear left
lidar_msg.sensor_4 = 2.0 # rear right
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
end_time = time.time() + 10.0
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
self.lidar_publisher.publish(lidar_msg)
lidar_msgs_sent += 1
if (lidar_msgs_sent == 10):
lidar_msg.sensor_2 = 2.0
lidar_msg.sensor_3 = 0.78
elif (lidar_msgs_sent == 20):
break
if not self.called_positionchanger_service:
future = self.move_position_client.call_async(self.request)
future.add_done_callback(self.move_position_callback)
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention left: 0.89", 'position_changer-1')
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention left: 0.22", 'position_changer-1')
def test_positionchanger_lidar_moves_away_back(self, proc_output):
self.node.get_logger().info("starting back test")
self.request.front_back = -1.0
self.request.left_right = 0.0
self.request.up_down = 0.0
self.request.angle = 0.0
lidar_msgs_sent = 0
lidar_msg = LidarReading()
lidar_msg.sensor_1 = 2.0 # front right
lidar_msg.sensor_2 = 2.0 # front left
lidar_msg.sensor_3 = 0.36 # rear left
lidar_msg.sensor_4 = 2.0 # rear right
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
#this test is carried out first, so wait for nodes to start
time.sleep(5)
end_time = time.time() + 20.0
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
self.lidar_publisher.publish(lidar_msg)
lidar_msgs_sent += 1
if (lidar_msgs_sent == 10):
lidar_msg.sensor_3 = 2.0
lidar_msg.sensor_4 = 0.12
elif (lidar_msgs_sent == 20):
break
if not self.called_positionchanger_service:
future = self.move_position_client.call_async(self.request)
future.add_done_callback(self.move_position_callback)
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention back: 0.64", 'position_changer-1')
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention back: 0.88", 'position_changer-1')
def test_positionchanger_lidar_moves_away_still(self, proc_output):
self.node.get_logger().info("starting back test")
self.request.front_back = 0.0
self.request.left_right = 0.0
self.request.up_down = 0.0
self.request.angle = 0.0
lidar_msgs_sent = 0
lidar_msg = LidarReading()
lidar_msg.sensor_1 = 2.0 # front right
lidar_msg.sensor_2 = 2.0 # front left
lidar_msg.sensor_3 = 2.0 # rear left
lidar_msg.sensor_4 = 0.12 # rear right
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
end_time = time.time() + 10.0
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
self.lidar_publisher.publish(lidar_msg)
lidar_msgs_sent += 1
if (lidar_msgs_sent == 10):
lidar_msg.sensor_4 = 2.0
lidar_msg.sensor_3 = 0.36
elif (lidar_msgs_sent == 20):
break
if not self.called_positionchanger_service:
future = self.move_position_client.call_async(self.request)
future.add_done_callback(self.move_position_callback)
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention back: 0.64", 'position_changer-1')
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention back: 0.88", 'position_changer-1')

View File

@@ -31,6 +31,8 @@ rosidl_generate_interfaces(${PROJECT_NAME}
"srv/ControlRelais.srv"
"srv/MovePosition.srv"
"srv/EnableFailsafe.srv"
"srv/ReadyDrone.srv"
"srv/Land.srv"
"msg/DroneControlMode.msg"
"msg/DroneArmStatus.msg"
"msg/DroneRouteStatus.msg"
@@ -38,6 +40,7 @@ rosidl_generate_interfaces(${PROJECT_NAME}
"msg/HeightData.msg"
"msg/LidarReading.msg"
"msg/MultiflexReading.msg"
"msg/FailsafeMsg.msg"
)
if(BUILD_TESTING)

View File

@@ -2,4 +2,8 @@ float32 battery_percentage
float32 cpu_usage
int32 route_setpoint # -1 if no route
wstring control_mode
bool armed
bool armed
float32[3] velocity
float32[3] position
float32 height
bool failsafe

View File

@@ -0,0 +1,2 @@
---
bool is_landing

View File

@@ -0,0 +1,2 @@
---
bool success

View File

@@ -6,8 +6,10 @@ from drone_services.msg import DroneStatus
from drone_services.msg import DroneControlMode
from drone_services.msg import DroneArmStatus
from drone_services.msg import DroneRouteStatus
from drone_services.msg import HeightData
from px4_msgs.msg import BatteryStatus
from px4_msgs.msg import Cpuload
from px4_msgs.msg import VehicleOdometry
CONTROL_MODE_ATTITUDE = 1
CONTROL_MODE_VELOCITY = 2
@@ -32,17 +34,23 @@ class DroneStatusNode(Node):
DroneArmStatus, '/drone/arm_status', self.arm_status_callback, 10)
self.route_status_subscriber = self.create_subscription(
DroneRouteStatus, '/drone/route_status', self.route_status_callback, 10)
self.height_data_subscriber = self.create_subscription(HeightData, '/drone/height', self.height_data_callback, 10)
self.battery_status_subscriber = self.create_subscription(
BatteryStatus, '/fmu/out/battery_status', self.battery_status_callback, qos_profile=qos_profile)
self.cpu_load_subscriber = self.create_subscription(
Cpuload, '/fmu/out/cpuload', self.cpu_load_callback, qos_profile=qos_profile)
self.vehicle_odometry_subscriber = self.create_subscription(
VehicleOdometry, "/fmu/out/vehicle_odometry", self.vehicle_odometry_callback, qos_profile=qos_profile)
# publish every 0.5 seconds
self.timer = self.create_timer(0.5, self.publish_status)
self.armed = False
self.height = 0.0
self.control_mode = "attitude"
self.battery_percentage = 100.0
self.cpu_usage = 0.0
self.route_setpoint = 0
self.position = []
self.velocity = []
def publish_status(self):
msg = DroneStatus()
@@ -51,10 +59,14 @@ class DroneStatusNode(Node):
msg.battery_percentage = self.battery_percentage
msg.cpu_usage = self.cpu_usage
msg.route_setpoint = self.route_setpoint
msg.position = self.position
msg.velocity = self.velocity
msg.height = self.height
self.publisher.publish(msg)
self.get_logger().info('Publishing status message')
# self.get_logger().info('Publishing status message')
def control_mode_callback(self, msg):
self.get_logger().info('Got control mode callback!')
if msg.control_mode == CONTROL_MODE_ATTITUDE:
self.control_mode = "attitude"
elif msg.control_mode == CONTROL_MODE_VELOCITY:
@@ -64,7 +76,13 @@ class DroneStatusNode(Node):
else:
self.control_mode = "unknown"
def height_data_callback(self, msg):
self.height = msg.min_height
def arm_status_callback(self, msg):
self.get_logger().info("Got arm status callback!")
if msg.armed:
self.get_logger().info("DRONE IS ARMED")
self.armed = msg.armed
def route_status_callback(self, msg):
@@ -76,6 +94,11 @@ class DroneStatusNode(Node):
def cpu_load_callback(self, msg):
self.cpu_usage = msg.load
def vehicle_odometry_callback(self, msg):
self.position = msg.position
self.velocity = msg.velocity
def main(args=None):
rclpy.init(args=args)

View File

@@ -5,7 +5,7 @@ from drone_services.srv import EnableFailsafe
from drone_services.msg import FailsafeMsg
class FailSafe(Node):
def __init__(self):
super().init("failsafe")
super().__init__("failsafe")
self.failsafe_enabled = False
self.failsafe_msg = ""
self.get_logger().info("Failsafe node started")
@@ -18,14 +18,14 @@ class FailSafe(Node):
self.failsafe_msg = request.message
response.enabled = self.failsafe_enabled
response.message = self.failsafe_msg
self.get_logger().info("Failsafe triggered")
self.get_logger().info("Failsafe triggered! Message: " + self.failsafe_msg)
self.publish_failsafe()
return response
def publish_failsafe(self):
msg = FailsafeMsg()
msg.enabled = self.failsafe_enabled
msg.message = self.failsafe_msg
msg.msg = self.failsafe_msg
self.failsafe_publisher.publish(msg)
self.get_logger().info("Publishing failsafe message")
@@ -33,7 +33,7 @@ class FailSafe(Node):
def main(args=None):
rclpy.init(args=args)
failsafe_node = FailSafe()
failsafe_node.spin()
rclpy.spin(failsafe_node)
failsafe_node.destroy_node()
rclpy.shutdown()

View File

@@ -8,10 +8,6 @@
<license>Apache License 2.0</license>
<depend>rclpy</depend>
<depend>drone_services</depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>

View File

@@ -1,23 +0,0 @@
# Copyright 2015 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ament_copyright.main import main
import pytest
@pytest.mark.copyright
@pytest.mark.linter
def test_copyright():
rc = main(argv=['.', 'test'])
assert rc == 0, 'Found errors'

View File

@@ -0,0 +1,86 @@
import os
import sys
import unittest
import time
import launch
import launch_ros
import launch_ros.actions
import launch_testing.actions
import pytest
import rclpy
from drone_services.srv import EnableFailsafe
from drone_services.msg import FailsafeMsg
# launch node
@pytest.mark.rostest
def generate_test_description():
file_path = os.path.dirname(__file__)
failsafe_node = launch_ros.actions.Node(
executable=sys.executable,
arguments=[os.path.join(file_path, '..', 'failsafe', 'failsafe.py')],
additional_env={'PYTHONUNBUFFERED': '1'}
)
return (
launch.LaunchDescription([
failsafe_node,
launch_testing.actions.ReadyToTest(),
]),
{
'failsafe_node': failsafe_node,
}
)
class FailsafeUnitTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
rclpy.init()
@classmethod
def tearDownClass(cls):
rclpy.shutdown()
def setUp(self):
self.node = rclpy.create_node('test_failsafe')
self.service_called = False
def tearDown(self):
self.node.destroy_node()
def service_call_callback(self,future):
self.assertIsNotNone(future.result())
self.assertTrue(future.result().enabled)
self.assertEqual(future.result().message, "test")
self.service_called = True
def test_failsafe_node_enables(self,failsafe_node,proc_output):
failsafe_msgs = []
failsafe_subscription = self.node.create_subscription(FailsafeMsg, "/drone/failsafe", lambda msg: failsafe_msgs.append(msg), 10)
failsafe_client = self.node.create_client(EnableFailsafe, "/drone/enable_failsafe")
while not failsafe_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info("service not available, waiting again...")
request = EnableFailsafe.Request()
request.message = "test"
end_time = time.time() + 10.0
try:
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
if (not self.service_called):
future = failsafe_client.call_async(request)
future.add_done_callback(self.service_call_callback)
else:
break
self.assertTrue(failsafe_msgs[0].enabled)
self.assertEqual(failsafe_msgs[0].msg, "test")
self.assertTrue(self.service_called)
finally:
self.node.destroy_subscription(failsafe_subscription)
self.node.destroy_client(failsafe_client)

View File

@@ -1,25 +0,0 @@
# Copyright 2017 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ament_flake8.main import main_with_errors
import pytest
@pytest.mark.flake8
@pytest.mark.linter
def test_flake8():
rc, errors = main_with_errors(argv=[])
assert rc == 0, \
'Found %d code style errors / warnings:\n' % len(errors) + \
'\n'.join(errors)

View File

@@ -1,23 +0,0 @@
# Copyright 2015 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ament_pep257.main import main
import pytest
@pytest.mark.linter
@pytest.mark.pep257
def test_pep257():
rc = main(argv=['.', 'test'])
assert rc == 0, 'Found code style errors / warnings'

View File

@@ -60,8 +60,12 @@ install(
DESTINATION share/${PROJECT_NAME}
)
install(FILES
test/test_failsafe_enabled.py
DESTINATION lib/${PROJECT_NAME})
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# uncomment the line when a copyright and license is not present in all source files
@@ -69,7 +73,9 @@ if(BUILD_TESTING)
# the following line skips cpplint (only works in a git repo)
# uncomment the line when this package is not in a git repo
# set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
# ament_lint_auto_find_test_dependencies()
find_package(launch_testing_ament_cmake REQUIRED)
add_launch_test(test/test_failsafe_enabled.py)
endif()
ament_package()

View File

@@ -15,9 +15,6 @@
<depend>drone_services</depend>
<depend>std_srvs</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>

View File

@@ -54,7 +54,7 @@ public:
vehicle_local_position_subscription_ = this->create_subscription<px4_msgs::msg::VehicleLocalPosition>("/fmu/out/vehicle_local_position", qos, std::bind(&PX4Controller::on_local_position_receive, this, std::placeholders::_1));
// subscription on receiving a new control mode
control_mode_subscription_ = this->create_subscription<drone_services::msg::DroneControlMode>("/drone/control_mode", qos, std::bind(&PX4Controller::on_control_mode_receive, this, std::placeholders::_1));
failsafe_subscription = this->create_subscription<drone_services::msg::FailsafeMsg>("/drone/failsafe", qos, std::bind(&PX4Controller::on_failsafe_receive, this, std::placeholders::_1));
failsafe_subscription_ = this->create_subscription<drone_services::msg::FailsafeMsg>("/drone/failsafe", qos, std::bind(&PX4Controller::on_failsafe_receive, this, std::placeholders::_1));
// services for controlling the drone
set_attitude_service_ = this->create_service<drone_services::srv::SetAttitude>("/drone/set_attitude", std::bind(&PX4Controller::handle_attitude_setpoint, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
set_trajectory_service_ = this->create_service<drone_services::srv::SetTrajectory>("/drone/set_trajectory", std::bind(&PX4Controller::handle_trajectory_setpoint, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
@@ -102,17 +102,17 @@ private:
char current_control_mode = CONTROL_MODE_ATTITUDE; // start with attitude control
const float omega = 0.3; // angular speed of the POLAR trajectory
const float K = 0; // [m] gain that regulates the spiral pitch
const float omega = 0.3; // angular speed of the POLAR trajectory
const float K = 0; // [m] gain that regulates the spiral pitch
float rho_0 = 0; // initial rho of polar coordinate
float theta_0 = 0; // initial theta of polar coordinate
float p0_z = -0.0; // initial height
float rho_0 = 0; // initial rho of polar coordinate
float theta_0 = 0; // initial theta of polar coordinate
float p0_z = -0.0; // initial height
float des_x = 0.0, des_y = 0.0, des_z = p0_z; // desired position
float gamma = M_PI_4; // desired heading direction
float gamma = M_PI_4; // desired heading direction
float local_x = 0; // local position x
float local_y = 0; // local position y
float local_x = 0; // local position x
float local_y = 0; // local position y
bool failsafe_enabled = false;
@@ -130,6 +130,12 @@ private:
const std::shared_ptr<drone_services::srv::SetAttitude::Request> request,
const std::shared_ptr<drone_services::srv::SetAttitude::Response> response)
{
if (this->failsafe_enabled)
{
RCLCPP_INFO(this->get_logger(), "Failsafe enabled, ignoring attitude setpoint");
response->success = false;
return;
}
if (armed)
{
if (request->yaw == 0 && request->pitch == 0 && request->roll == 0 && request->thrust == 0)
@@ -179,6 +185,12 @@ private:
const std::shared_ptr<drone_services::srv::SetTrajectory::Request> request,
const std::shared_ptr<drone_services::srv::SetTrajectory::Response> response)
{
if (this->failsafe_enabled)
{
RCLCPP_INFO(this->get_logger(), "Failsafe enabled, ignoring trajectory setpoint");
response->success = false;
return;
}
if (!(request->control_mode == CONTROL_MODE_VELOCITY || request->control_mode == CONTROL_MODE_POSITION))
{
RCLCPP_INFO(this->get_logger(), "Got invalid trajectory control mode: %d", request->control_mode);
@@ -231,6 +243,7 @@ private:
user_in_control = false;
publish_vehicle_command(px4_msgs::msg::VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 0.0, 0);
RCLCPP_INFO(this->get_logger(), "Publishing disarm status message");
auto msg = drone_services::msg::DroneArmStatus();
msg.armed = false;
arm_status_publisher_->publish(msg);
@@ -273,6 +286,7 @@ private:
this->last_thrust = -0.1; // spin motors at 10% thrust
armed = true;
RCLCPP_INFO(this->get_logger(), "Publishing arm status message");
auto msg = drone_services::msg::DroneArmStatus();
msg.armed = true;
arm_status_publisher_->publish(msg);
@@ -382,9 +396,6 @@ private:
send_velocity_setpoint();
return;
}
// the spiral, in polar coordinates (theta, rho), is given by
// theta = theta_0 + omega*t
// rho = rho_0 + K*theta
float theta = theta_0 + omega * 0.1;
float rho = rho_0 + K * theta;
@@ -395,7 +406,6 @@ private:
if (!user_in_control)
{
// RCLCPP_INFO(this->get_logger(), "Sending idle attitude setpoint");
send_attitude_setpoint();
}
else
@@ -488,10 +498,10 @@ private:
/**
* @brief Callback function for receiving failsafe messages
*
*
* @param msg the message indicating that the failsafe was enabled
*/
void on_failsafe_received(const drone_services::msg::FailsafeMsg::SharedPtr msg)
void on_failsafe_receive(const drone_services::msg::FailsafeMsg::SharedPtr msg)
{
if (msg->enabled)
{

View File

@@ -0,0 +1,172 @@
import os
import unittest
import launch
import launch_ros
import launch_ros.actions
import launch_testing.actions
import pytest
import rclpy
import time
from drone_services.srv import EnableFailsafe
from drone_services.srv import SetTrajectory
from drone_services.srv import SetAttitude
from drone_services.srv import ArmDrone
@pytest.mark.rostest
def generate_test_description():
file_path = os.path.dirname(__file__)
px4_controller_node = launch_ros.actions.Node(
package='px4_connection', executable='px4_controller')
failsafe_node = launch_ros.actions.Node(
package='failsafe', executable='failsafe')
return (
launch.LaunchDescription([
px4_controller_node,
failsafe_node,
launch_testing.actions.ReadyToTest(),
]),
{
'px4_controller_node': px4_controller_node,
'failsafe_node': failsafe_node,
}
)
class TestPx4Failsafe(unittest.TestCase):
@classmethod
def setUpClass(cls):
rclpy.init()
@classmethod
def tearDownClass(cls):
rclpy.shutdown()
def setUp(self):
self.node = rclpy.create_node('test_px4_failsafe')
self.called_failsafe_service = False
self.called_trajectory_service = False
self.called_arm_service = False
self.called_attitude_service = False
def tearDown(self):
self.node.destroy_node()
def failsafe_service_callback(self,future):
self.called_failsafe_service = True
def trajectory_service_callback(self,future):
self.called_trajectory_service = True
self.assertFalse(future.result().success)
def attitude_service_callback(self,future):
self.called_attitude_service = True
self.assertFalse(future.result().success)
def arm_service_callback(self,future):
self.called_arm_service = True
self.assertFalse(future.result().success)
def test_failsafe_enabled_set_trajectory_returns_false(self, px4_controller_node, proc_output):
self.called_failsafe_service = False
failsafe_client = self.node.create_client(EnableFailsafe, '/drone/enable_failsafe')
set_trajectory_client = self.node.create_client(SetTrajectory, '/drone/set_trajectory')
while not failsafe_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('failsafe service not available, waiting again...')
while not set_trajectory_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('set_trajectory service not available, waiting again...')
failsafe_request = EnableFailsafe.Request()
failsafe_request.message = "test"
set_trajectory_request = SetTrajectory.Request()
set_trajectory_request.control_mode = 2
set_trajectory_request.values = [1.0,1.0,1.0]
set_trajectory_request.yaw = 0.0
end_time = time.time() + 10.0
try:
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
if not self.called_failsafe_service:
failsafe_future = failsafe_client.call_async(failsafe_request)
failsafe_future.add_done_callback(self.failsafe_service_callback)
elif not self.called_trajectory_service:
trajectory_future = set_trajectory_client.call_async(set_trajectory_request)
trajectory_future.add_done_callback(self.trajectory_service_callback)
else:
break
self.assertTrue(self.called_failsafe_service, "Failsafe service was not called")
self.assertTrue(self.called_trajectory_service, "Trajectory service was not called")
finally:
self.node.destroy_client(failsafe_client)
self.node.destroy_client(set_trajectory_client)
def test_failsafe_enabled_set_attitude_returns_false(self, px4_controller_node, proc_output):
self.called_failsafe_service = False
failsafe_client = self.node.create_client(EnableFailsafe, '/drone/enable_failsafe')
set_attitude_client = self.node.create_client(SetAttitude, '/drone/set_attitude')
while not failsafe_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('failsafe not available, waiting again...')
while not set_attitude_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('attitude not available, waiting again...')
failsafe_request = EnableFailsafe.Request()
failsafe_request.message = "test"
set_attitude_request = SetAttitude.Request()
set_attitude_request.pitch = 1.0
set_attitude_request.yaw = 1.0
set_attitude_request.roll = 1.0
set_attitude_request.thrust = 0.5
end_time = time.time() + 10.0
try:
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
if not self.called_failsafe_service:
failsafe_future = failsafe_client.call_async(failsafe_request)
failsafe_future.add_done_callback(self.failsafe_service_callback)
elif not self.called_attitude_service:
attitude_future = set_attitude_client.call_async(set_attitude_request)
attitude_future.add_done_callback(self.attitude_service_callback)
else:
break
self.assertTrue(self.called_failsafe_service, "Failsafe service was not called")
self.assertTrue(self.called_attitude_service, "Attitude service was not called")
finally:
self.node.destroy_client(failsafe_client)
self.node.destroy_client(set_attitude_client)
def test_failsafe_enabled_arm_returns_false(self, px4_controller_node, proc_output):
self.called_failsafe_service = False
failsafe_client = self.node.create_client(EnableFailsafe, '/drone/enable_failsafe')
arm_client = self.node.create_client(ArmDrone, '/drone/arm')
while not failsafe_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('failsafe not available, waiting again...')
while not arm_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('arm not available, waiting again...')
failsafe_request = EnableFailsafe.Request()
failsafe_request.message = "test"
arm_request = ArmDrone.Request()
end_time = time.time() + 10.0
try:
while time.time() < end_time:
rclpy.spin_once(self.node, timeout_sec=0.1)
if not self.called_failsafe_service:
failsafe_future = failsafe_client.call_async(failsafe_request)
failsafe_future.add_done_callback(self.failsafe_service_callback)
elif not self.called_arm_service:
arm_future = arm_client.call_async(arm_request)
arm_future.add_done_callback(self.arm_service_callback)
else:
break
self.assertTrue(self.called_failsafe_service, "Failsafe service was not called")
self.assertTrue(self.called_arm_service, "Arm service was not called")
finally:
self.node.destroy_client(failsafe_client)
self.node.destroy_client(arm_client)

View File

@@ -0,0 +1,103 @@
import os
import sys
import unittest
import launch
import launch_ros
import launch_ros.actions
import launch_testing.actions
import pytest
import rclpy
from drone_services.srv import SetVehicleControl
from drone_services.msg import DroneControlMode
@pytest.mark.rostest
def generate_test_description():
heartbeat_node = launch_ros.actions.Node(
package='px4_connection', executable='heartbeat')
return (
launch.LaunchDescription([
heartbeat_node,
launch_testing.actions.ReadyToTest(),
]),
{
'heartbeat_node': heartbeat_node,
}
)
class TestHeartbeatControlMode(unittest.TestCase):
@classmethod
def setUpClass(cls):
rclpy.init()
@classmethod
def tearDownClass(cls):
rclpy.shutdown()
def setUp(self):
self.node = rclpy.create_node('test_heartbeat_control_mode')
self.called_control_mode_service = False
self.received_control_mode = False
def tearDown(self):
self.node.destroy_node()
def control_mode_callback(self,msg):
self.received_control_mode = True
self.assertEqual(msg.control_mode,1)
def heartbeat_service_callback_correct(self,future):
self.called_control_mode_service = True
self.assertIsNotNone(future.result())
self.assertTrue(future.result().success)
def heartbeat_service_callback_wrong(self,future):
self.called_control_mode_service = True
self.assertIsNotNone(future.result())
self.assertFalse(future.result().success)
def test_set_vehicle_control_mode_proper_value(self,heartbeat_node,proc_output):
self.called_control_mode_service = False
heartbeat_client = self.node.create_client(SetVehicleControl, '/drone/set_vehicle_control')
while not heartbeat_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('heartbeat service not available, waiting again...')
heartbeat_sub = self.node.create_subscription(DroneControlMode,"/drone/control_mode",self.control_mode_callback,10)
request = SetVehicleControl.Request()
request.control= 4 # attitude control
try:
while True:
rclpy.spin_once(self.node,timeout_sec=0.1)
if not self.called_control_mode_service:
vehicle_control_future = heartbeat_client.call_async(request)
vehicle_control_future.add_done_callback(self.heartbeat_service_callback_correct)
elif not self.received_control_mode:
continue
else:
break
finally:
self.node.destroy_client(heartbeat_client)
def test_set_vehicle_control_mode_wrong_value(self,heartbeat_node,proc_output):
self.called_control_mode_service = False
heartbeat_client = self.node.create_client(SetVehicleControl, '/drone/set_vehicle_control')
while not heartbeat_client.wait_for_service(timeout_sec=1.0):
self.node.get_logger().info('heartbeat service not available, waiting again...')
request = SetVehicleControl.Request()
request.control = 33 # wrong control mode
try:
while True:
rclpy.spin_once(self.node,timeout_sec=0.1)
if not self.called_control_mode_service:
vehicle_control_future = heartbeat_client.call_async(request)
vehicle_control_future.add_done_callback(self.heartbeat_service_callback_wrong)
else:
break
finally:
self.node.destroy_client(heartbeat_client)

46
test_stream.py Normal file
View File

@@ -0,0 +1,46 @@
import cv2
import requests
video_url = "http://10.1.1.41:8080/video"
# Set the headers for the POST request
headers = {'Content-Type': 'application/octet-stream'}
vid = cv2.VideoCapture(0)
# vid.set(cv2.CAP_PROP_FRAME_WIDTH, RES_4K_W)
# vid.set(cv2.CAP_PROP_FRAME_HEIGHT, RES_4K_H)
while True:
try:
while vid.isOpened():
pass
ret, frame = vid.read()
if not ret:
# If reading the frame failed, break the loop
break
# Convert the frame to bytes
_, img_encoded = cv2.imencode('.jpg', frame)
frame_data = img_encoded.tobytes()
# Send the frame data as the request body
response = requests.post(video_url, data=frame_data, headers=headers)
# Check the response status
if response.status_code == 200:
print('Frame sent successfully.')
else:
print('Failed to send frame.')
# if self.websocket is not None:
# img,frame = vid.read()
# frame = cv2.resize(frame,(640,480))
# encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 65]
# man = cv2.imencode('.jpg', frame, encode_param)[1]
# self.get_logger().info('Sending video')
# asyncio.ensure_future(self.websocket.send(man.tobytes()),loop=self.event_loop)
# await asyncio.sleep(1)
# else:
# self.get_logger().info('No websocket connection')
except Exception as e:
print('Something went wrong while reading and sending video: ' + str(e))