├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── drone.go ├── drone_test.go ├── example └── main.go ├── go.mod ├── go.sum └── state.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.20' 20 | 21 | - name: Install dependencies 22 | run: go mod download 23 | 24 | - name: Run tests 25 | run: go test -race -covermode atomic -coverprofile=covprofile ./... 26 | 27 | - if: github.event_name != 'pull_request' 28 | name: Send coverage 29 | env: 30 | COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 31 | run: | 32 | go install github.com/mattn/goveralls@latest 33 | goveralls -coverprofile=covprofile -service=github 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | .idea/ 4 | example.* 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Quentin Renard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoReportCard](http://goreportcard.com/badge/github.com/asticode/go-astitello)](http://goreportcard.com/report/github.com/asticode/go-astitello) 2 | [![GoDoc](https://godoc.org/github.com/asticode/go-astitello?status.svg)](https://godoc.org/github.com/asticode/go-astitello) 3 | [![Test](https://github.com/asticode/go-astitello/actions/workflows/test.yml/badge.svg)](https://github.com/asticode/go-astitello/actions/workflows/test.yml) 4 | [![Coveralls](https://coveralls.io/repos/github/asticode/go-astitello/badge.svg?branch=master)](https://coveralls.io/github/asticode/go-astitello) 5 | 6 | This is a Golang implementation of the DJI Tello SDK. 7 | 8 | Up-to-date SDK documentation can be downloaded [here](https://www.ryzerobotics.com/fr/tello/downloads). 9 | 10 | Right now this library is compatible with `v1.3.0.0` of SDK ([documentation](https://terra-1-g.djicdn.com/2d4dce68897a46b19fc717f3576b7c6a/Tello%20%E7%BC%96%E7%A8%8B%E7%9B%B8%E5%85%B3/For%20Tello/Tello%20SDK%20Documentation%20EN_1.3_1122.pdf)) 11 | 12 | # Disclaimer 13 | 14 | Tello is a registered trademark of Ryze Tech. The author of this package is in no way affiliated with Ryze, DJI, or Intel. 15 | 16 | Use this package at your own risk. The author(s) is/are in no way responsible for any damage caused either to or by the drone when using this software. 17 | 18 | # Install the project 19 | 20 | Run the following command: 21 | 22 | ``` 23 | $ go get -u github.com/asticode/go-astitello/... 24 | ``` 25 | 26 | # Run the example 27 | 28 | IMPORTANT: the drone will make a flip to its right during this example, make sure you have enough space around the drone! 29 | 30 | 1) Switch on your drone 31 | 32 | 2) Connect to its Wifi 33 | 34 | 3) If this is the first time you're using it, you may have to activate it using the official app 35 | 36 | 4) If you want to test the video, install `ffmpeg` on your machine 37 | 38 | 5) Run the following command: 39 | 40 | ``` 41 | $ go run example/main.go 42 | ``` 43 | 44 | 5) Watch your drone take off, make a flip to its right and land! Make sure to look at the terminal output too, some valuable information were printed there! 45 | 46 | 6) If you've installed `ffmpeg` you should also see a new file called `example.ts`. Check it out! 47 | 48 | # Use it in your code 49 | 50 | WARNING1: the code below doesn't handle errors for readibility purposes. However you SHOULD! 51 | 52 | WARNING2: the code below doesn't list all available methods, be sure to check out the [doc](https://godoc.org/github.com/asticode/go-astitello)! 53 | 54 | ## Set up the drone 55 | 56 | ```go 57 | // Create logger 58 | l := log.New(os.StdErr, "", 0) 59 | 60 | // Create the drone 61 | d := astitello.New(l) 62 | 63 | // Start the drone 64 | d.Start() 65 | 66 | // Make sure to close the drone once everything is over 67 | defer d.Close() 68 | ``` 69 | 70 | ## Basic commands 71 | 72 | ```go 73 | // Handle take off event 74 | d.On(astitello.TakeOffEvent, func(interface{}) { l.Println("drone has took off!") }) 75 | 76 | // Take off 77 | d.TakeOff() 78 | 79 | // Flip 80 | d.Flip(astitello.FlipRight) 81 | 82 | // Log state 83 | l.Printf("state is: %+v\n", d.State()) 84 | 85 | // In case you're using controllers, you can use set sticks positions directly 86 | d.SetSticks(-20, 10, -30, 40) 87 | 88 | // Land 89 | d.Land() 90 | ``` 91 | 92 | ## Video 93 | 94 | ```go 95 | // Handle new video packet 96 | d.On(astitello.VideoPacketEvent, astitello.VideoPacketEventHandler(func(p []byte) { 97 | l.Printf("video packet length: %d\n", len(p)) 98 | })) 99 | 100 | // Start video 101 | d.StartVideo() 102 | 103 | // Make sure to stop video 104 | defer d.StopVideo() 105 | ``` 106 | 107 | # Why this library? 108 | 109 | First off, I'd like to say there are very nice DJI Tello libraries out there such as: 110 | 111 | - https://github.com/hybridgroup/gobot/tree/master/platforms/dji/tello 112 | - https://github.com/SMerrony/tello 113 | 114 | Unfortunately they seem to rely on reverse-engineering the official app which is undocumented. 115 | 116 | If you'd rather use a library that is based on an official documentation, you've come to the right place! 117 | 118 | # Known problems with the SDK 119 | 120 | - sometimes a cmd doesn't get any response back from the SDK. In that case the cmd will idle until its timeout is reached. -------------------------------------------------------------------------------- /drone.go: -------------------------------------------------------------------------------- 1 | package astitello 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | "github.com/asticode/go-astikit" 14 | ) 15 | 16 | // Defaults 17 | var ( 18 | defaultTimeout = 5 * time.Second 19 | cmdAddr = "192.168.10.1:8889" 20 | respAddr = ":8889" 21 | stateAddr = ":8890" 22 | videoAddr = ":11111" 23 | ) 24 | 25 | // Events 26 | const ( 27 | LandEvent = "land" 28 | StateEvent = "state" 29 | TakeOffEvent = "take.off" 30 | VideoPacketEvent = "video.packet" 31 | ) 32 | 33 | // Flip directions 34 | const ( 35 | FlipBack = "b" 36 | FlipForward = "f" 37 | FlipLeft = "l" 38 | FlipRight = "r" 39 | ) 40 | 41 | // ErrNotConnected is the error thrown when trying to send a cmd while not connected to the drone 42 | var ErrNotConnected = errors.New("astitello: not connected") 43 | 44 | // Drone represents an object capable of interacting with the SDK 45 | type Drone struct { 46 | cancel context.CancelFunc 47 | cmdConn *net.UDPConn 48 | cmds map[*cmd]bool 49 | ctx context.Context 50 | e *astikit.Eventer 51 | l astikit.SeverityLogger 52 | lr string 53 | mc *sync.Mutex // Locks cmds 54 | ms *sync.Mutex // Locks s 55 | msc *sync.Mutex // Locks sendCmd 56 | ol *sync.Once // Limits Close() 57 | oo *sync.Once // Limits Connect() 58 | rc *sync.Cond 59 | s *State 60 | stateConn *net.UDPConn 61 | videoConn *net.UDPConn 62 | } 63 | 64 | // New creates a new Drone 65 | func New(l astikit.StdLogger) *Drone { 66 | return &Drone{ 67 | cmds: make(map[*cmd]bool), 68 | e: astikit.NewEventer(astikit.EventerOptions{}), 69 | l: astikit.AdaptStdLogger(l), 70 | mc: &sync.Mutex{}, 71 | msc: &sync.Mutex{}, 72 | ms: &sync.Mutex{}, 73 | ol: &sync.Once{}, 74 | oo: &sync.Once{}, 75 | rc: sync.NewCond(&sync.Mutex{}), 76 | s: &State{}, 77 | } 78 | } 79 | 80 | // State returns the drone's state 81 | func (d *Drone) State() State { 82 | d.ms.Lock() 83 | defer d.ms.Unlock() 84 | return *d.s 85 | } 86 | 87 | // On adds an event handler 88 | func (d *Drone) On(name string, h astikit.EventerHandler) { 89 | d.e.On(name, h) 90 | } 91 | 92 | // Close closes the drone properly 93 | func (d *Drone) Close() { 94 | // Make sure to execute this only once 95 | d.ol.Do(func() { 96 | // Cancel context 97 | if d.cancel != nil { 98 | d.cancel() 99 | } 100 | 101 | // Reset once 102 | d.oo = &sync.Once{} 103 | 104 | // Stop and reset eventer 105 | d.e.Stop() 106 | d.e.Reset() 107 | 108 | // Reset cmds 109 | d.cmds = make(map[*cmd]bool) 110 | 111 | // Close connections 112 | if d.cmdConn != nil { 113 | d.cmdConn.Close() 114 | } 115 | if d.stateConn != nil { 116 | d.stateConn.Close() 117 | } 118 | if d.videoConn != nil { 119 | d.videoConn.Close() 120 | } 121 | }) 122 | } 123 | 124 | // Start starts to the drone 125 | func (d *Drone) Start() (err error) { 126 | // Make sure to execute this only once 127 | d.oo.Do(func() { 128 | // Create context 129 | d.ctx, d.cancel = context.WithCancel(context.Background()) 130 | 131 | // Reset once 132 | d.ol = &sync.Once{} 133 | 134 | // Start eventer 135 | go d.e.Start(d.ctx) 136 | 137 | // Handle state 138 | if err = d.handleState(); err != nil { 139 | err = fmt.Errorf("astitello: handling state failed: %w", err) 140 | return 141 | } 142 | 143 | // Handle video 144 | if err = d.handleVideo(); err != nil { 145 | err = fmt.Errorf("astitello: handling video failed: %w", err) 146 | return 147 | } 148 | 149 | // Handle commands 150 | if err = d.handleCmds(); err != nil { 151 | err = fmt.Errorf("astitello: handling commands failed: %w", err) 152 | return 153 | } 154 | }) 155 | return 156 | } 157 | 158 | func (d *Drone) handleState() (err error) { 159 | // Create laddr 160 | var laddr *net.UDPAddr 161 | if laddr, err = net.ResolveUDPAddr("udp", stateAddr); err != nil { 162 | err = fmt.Errorf("astitello: creating laddr failed: %w", err) 163 | return 164 | } 165 | 166 | // Listen 167 | if d.stateConn, err = net.ListenUDP("udp", laddr); err != nil { 168 | err = fmt.Errorf("astitello: listening failed: %w", err) 169 | return 170 | } 171 | 172 | // Read state 173 | go d.readState() 174 | return 175 | } 176 | 177 | func (d *Drone) readState() { 178 | for { 179 | // Check context 180 | if d.ctx.Err() != nil { 181 | return 182 | } 183 | 184 | // Read 185 | b := make([]byte, 2048) 186 | n, err := d.stateConn.Read(b) 187 | if err != nil { 188 | if d.ctx.Err() == nil { 189 | d.l.Error(fmt.Errorf("astitello: reading state failed: %w", err)) 190 | } 191 | continue 192 | } 193 | 194 | // Create state 195 | s, err := newState(string(bytes.TrimSpace(b[:n]))) 196 | if err != nil { 197 | d.l.Error(fmt.Errorf("astitello: creating state failed: %w", err)) 198 | continue 199 | } 200 | 201 | // Update state 202 | d.ms.Lock() 203 | *d.s = s 204 | d.ms.Unlock() 205 | 206 | // Dispatch 207 | d.e.Dispatch(StateEvent, s) 208 | } 209 | } 210 | 211 | // StateEventHandler returns the proper EventHandler for the State event 212 | func StateEventHandler(f func(s State)) astikit.EventerHandler { 213 | return func(payload interface{}) { 214 | f(payload.(State)) 215 | } 216 | } 217 | 218 | func (d *Drone) handleVideo() (err error) { 219 | // Create laddr 220 | var laddr *net.UDPAddr 221 | if laddr, err = net.ResolveUDPAddr("udp", videoAddr); err != nil { 222 | err = fmt.Errorf("astitello: creating laddr failed: %w", err) 223 | return 224 | } 225 | 226 | // Listen 227 | if d.videoConn, err = net.ListenUDP("udp", laddr); err != nil { 228 | err = fmt.Errorf("astitello: listening failed: %w", err) 229 | return 230 | } 231 | 232 | // Read video 233 | go d.readVideo() 234 | return 235 | } 236 | 237 | func (d *Drone) readVideo() { 238 | var buf []byte 239 | var bufLength int 240 | for { 241 | // Check context 242 | if d.ctx.Err() != nil { 243 | return 244 | } 245 | 246 | // Read 247 | b := make([]byte, 2048) 248 | n, err := d.videoConn.Read(b) 249 | if err != nil { 250 | if d.ctx.Err() == nil { 251 | d.l.Error(fmt.Errorf("astitello: reading video failed: %w", err)) 252 | } 253 | continue 254 | } 255 | 256 | // Append to buffer 257 | buf = append(buf, b[:n]...) 258 | bufLength += n 259 | 260 | // Packet is not over 261 | if n == 1460 { 262 | continue 263 | } 264 | 265 | // Dispatch 266 | p := make([]byte, bufLength) 267 | copy(p, buf[:bufLength]) 268 | d.e.Dispatch(VideoPacketEvent, p) 269 | 270 | // Reset buffer 271 | buf = buf[:0] 272 | bufLength = 0 273 | } 274 | } 275 | 276 | // VideoPacketEventHandler returns the proper EventHandler for the VideoPacket event 277 | func VideoPacketEventHandler(f func(p []byte)) astikit.EventerHandler { 278 | return func(payload interface{}) { 279 | f(payload.([]byte)) 280 | } 281 | } 282 | 283 | func (d *Drone) handleCmds() (err error) { 284 | // Create raddr 285 | var raddr *net.UDPAddr 286 | if raddr, err = net.ResolveUDPAddr("udp", cmdAddr); err != nil { 287 | err = fmt.Errorf("astitello: creating raddr failed: %w", err) 288 | return 289 | } 290 | 291 | // Create laddr 292 | var laddr *net.UDPAddr 293 | if laddr, err = net.ResolveUDPAddr("udp", respAddr); err != nil { 294 | err = fmt.Errorf("astitello: creating laddr failed: %w", err) 295 | return 296 | } 297 | 298 | // Dial 299 | if d.cmdConn, err = net.DialUDP("udp", laddr, raddr); err != nil { 300 | err = fmt.Errorf("astitello: dialing failed: %w", err) 301 | return 302 | } 303 | 304 | // Read responses 305 | go d.readResponses() 306 | 307 | // Command 308 | if err = d.command(); err != nil { 309 | err = fmt.Errorf("astitello: command failed: %w", err) 310 | return 311 | } 312 | return 313 | } 314 | 315 | func (d *Drone) readResponses() { 316 | for { 317 | // Check context 318 | if d.ctx.Err() != nil { 319 | return 320 | } 321 | 322 | // Read 323 | b := make([]byte, 2048) 324 | n, err := d.cmdConn.Read(b) 325 | if err != nil { 326 | if d.ctx.Err() == nil { 327 | d.l.Error(fmt.Errorf("astitello: reading response failed: %w", err)) 328 | } 329 | continue 330 | } 331 | 332 | // Log 333 | r := bytes.TrimSpace(b[:n]) 334 | d.l.Debugf("astitello: received resp '%s'", r) 335 | 336 | // Signal 337 | d.rc.L.Lock() 338 | d.lr = string(r) 339 | d.rc.Signal() 340 | d.rc.L.Unlock() 341 | } 342 | } 343 | 344 | type respHandler func(resp string) error 345 | 346 | func defaultRespHandler(resp string) (err error) { 347 | // Check response 348 | if resp != "ok" { 349 | err = fmt.Errorf("astitello: invalid response: %w", errors.New(resp)) 350 | return 351 | } 352 | return 353 | } 354 | 355 | func (d *Drone) respHandlerWithEvent(name string) respHandler { 356 | return func(resp string) (err error) { 357 | // Default 358 | if err = defaultRespHandler(resp); err != nil { 359 | return 360 | } 361 | 362 | // Dispatch 363 | d.e.Dispatch(name, nil) 364 | return 365 | } 366 | } 367 | 368 | type cmd struct { 369 | canceller bool 370 | cmd string 371 | h respHandler 372 | timeout time.Duration 373 | } 374 | 375 | func (d *Drone) priorityCmd(cmd *cmd) (priority bool) { 376 | // Lock 377 | d.mc.Lock() 378 | defer d.mc.Unlock() 379 | 380 | // Check 381 | if cmd.canceller { 382 | priority = true 383 | for p := range d.cmds { 384 | if p.canceller { 385 | priority = false 386 | break 387 | } 388 | 389 | // Takeoff and land can't be sent at the same time 390 | if cmd.cmd == "land" && p.cmd == "takeoff" { 391 | priority = false 392 | break 393 | } 394 | } 395 | } 396 | return 397 | } 398 | 399 | func (d *Drone) sendCmd(cmd *cmd) (err error) { 400 | // No connection 401 | if d.cmdConn == nil { 402 | err = ErrNotConnected 403 | return 404 | } 405 | 406 | // In most cases we need to wait for the previous cmd to be done. But not when this is a priority cmd. 407 | // This is a priority cmd if cmd is a canceller and no other canceller is running 408 | priority := d.priorityCmd(cmd) 409 | 410 | // Add cmd 411 | d.mc.Lock() 412 | d.cmds[cmd] = true 413 | d.mc.Unlock() 414 | 415 | // Make sure to remove cmd 416 | defer func() { 417 | d.mc.Lock() 418 | delete(d.cmds, cmd) 419 | d.mc.Unlock() 420 | }() 421 | 422 | // Not a priority cmd 423 | if !priority { 424 | // Check context 425 | if err = d.ctx.Err(); err != nil { 426 | return 427 | } 428 | 429 | // Make sure not to send several cmds at the same time 430 | d.msc.Lock() 431 | defer d.msc.Unlock() 432 | } 433 | 434 | // Lock resp 435 | d.rc.L.Lock() 436 | defer d.rc.L.Unlock() 437 | 438 | // Log 439 | d.l.Debugf("astitello: sending cmd '%s'", cmd.cmd) 440 | 441 | // Write 442 | if _, err = d.cmdConn.Write([]byte(cmd.cmd)); err != nil { 443 | err = fmt.Errorf("astitello: writing failed: %w", err) 444 | return 445 | } 446 | 447 | // No handler 448 | if cmd.h == nil { 449 | return 450 | } 451 | 452 | // Create context 453 | ctx, cancel := context.WithCancel(d.ctx) 454 | if cmd.timeout > 0 { 455 | ctx, cancel = context.WithTimeout(d.ctx, cmd.timeout) 456 | } 457 | defer cancel() 458 | 459 | // Handle context 460 | go func() { 461 | // Wait for context to be done 462 | <-ctx.Done() 463 | 464 | // Check error 465 | if d.ctx.Err() != context.Canceled && ctx.Err() != context.DeadlineExceeded { 466 | return 467 | } 468 | 469 | // Signal 470 | d.rc.L.Lock() 471 | d.rc.Signal() 472 | d.rc.L.Unlock() 473 | }() 474 | 475 | // Wait for response 476 | d.rc.Wait() 477 | 478 | // Check context 479 | if ctx.Err() != nil { 480 | err = ctx.Err() 481 | return 482 | } 483 | 484 | // Custom 485 | if err = cmd.h(d.lr); err != nil { 486 | err = fmt.Errorf("astitello: custom handler failed: %w", err) 487 | return 488 | } 489 | return 490 | } 491 | 492 | func (d *Drone) command() (err error) { 493 | // Send "command" cmd 494 | if err = d.sendCmd(&cmd{ 495 | cmd: "command", 496 | h: defaultRespHandler, 497 | timeout: defaultTimeout, 498 | }); err != nil { 499 | err = fmt.Errorf("astitello: sending 'command' cmd failed: %w", err) 500 | return 501 | } 502 | return 503 | } 504 | 505 | // StartVideo makes Tello start streaming video 506 | func (d *Drone) StartVideo() (err error) { 507 | // Send cmd 508 | if err = d.sendCmd(&cmd{ 509 | cmd: "streamon", 510 | h: defaultRespHandler, 511 | timeout: defaultTimeout, 512 | }); err != nil { 513 | err = fmt.Errorf("astitello: sending streamon cmd failed: %w", err) 514 | return 515 | } 516 | return 517 | } 518 | 519 | // StopVideo makes Tello stop streaming video 520 | func (d *Drone) StopVideo() (err error) { 521 | // Send cmd 522 | if err = d.sendCmd(&cmd{ 523 | cmd: "streamoff", 524 | h: defaultRespHandler, 525 | timeout: defaultTimeout, 526 | }); err != nil { 527 | err = fmt.Errorf("astitello: sending streamoff cmd failed: %w", err) 528 | return 529 | } 530 | return 531 | } 532 | 533 | // Emergency makes Tello stop all motors immediately 534 | // This cmd doesn't seem to be receiving any response, that's why we don't provide any handler 535 | func (d *Drone) Emergency() (err error) { 536 | // Send cmd 537 | if err = d.sendCmd(&cmd{ 538 | canceller: true, 539 | cmd: "emergency", 540 | timeout: defaultTimeout, 541 | }); err != nil { 542 | err = fmt.Errorf("astitello: sending emergency cmd failed: %w", err) 543 | return 544 | } 545 | return 546 | } 547 | 548 | // TakeOff makes Tello auto takeoff 549 | func (d *Drone) TakeOff() (err error) { 550 | // Send cmd 551 | if err = d.sendCmd(&cmd{ 552 | cmd: "takeoff", 553 | h: d.respHandlerWithEvent(TakeOffEvent), 554 | timeout: 20 * time.Second, 555 | }); err != nil { 556 | err = fmt.Errorf("astitello: sending takeoff cmd failed: %w", err) 557 | return 558 | } 559 | return 560 | } 561 | 562 | // Land makes Tello auto land 563 | func (d *Drone) Land() (err error) { 564 | // Send cmd 565 | if err = d.sendCmd(&cmd{ 566 | canceller: true, 567 | cmd: "land", 568 | h: d.respHandlerWithEvent(LandEvent), 569 | timeout: 20 * time.Second, 570 | }); err != nil { 571 | err = fmt.Errorf("astitello: sending land cmd failed: %w", err) 572 | return 573 | } 574 | return 575 | } 576 | 577 | // Up makes Tello fly up with distance x cm 578 | func (d *Drone) Up(x int) (err error) { 579 | // Send cmd 580 | if err = d.sendCmd(&cmd{ 581 | cmd: fmt.Sprintf("up %d", x), 582 | h: defaultRespHandler, 583 | timeout: time.Minute, 584 | }); err != nil { 585 | err = fmt.Errorf("astitello: sending up cmd failed: %w", err) 586 | return 587 | } 588 | return 589 | } 590 | 591 | // Down makes Tello fly down with distance x cm 592 | func (d *Drone) Down(x int) (err error) { 593 | // Send cmd 594 | if err = d.sendCmd(&cmd{ 595 | cmd: fmt.Sprintf("down %d", x), 596 | h: defaultRespHandler, 597 | timeout: time.Minute, 598 | }); err != nil { 599 | err = fmt.Errorf("astitello: sending down cmd failed: %w", err) 600 | return 601 | } 602 | return 603 | } 604 | 605 | // Left makes Tello fly left with distance x cm 606 | func (d *Drone) Left(x int) (err error) { 607 | // Send cmd 608 | if err = d.sendCmd(&cmd{ 609 | cmd: fmt.Sprintf("left %d", x), 610 | h: defaultRespHandler, 611 | timeout: time.Minute, 612 | }); err != nil { 613 | err = fmt.Errorf("astitello: sending left cmd failed: %w", err) 614 | return 615 | } 616 | return 617 | } 618 | 619 | // Right makes Tello fly right with distance x cm 620 | func (d *Drone) Right(x int) (err error) { 621 | // Send cmd 622 | if err = d.sendCmd(&cmd{ 623 | cmd: fmt.Sprintf("right %d", x), 624 | h: defaultRespHandler, 625 | timeout: time.Minute, 626 | }); err != nil { 627 | err = fmt.Errorf("astitello: sending right cmd failed: %w", err) 628 | return 629 | } 630 | return 631 | } 632 | 633 | // Forward makes Tello fly forward with distance x cm 634 | func (d *Drone) Forward(x int) (err error) { 635 | // Send cmd 636 | if err = d.sendCmd(&cmd{ 637 | cmd: fmt.Sprintf("forward %d", x), 638 | h: defaultRespHandler, 639 | timeout: time.Minute, 640 | }); err != nil { 641 | err = fmt.Errorf("astitello: sending forward cmd failed: %w", err) 642 | return 643 | } 644 | return 645 | } 646 | 647 | // Back makes Tello fly back with distance x cm 648 | func (d *Drone) Back(x int) (err error) { 649 | // Send cmd 650 | if err = d.sendCmd(&cmd{ 651 | cmd: fmt.Sprintf("back %d", x), 652 | h: defaultRespHandler, 653 | timeout: time.Minute, 654 | }); err != nil { 655 | err = fmt.Errorf("astitello: sending back cmd failed: %w", err) 656 | return 657 | } 658 | return 659 | } 660 | 661 | // RotateClockwise makes Tello rotate x degree clockwise 662 | func (d *Drone) RotateClockwise(x int) (err error) { 663 | // Send cmd 664 | if err = d.sendCmd(&cmd{ 665 | cmd: fmt.Sprintf("cw %d", x), 666 | h: defaultRespHandler, 667 | timeout: time.Minute, 668 | }); err != nil { 669 | err = fmt.Errorf("astitello: sending cw cmd failed: %w", err) 670 | return 671 | } 672 | return 673 | } 674 | 675 | // RotateCounterClockwise makes Tello rotate x degree counter-clockwise 676 | func (d *Drone) RotateCounterClockwise(x int) (err error) { 677 | // Send cmd 678 | if err = d.sendCmd(&cmd{ 679 | cmd: fmt.Sprintf("ccw %d", x), 680 | h: defaultRespHandler, 681 | timeout: time.Minute, 682 | }); err != nil { 683 | err = fmt.Errorf("astitello: sending ccw cmd failed: %w", err) 684 | return 685 | } 686 | return 687 | } 688 | 689 | // Flip makes Tello flip in the specified direction 690 | // Check out Flip... constants for available flip directions 691 | func (d *Drone) Flip(x string) (err error) { 692 | // Send cmd 693 | if err = d.sendCmd(&cmd{ 694 | cmd: fmt.Sprintf("flip %s", x), 695 | h: defaultRespHandler, 696 | timeout: 20 * time.Second, 697 | }); err != nil { 698 | err = fmt.Errorf("astitello: sending flip cmd failed: %w", err) 699 | return 700 | } 701 | return 702 | } 703 | 704 | // Go makes Tello fly to x y z in speed (cm/s) 705 | func (d *Drone) Go(x, y, z, speed int) (err error) { 706 | // Send cmd 707 | if err = d.sendCmd(&cmd{ 708 | cmd: fmt.Sprintf("go %d %d %d %d", x, y, z, speed), 709 | h: defaultRespHandler, 710 | timeout: time.Minute, 711 | }); err != nil { 712 | err = fmt.Errorf("astitello: sending go cmd failed: %w", err) 713 | return 714 | } 715 | return 716 | } 717 | 718 | // Curve makes Tello fly a curve defined by the current and two given coordinates with speed (cm/s) 719 | func (d *Drone) Curve(x1, y1, z1, x2, y2, z2, speed int) (err error) { 720 | // Send cmd 721 | if err = d.sendCmd(&cmd{ 722 | cmd: fmt.Sprintf("curve %d %d %d %d %d %d %d", x1, y1, z1, x2, y2, z2, speed), 723 | h: defaultRespHandler, 724 | timeout: time.Minute, 725 | }); err != nil { 726 | err = fmt.Errorf("astitello: sending go cmd failed: %w", err) 727 | return 728 | } 729 | return 730 | } 731 | 732 | // SetSticks sends RC control via four channels 733 | // All values are between -100 and 100 734 | // lr: left/right 735 | // fb: forward/backward 736 | // ud: up/down 737 | // y: yawn 738 | // This cmd doesn't seem to be receiving any response, that's why we don't provide any handler 739 | func (d *Drone) SetSticks(lr, fb, ud, y int) (err error) { 740 | // Send cmd 741 | if err = d.sendCmd(&cmd{ 742 | cmd: fmt.Sprintf("rc %d %d %d %d", lr, fb, ud, y), 743 | timeout: defaultTimeout, 744 | }); err != nil { 745 | err = fmt.Errorf("astitello: sending rc cmd failed: %w", err) 746 | return 747 | } 748 | return 749 | } 750 | 751 | // SetWifi sets Wi-Fi with SSID password 752 | // I couldn't make this work (it returned 'error' even though the SSID was changed but the password was not) 753 | // If anyone manages to make it work, create an issue in github, I'm really interested in how you managed that :D 754 | func (d *Drone) SetWifi(ssid, password string) (err error) { 755 | // Send cmd 756 | if err = d.sendCmd(&cmd{ 757 | cmd: fmt.Sprintf("wifi %s %s", ssid, password), 758 | h: defaultRespHandler, 759 | timeout: defaultTimeout, 760 | }); err != nil { 761 | err = fmt.Errorf("astitello: sending wifi cmd failed: %w", err) 762 | return 763 | } 764 | return 765 | } 766 | 767 | // Wifi returns the Wifi SNR 768 | func (d *Drone) Wifi() (snr int, err error) { 769 | // Send cmd 770 | // It returns "100.0" 771 | if err = d.sendCmd(&cmd{ 772 | cmd: "wifi?", 773 | h: func(resp string) (err error) { 774 | // Parse 775 | if snr, err = strconv.Atoi(resp); err != nil { 776 | err = fmt.Errorf("astitello: atoi %s failed: %w", resp, err) 777 | return 778 | } 779 | return 780 | }, 781 | timeout: defaultTimeout, 782 | }); err != nil { 783 | err = fmt.Errorf("astitello: sending wifi? cmd failed: %w", err) 784 | return 785 | } 786 | return 787 | } 788 | 789 | // SetSpeed sets speed to x cm/s 790 | func (d *Drone) SetSpeed(x int) (err error) { 791 | // Send cmd 792 | if err = d.sendCmd(&cmd{ 793 | cmd: fmt.Sprintf("speed %d", x), 794 | h: defaultRespHandler, 795 | timeout: defaultTimeout, 796 | }); err != nil { 797 | err = fmt.Errorf("astitello: sending speed cmd failed: %w", err) 798 | return 799 | } 800 | return 801 | } 802 | 803 | // Speed returns the current speed (cm/s) 804 | func (d *Drone) Speed() (x int, err error) { 805 | // Send cmd 806 | // It returns "100.0" 807 | if err = d.sendCmd(&cmd{ 808 | cmd: "speed?", 809 | h: func(resp string) (err error) { 810 | // Parse 811 | var f float64 812 | if f, err = strconv.ParseFloat(resp, 64); err != nil { 813 | err = fmt.Errorf("astitello: parsing float %s failed: %w", resp, err) 814 | return 815 | } 816 | 817 | // Set speed 818 | x = int(f) 819 | return 820 | }, 821 | timeout: defaultTimeout, 822 | }); err != nil { 823 | err = fmt.Errorf("astitello: sending speed? cmd failed: %w", err) 824 | return 825 | } 826 | return 827 | } 828 | -------------------------------------------------------------------------------- /drone_test.go: -------------------------------------------------------------------------------- 1 | package astitello 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "reflect" 10 | "sync" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | var ( 16 | strState = "pitch:8;roll:9;yaw:10;vgx:11;vgy:12;vgz:13;templ:14;temph:15;tof:16;h:17;bat:18;baro:19.1;time:20;agx:21.1;agy:22.1;agz:23.1;" 17 | expectedState = State{Acceleration: Acceleration{X: 21.1, Y: 22.1, Z: 23.1}, Attitude: Attitude{Pitch: 8, Roll: 9, Yaw: 10}, Barometer: 19.1, Battery: 18, FlightDistance: 16, FlightTime: 20, Height: 17, HighestTemperature: 15, LowestTemperature: 14, Speed: Speed{X: 11, Y: 12, Z: 13}} 18 | ) 19 | 20 | type dialer struct { 21 | cancel context.CancelFunc 22 | ctx context.Context 23 | conn *net.UDPConn 24 | h func([]byte) []byte 25 | laddr string 26 | raddr string 27 | mt *sync.Mutex // Locks timeout 28 | rs []string 29 | t *testing.T 30 | timeout bool 31 | } 32 | 33 | func newDialer(t *testing.T, laddr, raddr string) *dialer { 34 | return &dialer{ 35 | laddr: laddr, 36 | mt: &sync.Mutex{}, 37 | raddr: raddr, 38 | t: t, 39 | } 40 | } 41 | 42 | func (d *dialer) start() (err error) { 43 | // Create context 44 | d.ctx, d.cancel = context.WithCancel(context.Background()) 45 | 46 | // Create raddr 47 | var raddr *net.UDPAddr 48 | if raddr, err = net.ResolveUDPAddr("udp", d.raddr); err != nil { 49 | err = fmt.Errorf("test: creating raddr failed: %w", err) 50 | return 51 | } 52 | 53 | // Create laddr 54 | var laddr *net.UDPAddr 55 | if laddr, err = net.ResolveUDPAddr("udp", d.laddr); err != nil { 56 | err = fmt.Errorf("test: creating laddr failed: %w", err) 57 | return 58 | } 59 | 60 | // Dial 61 | if d.conn, err = net.DialUDP("udp", laddr, raddr); err != nil { 62 | err = fmt.Errorf("test: dialing failed: %w", err) 63 | return 64 | } 65 | 66 | // Read 67 | go func() { 68 | for { 69 | // Check context 70 | if d.ctx.Err() != nil { 71 | return 72 | } 73 | 74 | // Read 75 | b := make([]byte, 2048) 76 | n, err := d.conn.Read(b) 77 | if err != nil { 78 | if d.ctx.Err() == nil { 79 | d.t.Log(fmt.Errorf("test: reading failed: %w", err)) 80 | } 81 | continue 82 | } 83 | 84 | // Append 85 | d.rs = append(d.rs, string(b[:n])) 86 | 87 | // Handle 88 | d.mt.Lock() 89 | if d.h != nil && !d.timeout { 90 | if r := d.h(b[:n]); len(r) > 0 { 91 | if _, err := d.conn.Write(r); err != nil { 92 | d.mt.Unlock() 93 | d.t.Log(fmt.Errorf("test: writing failed: %w", err)) 94 | return 95 | } 96 | } 97 | } 98 | d.mt.Unlock() 99 | } 100 | }() 101 | return 102 | } 103 | 104 | func (d *dialer) close() { 105 | if d.cancel != nil { 106 | d.cancel() 107 | } 108 | if d.conn != nil { 109 | d.conn.Close() 110 | } 111 | } 112 | 113 | func setup(t *testing.T) (d *Drone, c, s, v *dialer, err error) { 114 | // Create cmd dialer 115 | c = newDialer(t, "127.0.0.1:", respAddr) 116 | 117 | // Set cmd handler 118 | c.h = func(cmd []byte) (resp []byte) { 119 | // Log 120 | t.Logf("received cmd: '%s'", cmd) 121 | 122 | // Switch on command 123 | switch string(cmd) { 124 | case "command", "takeoff", "land", "up 1", "down 1", "left 1", "right 1", "forward 1", "back 1", "cw 1", 125 | "ccw 1", "flip l", "go 1 2 3 4", "curve 1 2 3 4 5 6 7", "wifi 1 2", "speed 1", "streamon", "streamoff": 126 | resp = []byte("ok") 127 | case "speed?": 128 | resp = []byte("100.0") 129 | case "wifi?": 130 | resp = []byte("100") 131 | } 132 | return 133 | } 134 | 135 | // Start cmd listener 136 | if err = c.start(); err != nil { 137 | err = fmt.Errorf("test: starting cmd listener failed: %w", err) 138 | return 139 | } 140 | 141 | // Create state dialer 142 | s = newDialer(t, "127.0.0.1:", stateAddr) 143 | 144 | // Start state dialer 145 | if err = s.start(); err != nil { 146 | err = fmt.Errorf("test: starting state dialer failed: %w", err) 147 | return 148 | } 149 | 150 | // Create video dialer 151 | v = newDialer(t, "127.0.0.1:", videoAddr) 152 | 153 | // Start video dialer 154 | if err = v.start(); err != nil { 155 | err = fmt.Errorf("test: starting video dialer failed: %w", err) 156 | return 157 | } 158 | 159 | // Update defaults 160 | cmdAddr = c.conn.LocalAddr().String() 161 | 162 | // Create drone 163 | d = New(nil) 164 | return 165 | } 166 | 167 | func TestDrone(t *testing.T) { 168 | // Set up 169 | d, c, s, v, err := setup(t) 170 | if err != nil { 171 | t.Error(fmt.Errorf("test: setting up failed: %w", err)) 172 | } 173 | 174 | // Make sure to close everything properly 175 | defer func() { 176 | c.close() 177 | s.close() 178 | }() 179 | 180 | // Start 181 | if err = d.Start(); err != nil { 182 | t.Error(fmt.Errorf("test: starting the drone failed: %w", err)) 183 | } 184 | defer d.Close() 185 | 186 | // Handle events 187 | me := &sync.Mutex{} // Locks events 188 | landed := false 189 | tookOff := false 190 | wg := handleEvents(t, d, &tookOff, &landed, me) 191 | 192 | // Test functions returning an error 193 | for idx, f := range []func() error{ 194 | d.Emergency, 195 | d.TakeOff, 196 | d.Land, 197 | func() error { return d.Up(1) }, 198 | func() error { return d.Down(1) }, 199 | func() error { return d.Left(1) }, 200 | func() error { return d.Right(1) }, 201 | func() error { return d.Forward(1) }, 202 | func() error { return d.Back(1) }, 203 | func() error { return d.RotateClockwise(1) }, 204 | func() error { return d.RotateCounterClockwise(1) }, 205 | func() error { return d.Flip(FlipLeft) }, 206 | func() error { return d.Go(1, 2, 3, 4) }, 207 | func() error { return d.Curve(1, 2, 3, 4, 5, 6, 7) }, 208 | func() error { return d.SetSticks(1, 2, 3, 4) }, 209 | func() error { return d.SetWifi("1", "2") }, 210 | func() error { return d.SetSpeed(1) }, 211 | func() error { return d.StartVideo() }, 212 | func() error { return d.StopVideo() }, 213 | } { 214 | if err = f(); err != nil { 215 | t.Error(fmt.Errorf("err %d should be nil", idx)) 216 | } 217 | } 218 | 219 | // Wifi 220 | var snr int 221 | if snr, err = d.Wifi(); err != nil { 222 | t.Error(fmt.Errorf("err should be nil")) 223 | } else if snr != 100 { 224 | t.Errorf("expected 100, got %d", snr) 225 | } 226 | 227 | // Speed 228 | var speed int 229 | if speed, err = d.Speed(); err != nil { 230 | t.Error(fmt.Errorf("err should be nil")) 231 | } else if snr != 100 { 232 | t.Errorf("expected 100, got %d", speed) 233 | } 234 | 235 | // Cmds 236 | e := []string{"command", "emergency", "takeoff", "land", "up 1", "down 1", "left 1", "right 1", "forward 1", 237 | "back 1", "cw 1", "ccw 1", "flip l", "go 1 2 3 4", "curve 1 2 3 4 5 6 7", "rc 1 2 3 4", "wifi 1 2", "speed 1", 238 | "streamon", "streamoff", "wifi?", "speed?"} 239 | if !reflect.DeepEqual(c.rs, e) { 240 | t.Errorf("expected cmds %+v, got %+v", e, c.rs) 241 | } 242 | 243 | // Test events 244 | testEvents(t, &tookOff, &landed, wg, s, v, me) 245 | 246 | // Timeout 247 | defaultTimeout = time.Millisecond 248 | c.mt.Lock() 249 | c.timeout = true 250 | c.mt.Unlock() 251 | if err = d.command(); err == nil || !errors.Is(err, context.DeadlineExceeded) { 252 | t.Errorf("error should be %s", context.DeadlineExceeded) 253 | } 254 | c.mt.Lock() 255 | c.timeout = false 256 | c.mt.Unlock() 257 | } 258 | 259 | func handleEvents(t *testing.T, d *Drone, tookOff, landed *bool, m *sync.Mutex) (wg *sync.WaitGroup) { 260 | // Create wait group 261 | wg = &sync.WaitGroup{} 262 | 263 | // State events 264 | wg.Add(1) 265 | d.On(StateEvent, StateEventHandler(func(s State) { 266 | defer wg.Done() 267 | 268 | // Check state 269 | if s != expectedState { 270 | t.Errorf("expected state %+v, got %+v", expectedState, s) 271 | } else if d.State() != s { 272 | t.Error("state has not been updated") 273 | } 274 | })) 275 | 276 | // Video events 277 | wg.Add(1) 278 | d.On(VideoPacketEvent, VideoPacketEventHandler(func(p []byte) { 279 | defer wg.Done() 280 | 281 | // Check packet 282 | if !bytes.Equal(p, []byte("packet")) { 283 | t.Errorf("expected packet, got %s", p) 284 | } 285 | })) 286 | 287 | // Take off event 288 | wg.Add(1) 289 | d.On(TakeOffEvent, func(interface{}) { 290 | defer wg.Done() 291 | 292 | m.Lock() 293 | *tookOff = true 294 | m.Unlock() 295 | }) 296 | 297 | // Land event 298 | wg.Add(1) 299 | d.On(LandEvent, func(interface{}) { 300 | defer wg.Done() 301 | 302 | m.Lock() 303 | *landed = true 304 | m.Unlock() 305 | }) 306 | return 307 | } 308 | 309 | func testEvents(t *testing.T, tookOff, landed *bool, wg *sync.WaitGroup, s, v *dialer, m *sync.Mutex) { 310 | // Trigger state event 311 | if _, err := s.conn.Write([]byte(strState)); err != nil { 312 | t.Error(fmt.Errorf("test: writing state failed: %w", err)) 313 | } 314 | 315 | // Trigger video event 316 | if _, err := v.conn.Write([]byte("packet")); err != nil { 317 | t.Error(fmt.Errorf("test: writing video packet failed: %w", err)) 318 | } 319 | 320 | // Wait 321 | wg.Wait() 322 | 323 | // Lock 324 | m.Lock() 325 | defer m.Unlock() 326 | 327 | // Check 328 | if !*tookOff { 329 | t.Error("expected tookoff == true, got false") 330 | } else if !*landed { 331 | t.Error("expected landed == true, got false") 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os/exec" 8 | 9 | "github.com/asticode/go-astikit" 10 | "github.com/asticode/go-astitello" 11 | ) 12 | 13 | func main() { 14 | // Create logger 15 | l := log.New(log.Writer(), log.Prefix(), log.Flags()) 16 | 17 | // Create worker 18 | w := astikit.NewWorker(astikit.WorkerOptions{Logger: l}) 19 | 20 | // Create the drone 21 | d := astitello.New(l) 22 | 23 | // Handle signals 24 | w.HandleSignals(astikit.TermSignalHandler(func() { 25 | // Make sure to land on term signal 26 | if err := d.Land(); err != nil { 27 | l.Println(fmt.Errorf("main: landing failed: %w", err)) 28 | return 29 | } 30 | })) 31 | 32 | // Check whether ffmpeg exists on the machine 33 | var video bool 34 | if _, err := exec.LookPath("ffmpeg"); err == nil { 35 | // Execute ffmpeg 36 | var in io.WriteCloser 37 | if _, err = astikit.ExecCmd(w, astikit.ExecCmdOptions{ 38 | Args: []string{"-y", "-i", "pipe:0", "example.ts"}, 39 | CmdAdapter: func(cmd *exec.Cmd, h *astikit.ExecHandler) (err error) { 40 | // Pipe stdin 41 | if in, err = cmd.StdinPipe(); err != nil { 42 | err = fmt.Errorf("main: piping stdin failed: %w", err) 43 | return 44 | } 45 | 46 | // Handle new video packets 47 | d.On(astitello.VideoPacketEvent, astitello.VideoPacketEventHandler(func(p []byte) { 48 | // Check status 49 | if h.Status() != astikit.ExecStatusRunning { 50 | return 51 | } 52 | 53 | // Write the packet in stdin 54 | if _, err := in.Write(p); err != nil { 55 | l.Println(fmt.Errorf("main: writing video packet failed: %w", err)) 56 | return 57 | } 58 | })) 59 | return 60 | }, 61 | Name: "ffmpeg", 62 | }); err != nil { 63 | l.Println(fmt.Errorf("main: executing ffmpeg failed: %w", err)) 64 | return 65 | } 66 | defer in.Close() 67 | 68 | // Update 69 | video = true 70 | } else { 71 | // Log 72 | l.Println("main: ffmpeg was not found, video won't be started") 73 | } 74 | 75 | // Handle take off event 76 | d.On(astitello.TakeOffEvent, func(interface{}) { l.Println("main: drone has took off!") }) 77 | 78 | // Start the drone 79 | if err := d.Start(); err != nil { 80 | l.Println(fmt.Errorf("main: starting to the drone failed: %w", err)) 81 | return 82 | } 83 | defer d.Close() 84 | 85 | // Execute in a task 86 | w.NewTask().Do(func() { 87 | // Start video 88 | if video { 89 | if err := d.StartVideo(); err != nil { 90 | l.Println(fmt.Errorf("main: starting video failed: %w", err)) 91 | return 92 | } 93 | } 94 | 95 | // Take off 96 | if err := d.TakeOff(); err != nil { 97 | l.Println(fmt.Errorf("main: taking off failed: %w", err)) 98 | return 99 | } 100 | 101 | // Flip 102 | if err := d.Flip(astitello.FlipRight); err != nil { 103 | l.Println(fmt.Errorf("main: flipping failed: %w", err)) 104 | return 105 | } 106 | 107 | // Log state 108 | l.Printf("main: state is: %+v\n", d.State()) 109 | 110 | // Land 111 | if err := d.Land(); err != nil { 112 | l.Println(fmt.Errorf("main: landing failed: %w", err)) 113 | return 114 | } 115 | 116 | // Stop video 117 | if video { 118 | if err := d.StopVideo(); err != nil { 119 | l.Println(fmt.Errorf("main: stopping video failed: %w", err)) 120 | return 121 | } 122 | } 123 | 124 | // Stop worker 125 | w.Stop() 126 | }) 127 | 128 | // Wait 129 | w.Wait() 130 | } 131 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/asticode/go-astitello 2 | 3 | go 1.13 4 | 5 | require github.com/asticode/go-astikit v0.2.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/asticode/go-astikit v0.2.0 h1:QonRVJKQB2btMYZGW+YkibMDOXje2F49RLW4UCnyjns= 2 | github.com/asticode/go-astikit v0.2.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= 3 | -------------------------------------------------------------------------------- /state.go: -------------------------------------------------------------------------------- 1 | package astitello 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // State represents the drone's state 8 | type State struct { 9 | Acceleration Acceleration // The acceleration 10 | Attitude Attitude // The attitude 11 | Barometer float64 // The barometer measurement in cm 12 | Battery int // The percentage of the current battery level 13 | FlightDistance int // The time of flight distance in cm 14 | FlightTime int // The amount of time the motor has been used 15 | Height int // The height in cm 16 | HighestTemperature int // The highest temperature in degree Celsius 17 | LowestTemperature int // The lowest temperature in degree Celsius 18 | Speed Speed // The speed 19 | } 20 | 21 | // Acceleration represents the drone's acceleration 22 | type Acceleration struct { 23 | X float64 24 | Y float64 25 | Z float64 26 | } 27 | 28 | // Attitude represents the drone's attitude 29 | type Attitude struct { 30 | Pitch int // The degree of the attitude pitch 31 | Roll int // The degree of the attitude roll 32 | Yaw int // The degree of the attitude yaw 33 | } 34 | 35 | // Speed represents the drone's speed 36 | type Speed struct { 37 | X int 38 | Y int 39 | Z int 40 | } 41 | 42 | func newState(i string) (s State, err error) { 43 | var n int 44 | if n, err = fmt.Sscanf(i, "pitch:%d;roll:%d;yaw:%d;vgx:%d;vgy:%d;vgz:%d;templ:%d;temph:%d;tof:%d;h:%d;bat:%d;baro:%f;time:%d;agx:%f;agy:%f;agz:%f;", &s.Attitude.Pitch, &s.Attitude.Roll, &s.Attitude.Yaw, &s.Speed.X, &s.Speed.Y, &s.Speed.Z, &s.LowestTemperature, &s.HighestTemperature, &s.FlightDistance, &s.Height, &s.Battery, &s.Barometer, &s.FlightTime, &s.Acceleration.X, &s.Acceleration.Y, &s.Acceleration.Z); err != nil { 45 | err = fmt.Errorf("astitello: scanf failed: %w", err) 46 | return 47 | } else if n != 16 { 48 | err = fmt.Errorf("astitello: scanf only parsed %d items, expected 10", n) 49 | return 50 | } 51 | return 52 | } 53 | --------------------------------------------------------------------------------