forked from goodrobots/vision_landing
-
Notifications
You must be signed in to change notification settings - Fork 0
/
vision_landing
executable file
·729 lines (687 loc) · 34.1 KB
/
vision_landing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vision_landing
# https://github.com/fnoop/vision_landing
#
# This python script connects to arducopter using dronekit, to control precision landing.
# The control is purely visual using pose estimation from fiducial markers (eg. aruco or april tags), no additional rangefinder is needed.
# It launches a separate program track_targets which does the actual cv work and captures the resulting vectors from non-blocking thread handler.
from threading import Thread, Event
from Queue import LifoQueue, Queue, Empty
from subprocess import Popen, PIPE
from dronekit import connect, VehicleMode, LocationGlobal, LocationGlobalRelative
from pymavlink import mavutil
from time import sleep, time
from datetime import datetime, timedelta
from sys import exit, stdout, stderr
from os import path, makedirs, symlink, remove
from math import pi
from re import sub, search
from collections import deque
from math import sqrt
import signal
import logging
import ctypes
### ================================
### Define Classes
### ================================
# Threaded Reader class connects the output from the track_targets process to a non-blocking reader, and adds any messages to a queue.
# The queue is then read by the main loop and if anything is waiting it calls a method to process from the track_targets object.
class ThreadedReader:
def __init__(self, stdout, stderr):
self.stdout = stdout
self.stderr = stderr
self.queue = LifoQueue()
self._stop = Event()
def insertQueue(pipe, queue, qtype="stdout"):
while not self.stopped():
self.clear() # Clear the queue before adding anything - we only want the latest data
line = pipe.readline().rstrip() + ":{:.0f}".format(monotonic_time())
if args.verbose:
log.debug("Adding line: {} : {}".format(qtype, line))
if line and qtype == "stdout":
queue.put(line)
elif line and qtype == "stderr":
queue.put("error:"+line)
self.threadout = Thread(target = insertQueue, args = (self.stdout, self.queue))
self.threadout.daemon = True
self.threadout.start()
self.threaderr = Thread(target = insertQueue, args = (self.stderr, self.queue, "stderr"))
self.threaderr.daemon = True
self.threaderr.start()
def size(self):
return self.queue.qsize()
def readline(self, timeout = None):
try:
line = self.queue.get(block = timeout is not None, timeout = timeout)
self.queue.task_done()
return line
except Empty:
log.debug("empty queue")
return None
def clear(self):
#with self.queue.mutex:
# self.queue.clear()
while self.size() > 10:
# self.readline()
tmpline = self.queue.get(False)
log.debug("clearing queue: {} : {}".format(self.size(), tmpline))
self.queue.task_done()
return
def stop(self):
self._stop.set()
def stopped(self):
return self._stop.isSet()
# TrackTime class is used to synchronise time between the companion computer and the flight controller.
# This is necessary to ensure that the vision frames are matched as closely as possible with the inertial frames.
# Note: This requires a Craft object to be passed, in order to perform the actual sync.
class TrackTime:
def __init__(self, craft):
self.fctime_nanos = 0
self.mytime_nanos = 0
self.local_tracker = {}
self.delta = None
self.difference = None
self.delta_buffer = deque(maxlen=50) # Circular buffer to track timesync deltas
self.difference_buffer = deque(maxlen=50) # Circular buffer to track difference between two systems
self.debug = False
self.vehicle = craft.vehicle
# Setup time struct to hold local monotonic clock queries
self.t = timespec()
# Setup listener decorator
@self.vehicle.on_message("TIMESYNC")
def listener_timesync(subself, name, message):
try:
if message.ts1 < self.mytime_nanos:
#log.info("Newer timesync message already received, ignoring")
return False
self.mytime_nanos = message.ts1 # Set the time that the message was sent
_cts = self.current_timestamp()
_avg_ts = (self.local_tracker[message.ts1] + _cts)/2
self._delta = _cts - _avg_ts # Calculuate the time delta
self.delta_buffer.append(self._delta) # Push the delta into the circular buffer
# If the averaged delta has been reached, use that, and update the delta from the latest buffer
if self.delta:
self.delta = sum(list(self.delta_buffer)) / len(list(self.delta_buffer))
self.fctime_nanos = message.tc1 - self.delta
# Otherwise use the spot delta from the incoming message
else:
self.fctime_nanos = message.tc1 - self._delta
self.difference_buffer.append(self.mytime_nanos - self.fctime_nanos)
self.difference = sum(list(self.difference_buffer)) / len(list(self.difference_buffer))
if self.debug:
log.debug("Timesync message received:"+", ".join([str(message), str(self._delta), str(self.fctime_nanos)]))
except:
if self.debug:
log.debug("Error, unexpected timesync timestamp received:"+str(message.ts1))
# Blocking method to call once during startup, to calculate delta
def initial_sync(self):
# Keep sending timesync requests until the delta buffer is full, or a timeout is reached
start_timesync = self.current_timestamp()
while len(list(self.delta_buffer)) < 50 and self.current_timestamp() - start_timesync < (15 * 1000000000):
self.update()
sleep(0.1) # Ardupilot can't cope with too many timesync messages being sent in a short time period, the results are nonsense
# When full, average the delta buffer and then set that to use as the ongoing delta
if len(self.delta_buffer) == 50:
self.delta = sum(list(self.delta_buffer)) / len(list(self.delta_buffer))
else:
log.warning("Error creating timesync delta buffer - unpredictable results will follow")
# Method to return monotonic clock time
def current_timestamp(self):
return monotonic_time()
# Request a timesync update from the flight controller
def update(self, ts=0, tc=0):
if ts == 0:
ts = self.current_timestamp()
self.local_tracker[ts] = self.current_timestamp() # Create entry to correlate sending timestamp with actual localtime
msg = self.vehicle.message_factory.timesync_encode(
tc, # tc1
ts # ts1
)
self.vehicle.send_mavlink(msg)
self.vehicle.flush()
# Note: This returns the 'actual' time of the FC with the delay compensated from the last sync event
def actual(self):
return self.fctime_nanos
# Return actual + time elapsed since actual set
def estimate(self):
estmynanos = self.current_timestamp() - self.difference
estfcnanos = self.actual() + (self.current_timestamp() - self.mytime_nanos)
#log.debug("Estimate time: current-difference:"+str(estmynanos/1000000)+", fctime_nanos+elapsed:"+str(estfcnanos/1000000)+", difference:"+str((estmynanos - estfcnanos) / 1000000))
return estmynanos
def toFcTime(self, inTime):
return inTime - self.difference
# TrackTargets class launches, controls and handles the separate track_targets process that does the actual vision processing.
class TrackTargets:
def __init__(self, args, calibration):
self.state = None
self.shutdown = False
# Setup timing containers
self.frame_times = []
self.start_t = time()
# Work out starting parameters
self.setup(args, calibration)
# Launch tracking process and process output
log.info("Launching track_targets with arguments:" + " ".join(self.track_arguments))
self.launch()
def setup(self, args, calibration):
# Define the basic arguments to track_targets
self.track_arguments = [cwd+'/track_targets']
# If marker dictionary is set, pass it through
if args.markerdict:
self.track_arguments.extend(['-d', args.markerdict])
# If output is set, pass it through
if args.output:
now = datetime.now()
nowtime = now.strftime("%Y-%m-%d-%H-%M-%S")
args_output = sub("-xxx", "-"+str(nowtime), args.output)
self.track_arguments.extend(['-o', args_output])
# If marker id is set, pass it through
if args.markerid:
self.track_arguments.extend(['-i', args.markerid])
# If width is set, pass it through
if args.width:
self.track_arguments.extend(['-w', str(args.width)])
# If height is set, pass it through
if args.height:
self.track_arguments.extend(['-g', str(args.height)])
# If fps is set, pass it through
if args.fps:
self.track_arguments.extend(['-f', str(args.fps)])
# If verbose is set, pass it through
if args.verbose:
self.track_arguments.extend(['-v'])
# If brightness is set, pass it through
if args.brightness:
self.track_arguments.extend(['-b', str(args.brightness)])
# If fourcc codec is set, pass it through
if args.fourcc:
self.track_arguments.extend(['-c', args.fourcc])
# If sizemapping is set, pass it through
if args.sizemapping:
self.track_arguments.extend(['-z', args.sizemapping])
# If markerhistory is set, pass it through
if args.markerhistory:
self.track_arguments.extend(['--markerhistory', str(args.markerhistory)])
# If markerthreshold is set, pass it through
if args.markerthreshold:
self.track_arguments.extend(['--markerthreshold', str(args.markerthreshold)])
# Add positional arguments
self.track_arguments.extend([args.input, calibration, str(args.markersize)])
def launch(self):
self.process = Popen(self.track_arguments, stdin = PIPE, stdout = PIPE, stderr = PIPE, shell = False, bufsize=1)
self.reader = ThreadedReader(self.process.stdout, self.process.stderr)
self.state = "initialising"
def processline(self, line):
# Unpack data from track_targets data
trackdata = line.rstrip().split(":")
# If not target data, log and skip to next data
if trackdata[0] != "target":
datatype = trackdata[0]
if trackdata[0] == "error":
log.error("track_targets: " +str(trackdata[1:]))
elif trackdata[0] == "debug" and args.verbose:
log.debug("track_targets: " + datatype[0].upper() + datatype[1:] + str(trackdata[1:]))
elif trackdata[0] == "info":
log.info("track_targets: " + str(trackdata[1:]))
if trackdata[1] == "initcomp":
self.state = "initialised"
return
# Unpack and cast target data to correct types
try:
[id,x,y,z,processtime,sensorTimestamp,sendTimestamp,receiveTimestamp] = trackdata[1:]
[id,x,y,z,processtime,sensorTimestamp,sendTimestamp,receiveTimestamp] = [int(id),float(x),float(y),float(z),float(processtime),float(sensorTimestamp),float(sendTimestamp),float(receiveTimestamp)]
except:
log.debug("track_targets: error in data: " + str(line.rstrip()))
return
# If fake rangefinder distance is required, send it
if args.fakerangefinder:
craft.send_distance_message(int(z*100))
# Send the landing_target message to arducopter
# NOTE: This sends the current estimated fctime, NOT the time the frame was actually captured.
# So this does not allow for the CV processing time. This should be sending when the frame is actually captured, before processing.
nowTime = monotonic_time()
# If data is less than 250ms since the image shuttr, send the message.
if nowTime - sensorTimestamp < 250 * 1e+6:
craft.send_land_message(x, y, z, tracktime.toFcTime(sensorTimestamp) / 1000, id) # Send sensor timestamp adjusted to timesync, in micros
# Track frame timing to calculate fps, running average over last 10 frames
end_t = time()
time_taken = end_t - self.start_t
self.start_t = end_t
self.frame_times.append(time_taken)
self.frame_times = self.frame_times[-10:]
fps = len(self.frame_times) / sum(self.frame_times)
log.info("Fctime:{:.0f}:{:.0f}, Fps:{:.2f}, Alt:{}, Rng:{}, Marker:{}, Distance:{}, xOffset:{}, yOffset:{}, processTime:{:.0f}, sensorTime:{:.0f}, sendTime:{:.0f}, receiveTime:{:.0f}, currentTime:{:.0f}, sensorDiff:{}, sendDiff:{}, receiveDiff:{}".format(tracktime.actual(), tracktime.estimate(), fps, craft.vehicle.location.global_relative_frame.alt, craft.vehicle.rangefinder.distance, id, z, x, y, processtime, sensorTimestamp, sendTimestamp, receiveTimestamp, nowTime, nowTime - sensorTimestamp, nowTime - sendTimestamp, nowTime - receiveTimestamp))
def start(self):
if self.state == "initialised":
log.info("Requesting track_targets to start tracking:"+str(craft.vehicle.mode))
self.process.send_signal(signal.SIGUSR1)
self.state = "started"
else:
log.info("Request to start track_targets tracking failed, state is not 'initialised'")
def stop(self):
log.info("Requesting track_targets to stop tracking:"+str(craft.vehicle.mode))
self.state = "initialised"
self.process.send_signal(signal.SIGUSR2)
def end(self):
log.info("Shutting down track_targets")
# Stop threaded reader
self.reader.stop()
# track_targets should always be stopped by now, but be sure
self.process.poll()
if self.process.returncode == None:
self.process.terminate()
self.shutdown = True
# The Craft class connects to the flight controller and sends controlling messages
class Craft:
def __init__(self, connectstr):
self.debug = False
self.mode = None
self.connected = None
self.vehicle = None
self.connect(connectstr)
self.precloiter_opt = self.find_precloiter_opt()
if self.precloiter_opt:
log.info("Precision loiter switch found: Channel "+str(self.precloiter_opt))
def connect(self, connectstr):
try:
# self.vehicle = connect(connectstr, wait_ready=True, rate=1)
# self.vehicle = connect(connectstr, wait_ready=True)
# self.vehicle = connect(connectstr, wait_ready=['system_status','mode'], baud=921600)
self.vehicle = connect(connectstr, wait_ready=['system_status','mode'])
self.connected = True
except Exception,e:
log.critical("Error connecting to vehicle:"+str(repr(e)))
self.connected = False
# Define function to send distance_message mavlink message for mavlink based rangefinder, must be >10hz
# http://mavlink.org/messages/common#DISTANCE_SENSOR
def send_distance_message(self, dist):
msg = self.vehicle.message_factory.distance_sensor_encode(
0, # time since system boot, not used
1, # min distance cm
10000, # max distance cm
dist, # current distance, must be int
0, # type = laser?
0, # onboard id, not used
mavutil.mavlink.MAV_SENSOR_ROTATION_PITCH_270, # must be set to MAV_SENSOR_ROTATION_PITCH_270 for mavlink rangefinder, represents downward facing
0 # covariance, not used
)
self.vehicle.send_mavlink(msg)
self.vehicle.flush()
if args.verbose:
log.debug("Sending mavlink distance_message:" +str(dist))
# Define function to send landing_target mavlink message for mavlink based precision landing
# http://mavlink.org/messages/common#LANDING_TARGET
def send_land_message(self, x,y,z, time_usec=0, target_num=0):
msg = self.vehicle.message_factory.landing_target_encode(
time_usec, # time target data was processed, as close to sensor capture as possible
target_num, # target num, not used
mavutil.mavlink.MAV_FRAME_BODY_NED, # frame, not used
x, # X-axis angular offset, in radians
y, # Y-axis angular offset, in radians
z, # distance, in meters
0, # Target x-axis size, in radians
0, # Target y-axis size, in radians
0, # x float X Position of the landing target on MAV_FRAME
0, # y float Y Position of the landing target on MAV_FRAME
0, # z float Z Position of the landing target on MAV_FRAME
(1,0,0,0), # q float[4] Quaternion of landing target orientation (w, x, y, z order, zero-rotation is 1, 0, 0, 0)
2, # type of landing target: 2 = Fiducial marker
1, # position_valid boolean
)
self.vehicle.send_mavlink(msg)
self.vehicle.flush()
if args.verbose:
log.debug("Sending mavlink landing_target - time_usec:{:.0f}, x:{}, y:{}, z:{}".format(time_usec, str(x), str(y), str(z)))
# Define function that arms and takes off, used for testing in SITL
# Lifted from dronekit-python examples
def arm_and_takeoff(self, targetAltitude):
log.info("Basic pre-arm checks")
# Don't let the user try to arm until autopilot is ready
while not self.vehicle.is_armable:
log.info(" Waiting for vehicle to initialise...")
sleep(1)
log.info("Arming motors")
# Copter should arm in GUIDED mode
self.vehicle.mode = VehicleMode("GUIDED")
self.vehicle.armed = True
while not self.vehicle.armed:
log.info(" Waiting for arming...")
sleep(1)
log.info("Taking off!")
self.vehicle.simple_takeoff(targetAltitude) # Take off to target altitude
# Wait until the vehicle reaches a safe height before processing the goto (otherwise the command
# after Vehicle.simple_takeoff will execute immediately).
while True:
log.info(" Altitude: " + str(self.vehicle.location.global_relative_frame.alt))
if self.vehicle.location.global_relative_frame.alt>=targetAltitude*0.95: #Trigger just below target alt.
log.info("Reached target altitude")
break
sleep(1)
# Scan through option channels for one set to 39 (precision loiter)
def find_precloiter_opt(self):
_optch = None
for ch in range(7,16):
try:
if self.vehicle.parameters['CH'+str(ch)+'_OPT'] == 39:
_optch = ch
except:
pass
return _optch
# Class for signal tracking and handling. This is used to accept Ctrl-C or SIGHUP/SIGKILL type events.
class SigTrack:
def __init__(self):
self.counter = 0
def add(self):
self.counter += 1
def count(self):
return self.counter
def handle_sig(self, signal, frame):
log.info("Signal handler called, calling shutdown and cleanup logic")
track_targets.end()
sigtracker.add()
log.debug("Sigtracker count:"+str(sigtracker.count()))
if sigtracker.count() >= 3:
log.warning("Signal handler called three times, exiting immediately")
exit(1)
### ================================
### End Define Classes
### ================================
### ================================
### Define Functions
### ================================
# Return correctly typed config parser option
def get_config_value(parser, section, option):
"""Parses config value as python type"""
try:
return parser.getint(section, option)
except ValueError:
pass
try:
return parser.getfloat(section, option)
except ValueError:
pass
try:
return parser.getboolean(section, option)
except ValueError:
pass
return parser.get(section, option)
### ================================
### End Define Functions
### ================================
### --------------------------------
### Parse arguments, setup logging
### --------------------------------
# Find full directory path of this script, used for loading config and other files
cwd = path.dirname(path.abspath(__file__))
# Find current time, used for dating files
now = datetime.now()
nowtime = now.strftime("%Y-%m-%d-%H-%M-%S")
# Configure argument parsing
import argparse
import ConfigParser
parser = argparse.ArgumentParser(description='Vision based precision landing')
parser.add_argument('--config', '-c', default=cwd+"/vision_landing.conf", help="config file location, defaults to same directory as vision_landing")
parser.add_argument('--connect', help="dronekit vehicle connection target string, eg. /dev/ttyS0, tcp:localhost:5770, udp:localhost:14560")
parser.add_argument('--markersize', help="Target marker size, in meters, required")
parser.add_argument('--sizemapping', '-z', help="Marker Size Mappings, in marker_id:size format, comma separated")
parser.add_argument('--markerhistory', help="Marker tracking history, in frames, defaults to 15")
parser.add_argument('--markerthreshold', help="Marker tracking threshold, in percentage, defaults to 50")
parser.add_argument('--calibration', help="camera calibration data, required")
parser.add_argument('--input', '-i', default="/dev/video0", help="camera input, defaults to /dev/video0")
parser.add_argument('--output', '-o', help="gstreamer output pipeline, defaults to none")
parser.add_argument('--markerdict', '-d', default="ARUCO_MIP_36h12", help="Target marker dictionary, defaults to ARUCO_MIP_36h12")
parser.add_argument('--markerid', '-id', help="Target ID (optional, if not specified will use closest target)")
parser.add_argument('--simulator', '-s', action='store_true', help="Perform initial simulator actions for testing, takeoff and initiate land")
parser.add_argument('--width', '-w', help="Video Input Resolution - Width")
parser.add_argument('--height', '-g', help="Video Input Resolution - Height")
parser.add_argument('--fps', '-f', help="Video Output FPS - Kludge factor")
parser.add_argument('--verbose', '-v', action='store_true', help="Verbose/Debug output")
parser.add_argument('--brightness', '-b', help="Camera Brightness/Gain")
parser.add_argument('--fourcc', '-u', help="Specify FourCC codec")
parser.add_argument('--fakerangefinder', '-r', action='store_true', help="Fake RangeFinder Data")
parser.add_argument('--logdir', '-l', help="Log directory, if not specified will log to stdout")
parser.add_argument('--controlprocessing', '-p', action='store_true', help="Control Vision Processing - enable/disable based on arming status and landing/precloiter mode")
parser.add_argument('--estimator', '-e', help="ACPrecLand estimator, 0=Raw, 1=EKF. Defaults to 0 (Raw)")
args = parser.parse_args()
# First parse config file, and set defaults
defaults = {}
if path.isfile(args.config):
config = ConfigParser.SafeConfigParser()
config.read([args.config])
for key, value in config.items("Defaults"):
defaults[key] = get_config_value(config, "Defaults", key)
parser.set_defaults(**defaults)
args = parser.parse_args()
else:
print
print "Error: Config file "+str(args.config)+" does not exist"
print
exit(1)
# If we don't have the mandatory arguments, exit
if not args.calibration or not args.markersize or not args.connect:
print
if not args.calibration:
print "Error: missing required argument 'calibration'"
if not args.markersize:
print "Error: missing required argument 'markersize'"
if not args.connect:
print "Error: missing required argument 'connect'"
print
parser.print_usage()
exit(1)
# Setup logging
console = logging.StreamHandler()
if args.logdir:
# If logdir specified, create if it doesn't exist
if not path.exists(args.logdir):
makedirs(args.logdir)
# Add timestamped logfile out
logging.basicConfig(filename=args.logdir+'/vision_landing.'+nowtime+'.log', format='%(asctime)s %(levelname)s %(message)s', level=logging.DEBUG)
# Create handly symlink to current logfile
if path.exists(args.logdir+'/last.log'):
remove(args.logdir+'/last.log')
symlink(args.logdir+'/vision_landing.'+nowtime+'.log', args.logdir+'/last.log')
else:
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.DEBUG)
log = logging.getLogger(__name__)
# Check if calibration data exists
if path.isfile(cwd+"/calibration/"+args.calibration):
calibration = cwd+"/calibration/"+args.calibration
elif path.isfile(args.calibration):
calibration = args.calibration
else:
log.critical("Error: specified calibration does not exist.")
exit(1)
### --------------------------------
### Start track_targets CV process
### and attach threaded reader
### --------------------------------
# Check track_targets exists
if not path.isfile(cwd+"/track_targets"):
log.critical("Error: track_targets does not exist. Please run 'make install' in src/ directory")
exit(1)
# Run the vision tracker as a thread
try:
track_targets = TrackTargets(args, calibration)
sleep(1)
except Exception as error:
log.critical("Error starting track_targets:"+ str(error))
exit(1)
track_targets.process.poll()
if track_targets.process.returncode != None:
log.critical("Error starting track_targets")
exit(1)
# Define a function that stops and clears up cv process
def cleanup():
# Do a final check to see if tracker process still running, try to shut down if so
track_targets.process.poll()
if track_targets.process.returncode == None:
track_targets.end()
# Close connection to the drone
try:
craft.vehicle.close()
except:
pass
### --------------------------------
### Start up procedure - setup signal handlers,
### connect to drone, start syncing time with drone,
### setup drone parameters
### --------------------------------
# Setup signal handlers
sigtracker = SigTrack()
signal.signal(signal.SIGINT, sigtracker.handle_sig)
signal.signal(signal.SIGTERM, sigtracker.handle_sig)
# Setup monotonic clock
CLOCK_MONOTONIC_RAW = 4
class timespec(ctypes.Structure):
_fields_ = [
('tv_sec', ctypes.c_long),
('tv_nsec', ctypes.c_long)
]
librt = ctypes.CDLL('librt.so.1', use_errno=True)
clock_gettime = librt.clock_gettime
clock_gettime.argtypes = [ctypes.c_int, ctypes.POINTER(timespec)]
def monotonic_time():
t = timespec()
if clock_gettime(CLOCK_MONOTONIC_RAW , ctypes.pointer(t)) != 0:
errno_ = ctypes.get_errno()
raise OSError(errno_, os.strerror(errno_))
return (t.tv_sec * 1e+9) + t.tv_nsec
# Connect to the Vehicle
log.info("Connecting to drone on: %s" % args.connect)
craft = Craft(args.connect)
if not craft.connected:
log.critical("Error: Could not connect to drone")
cleanup()
exit(1)
else:
log.info("Connected to drone")
# Create a new TrackTime object to try and keep sync with the flight controller
log.info("Starting time synchronisation with flight controller")
tracktime = TrackTime(craft)
tracktime.debug = False
# Perform the initial sync with the flight controller. This blocks until the sync is complete and good enough.
while True:
tracktime.initial_sync()
if not tracktime.delta:
log.warning("Timesync not supported by flight controller. THIS CAN LEAD TO DANGEROUS AND UNPREDICTABLE BEHAVIOUR.")
break
log.info("Initial timesync complete, offset is {} ms".format(tracktime.delta/1000000))
log.info("Initial timesync difference is {} ms".format(tracktime.difference/1000000))
log.info("Initial timesync delta buffer: {}".format(list(tracktime.delta_buffer)))
_dvariance = map(lambda x: (x - tracktime.delta)**2, tracktime.delta_buffer)
_variance = sum(_dvariance) * 1.0 / len(_dvariance)
# Calculate standard deviation of delta (latency) variance
# A median standard deviation would probably be better here to represent jitter
stddev = round((_variance ** 0.5 / 1000000), 2)
jitterpercent = round((stddev / (tracktime.delta/1000000)) * 100, 2)
log.info("Initial timesync jitter (standard deviation of delta buffer): {} ms, {}% of average latency".format(stddev, jitterpercent))
if tracktime.delta/1000000 >= 40:
log.warning("Error: Timesync not good enough, trying again")
tracktime.delta_buffer.clear() # Clear the buffer to force a fresh resync
continue
else:
break
if jitterpercent > 25:
log.info("WARNING: Timesync jitter is very high. This will signficantly affect video frame -> inertial frame correlation.")
# Set some parameters important to precision landing
log.info("Setting flight controller parameters")
craft.vehicle.parameters['PLND_ENABLED'] = 1
craft.vehicle.parameters['PLND_TYPE'] = 1 # Mavlink landing backend
# Set estimator type to 'raw', as ekf estimator can be very erratic
try:
if 'PLND_EST_TYPE' in craft.vehicle.parameters:
try:
craft.vehicle.parameters['PLND_EST_TYPE'] = int(args.estimator)
log.info("plnd estimator: {}, {}".format(craft.vehicle.parameters['PLND_EST_TYPE'], args.estimator))
except Exception as e:
log.info("plnd estimator error: {} - setting estimator to type 0".format(repr(e)))
craft.vehicle.parameters['PLND_EST_TYPE'] = 0
except:
pass
# Set the precland buffer to 250ms. This should give enough buffer space for even very slow computers
try:
if 'PLND_BUFFER' in craft.vehicle.parameters:
craft.vehicle.parameters['PLND_BUFFER'] = 250
except:
pass
if args.fakerangefinder:
# Following are for mavlink based rangefinder (not needed for 3.5-rc2+)
craft.vehicle.parameters['RNGFND_TYPE'] = 10
craft.vehicle.parameters['RNGFND_MIN_CM'] = 1
craft.vehicle.parameters['RNGFND_MAX_CM'] = 10000
craft.vehicle.parameters['RNGFND_GNDCLEAR'] = 5
log.info("Faking RangeFinder data with distance_sensor messages")
# If simulator option set, perform initial takeoff and initiate land
if args.simulator:
if args.fakerangefinder:
# Following are for SITL rangefinder
craft.vehicle.parameters['RNGFND_TYPE'] = 1
craft.vehicle.parameters['RNGFND_MIN_CM'] = 0
craft.vehicle.parameters['RNGFND_MAX_CM'] = 4000
craft.vehicle.parameters['RNGFND_PIN'] = 0
craft.vehicle.parameters['RNGFND_SCALING'] = 12.12
# Take off to 25m altitude and start landing if not already armed
if not craft.vehicle.armed:
craft.arm_and_takeoff(100)
# Start landing
log.info("Starting landing...")
craft.vehicle.mode = VehicleMode("LAND")
# Clear the vision tracking queue, we don't want to blast mavlink messages for everything stacked up
while True:
line = track_targets.reader.readline()
if not line:
break
elif not search("^target:", line):
if search("^error:", line):
log.error("track_targets: "+sub("^error:","",line))
# Make sure to catch and process initcomp if it's in this initial queue
elif search("initcomp", line):
track_targets.processline(line)
else:
log.info("track_targets: "+line)
### --------------------------------
### Main Loop
### Monitor for landing/precloiter mode
### Listen for incoming cv tracking results
### --------------------------------
log.info("Entering main tracking loop")
loop_counter = 0
while True:
# Update timesync
tracktime.update()
# If craft mode has changed, take action
if (args.controlprocessing and (craft.vehicle.armed and (craft.vehicle.mode == "LAND" or (craft.vehicle.mode == "LOITER" and (craft.precloiter_opt and craft.vehicle.channels[str(craft.precloiter_opt)] > 1800))))) and track_targets.state == "initialised":
track_targets.start()
elif (args.controlprocessing and (not craft.vehicle.armed or (craft.vehicle.mode != "LAND" and (craft.vehicle.mode != "LOITER" and (craft.precloiter_opt and craft.vehicle.channels[str(craft.precloiter_opt)] <= 1800))))) and track_targets.state == "started":
track_targets.stop()
elif not args.controlprocessing and track_targets.state == "initialised":
track_targets.start()
# See if there are any tracking results in the queue
#log.debug("Main loop: Before readline: {:.0f}".format(monotonic_time()))
line = track_targets.reader.readline(1)
#log.debug("Main loop: After readline: {:.0f}".format(monotonic_time()))
if line:
if args.verbose:
log.debug("queue size:{}, ourtime:{:.0f}, line:{}".format(track_targets.reader.size(), monotonic_time(), line))
track_targets.processline(line)
#log.debug("Main loop: After processline: {:.0f}".format(monotonic_time()))
else:
# Add a short 100ms sleep to slow down the loop, otherwise it consumes 100% cpu.
log.debug("Main loop queue read empty, sleeping for a few ms")
sleep(0.1)
if args.simulator and not craft.vehicle.armed and not track_targets.shutdown:
log.info("Landed")
track_targets.end()
track_targets.process.poll() # Poll the track_targets process and populate returncode, exit main loop when track_targets shut down
if track_targets.process.returncode != None and track_targets.shutdown:
log.info("Tracking process return code:"+str(track_targets.process.returncode)+", exiting main loop")
break
elif track_targets.process.returncode != None and not track_targets.shutdown and track_targets.state == "started":
log.error("Error: Tracking process shut down unexpectedly ("+str(track_targets.process.returncode)+"), restarting")
track_targets = TrackTargets(args, calibration)
loop_counter += 1
cleanup()
log.info("Vision_landing shutdown complete, exiting")
exit(track_targets.process.returncode)