-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathflogger.py
executable file
·1299 lines (1191 loc) · 65.6 KB
/
flogger.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# FLOGGER
#
# This program reads records from the OGN network processing only
# those received from a specified site and registration marks, eg aircraft belonging to
# a specific club.
# It writes each record to a database and at the end of each day process
# them to determine the flight times of each flight for each machine.
# Phase 1 will just collect the data.
# Phase 2 will process the data into a new table
# Phase 3 will then format that information with the intention of
# it being used to be checked against the manual log books.
# Phase 4 will remove old flight and track file older than a certain date
# The intention is that it will collect the data between the hours of daylight,
# producing the summary at the end of the day.
# This program could be run on a Raspberry Pi as it is so low powered
#
# Altitude in metres.
# Land speed in km/h.
# Latitude, west is negative decimal degrees.
# Longitude, south is negative decimal degrees.
#
# This program is covered by the GNU GENERAL PUBLIC LICENSE.
# See the file 'LICENSE' for details
#
#
# 20150312: First working version
# Usage: Run flogger.py to collect the daily flight data then
# run process.py which processes the raw data into a table flights in the database flogger.sgl3
# This first version is very experimental, it is proof of concept and processes. The code needs to
# be 'improved'.
# To be done: 1) The program should be run each day between 0900 and sunset. This should be handled by cron
# to start the program at a time specified in settings which then calculates sunrise and suspends
# until then. Once running the program determines sunset and stopping itself at that time. It also needs
# to handle power outages (not sure how at the moment)
# 2) The Flarm code to registration code needs to addressed using OGNs new database.
# 20150505 Second working version
# Only need to run flogger.py, it now handles collection of data during daylight hours and processes
# after sunset (assumes gliders only fly during daylight hours)
# Now reads aircraft registration data from Flarmnet to build own internal table
# 20150515 Third working version
# 1) APRS user and APRS passcode have to be supplied on the command line and not in settings
# 2) Changes to flogger_process_log to correct errors - still in testing
#
# 20150520 Fourth working version (V0.1.0)
# 1) On aircraft stop set altitude to initial value else highest value for any flight of the day
# will be the one compared against as the maximum and not the max for a specific flight.
# Bug 20150520-1 Assigned
# 2) Flights table only contains flights for one day and not all previous days flights
# Bug 20150520-2 Assigned
#
# 20150527 Fifth working version (V0.1.1)
# Test version for:
# 1) Bug 20150520-1
# 2) Bug 20150520-2
#
# 20150529 First beta test version (V0.2.0)
# 1) Bug 20150520-1 Solved
# 2) Bug 20150520-2 Solved
# 3) Enhancement - dump days flights table as .csv file
#
# 20150530 Correction to first beta test version (V0.2.1)
# 1) Correction to dump flights to .csv - to make it work!
#
# 20150604 Added enhancements to version V0.2 (V0.2.2)
# 1) Allowance for short duration flight
# 2) Use of geocoding to determine airfield position data - proposed by D.Spreitz
#
# To be done: 1) Tidy up code, remove all redundant testing comments
# 2) A lot more testing - some features might still not work!
# 3) Consider how this may be run as a service with standard start, stop etc options
# 4) Consider adding full logging with levels
# 5) Review the algorithm to determine if aircraft is on the ground. At the moment it determines
# this by the GPS ground speed being zero (ie below a defined value); the ground speed could be zero
# if the wind speed and airspeed are the same but opposite, eg when ridge flying. The algorithm could use
# the altitude as well, eg if ground speed is zero but altitude is greater than home airfield altitude then
# 'we're flying'. Note this still has issues!
# 6) Need to consider sending 'keep alives' when in the sleep state. Solved, not needed
# 7) There's a problem concerning character codes when building the flarm database which needs solving, only show in 1 record
#
# 20160208 1) Add modification to sequence tracks per flight by flarm record timestamp. Using multiple beacons can result in
# track points that are out of sequence when based on order received due to Internet time delays, hence
# use the GPS timestamp recorded in the data taken and sent by flarm (assumes timestamp is from Flarm!).
# 2) Also added graceful exit on Cntrl-C
#
# 20160323 1) Added optional output of track data in IGC format
# 2) Added optional deletion of old flight .csv and track .csv/.igc files
#
# 20160514 1) Use $ pipreqs --force /path/to/project to generate requirements.txt for pip install
#
# 20160518 1) Added attempt to load earlier version Linux libfap if current fails
#
# 20161026 1) Added flogger_find_tug code. This tries to determine which tug, if any, launched a particular glider.
# Note this doesn't always get the right result, but then nor does OGN Flight Log! This could be due to tugs
# sometimes powering down if a launch is not imminent. Gliders are likely to be always powered on and Flarm operating.
# Hence when it becomes time to launch the tug powers up, Flarm is now on but takes some time for the signal to be
# acquired and put onto and processed by the APRS system. It is therefore possible for the launch to take place
# with the take-off times for tug and glider to be too far displaced (from the APRS data) for flogger-find-tug
# to determine the launch has happened. The solution is possibly to increase the time delta used between glider and
# tug take-off but this could result in false positives, some fine tuning maybe needed. Interested to know if
# OGN Flight Log has similar reasoning.
#
# 20161108 1) Rewrote phase 2 flight log processing to be much simpler. Phase 2 puts flights into the flight_group
# table such that all flights by a single aircraft have the same group id. This enables each flight to
# be determined to be a distinct flight from its predecessor or not.
#
# 20170201: 1) Added simple function test_YorN to test for Y|y or N|n
# 2) Started developing using Eclipse Neon.2 (4.6.2)
#
import socket
from libfap import *
import settings
import string
import datetime
import time
import sqlite3
import pytz
from datetime import timedelta
import sys
from flarm_db import flarmdb
from pysqlite2 import dbapi2 as sqlite
from open_db import opendb
import ephem
from flogger_process_log import process_log
import argparse
from flogger_dump_flights import dump_flights
from flogger_dump_tracks import dump_tracks2
from flogger_get_coords import get_coords
from flogger_signals import sig_handler
import signal
import os
import os.path
from flogger_dump_IGC import dump_IGC
from flogger_email_log import email_log2
from flogger_landout import landout_check
from geopy.distance import vincenty
from flogger_email_msg import email_msg
from flogger_find_tug import find_tug
from flogger_test_YorN import test_YorN
prev_vals = {'latitude': 0, 'longitude': 0, "altitude": 0, "speed": 0}
nprev_vals = {"G-CKLW": {'latitude': 0, 'longitude': 0, "altitude": 0, "speed": 0, 'maxA': 0},
"G-CKFN": {'latitude': 0, 'longitude': 0, "altitude": 0, "speed": 0, 'maxA': 0}
}
values = {'latitude': 0, 'longitude': 0, "altitude": 0, "speed": 0}
nvalues = {"G-CKLW": {'latitude': 0, 'longitude': 0, "altitude": 0, "speed": 0, 'maxA': 0},
"G-CKFN": {'latitude': 0, 'longitude': 0, "altitude": 0, "speed": 0, 'maxA': 0}
}
L_SMALL = float(0.001) # Small latitude or longitude delta of a 0.001 degree
A_SMALL = float(0.01) # Small altitude delta of 0.01 a metre, ie 1cm
V_SMALL = float(settings.FLOGGER_V_SMALL) # Small velocity delta of 10.0 kph counts as zero ie not moving
V_TAKEOFF_MIN = float(settings.FLOGGER_V_TAKEOFF_MIN)
V_LANDING_MIN = float(settings.FLOGGER_V_LANDING_MIN)
frst_time = False
AIRFIELD = "SuttonBnk"
flight_no = {} # A dictionary {callsign: flight_no}
track_no = {} # A dictionary {callsign: track_no}
# Coded 001-099: Gliders,
# 101-199: Tugs,
# 201-299: Motor Gliders,
# 301-399: Other
aircraft = {"G-CKLW": 1, "G-CKLN": 2, "G-CJVZ": 3, "G-CHEF": 4, "G-CKFN": 5,
"G-CHVR": 6, "G-CKJH": 7, "G-CKRN": 8, "G-CGBK": 9, "G-CDKC": 10,
"G-BFRY": 101, "G-BJIV": 102, "G-MOYR": 103,
"G-OSUT": 201,
"FLRDDF9C4": 301, "FLRDDE5FC": 302, "FLRDDBF13": 303, "FLRDDA884": 304, "FLRDDA886": 305, "FLRDDACAE": 306, "FLRDDA7E9": 307,
"FLRDDABF7": 308, "FLRDDE671": 309}
def CheckPrev(callsignKey, dataKey, value):
print "CheckPrev if callsign in nprev_vals: ", callsignKey, " key: ", dataKey, " Value: ", value
if nprev_vals.has_key(callsignKey) == 1:
print "nprev_vals already has entry: ", callsignKey
else:
print "nprev_vals doesn't exist for callsignKey: ", callsignKey
nprev_vals[callsignKey] = {}
nprev_vals[callsignKey] = {'latitude': 0, 'longitude': 0, "altitude": 0, "speed": 0, 'maxA': 0}
nprev_vals[callsignKey][dataKey] = value
print "nprev_vals for callsignKey: ", callsignKey, " is: ", nprev_vals[callsignKey]
# print "nprev_vals is now: ", nprev_vals
return
def CheckVals(callsignKey, dataKey, value):
print "CheckVals if callsign in nvalues: ", callsignKey, " key: ", dataKey, " Value: ", value
if nvalues.has_key(callsignKey) == 1:
print "nvalues already has entry: ", callsignKey
else:
print "nvalues doesn't exist for callsignKey: ", callsignKey
nvalues[callsignKey] = {}
nvalues[callsignKey] = {'latitude': 0, 'longitude': 0, "altitude": 0, "speed": 0, 'maxA': 0}
nvalues[callsignKey][dataKey] = value
print "nvalues for callsignKey: ", callsignKey, " is: ", nvalues[callsignKey]
# print "nvalues is now: ", nvalues
return
def isDayLight ():
return True
def fleet_check(call_sign):
if aircraft.has_key(call_sign):
return True
else:
return False
def comp_vals(set1, set2):
# Works out if the difference in positions is small and both speeds are close to zero
# Return True is yes and False if no
# Set1 are new values, set2 old values
print "Set1 value for key latitude is: ", set1["latitude"], " value: ", float(set1["latitude"])
# lat1 = float(set1["latitude"])
# lat2 = float(set2["latitude"])
delta_latitude = float(set1["latitude"]) - float(set2["latitude"])
delta_longitude = float(set1["longitude"]) - float(set2["longitude"])
delta_altitude = float(set1["altitude"]) - float(set2["altitude"])
delta_speed = float(set1["speed"]) - float(set2["speed"])
print "Delta positions. Lat: ", delta_latitude, " Long: ", delta_longitude, " Alt: ", delta_altitude, " Speed: ", delta_speed
# if (delta_latitude < L_SMALL) and (delta_longitude < L_SMALL) and (delta_altitude < A_SMALL) and (delta_speed < V_SMALL):
if delta_speed <> 0.0:
print "Delta speed not zero, check others"
# if (delta_latitude == 0.0) and (delta_longitude == 0.0) and (delta_altitude == 0.0) and (delta_speed == 0.0):
if (delta_latitude == 0.0) and (delta_longitude == 0.0) and (delta_altitude == 0.0):
print "Positions same"
return True
else:
print "Positions different"
return False
else:
print "Delta speed zero, return same"
return True
def set_keepalive(sock, after_idle_sec=1, interval_sec=3, max_fails=5):
"""Set TCP keepalive on an open socket.
It activates after 1 second (after_idle_sec) of idleness,
then sends a keepalive ping once every 3 seconds (interval_sec),
and closes the connection after 5 failed ping (max_fails), or 15 seconds
"""
print "set_keepalive for idle after: ", after_idle_sec
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, after_idle_sec)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, interval_sec)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, max_fails)
return
def is_dst(zonename):
# Determine if in daylight
tz = pytz.timezone(zonename)
now = pytz.utc.localize(datetime.utcnow())
return now.astimezone(tz).dst() != timedelta(0)
def fleet_check_new(callsign):
#
# This has become a little confusing! If FLOGGER_FLEET_CHECK == n|N then FLOGGER_AIRFIELD_NAME is not used in
# the flarm_db search so a non-fleet aircraft can be found, but the later code checks whether the aircraft
# has taken off at FLOGGER_AIRFIELD_NAME; if it hasn't it won't be included in the flights, if it has it will.
#
# This logic and code needs to be re-thought!
#
print "In fleet check for: ", callsign
# cursor.execute('''SELECT ROWID FROM aircraft WHERE registration =? or flarm_id=? ''', (callsign,callsign,))
# row = cursor.fetchone()
# flarm_id = callsign[3:]
# print "search for flarm_id: ", flarm_id
# cursor.execute('''SELECT ROWID FROM flarm_db WHERE flarm_id =?''', (flarm_id,))
# if settings.FLOGGER_FLEET_CHECK == "N" or settings.FLOGGER_FLEET_CHECK == "n":
if not test_YorN(settings.FLOGGER_FLEET_CHECK):
fleet_name = "Fleet Name: Not used"
cursor.execute('''SELECT ROWID, registration FROM flarm_db WHERE registration =? OR flarm_id =? ''', (callsign,callsign[3:],))
else:
fleet_name = settings.FLOGGER_AIRFIELD_NAME
cursor.execute('''SELECT ROWID FROM flarm_db WHERE registration =? OR flarm_id =? AND airport=?''', (callsign,callsign[3:],settings.FLOGGER_AIRFIELD_NAME,))
#cursor.execute('''SELECT ROWID FROM flarm_db WHERE registration =? OR flarm_id =? AND airport=?''', (callsign,callsign[3:],settings.FLOGGER_AIRFIELD_NAME,))
row1 = cursor.fetchone()
if row1 == None:
print "Registration not found in flarm_db: ", callsign, " for: ", fleet_name
return False
else:
print "Aircraft: ", callsign, " found in flarm db at: ", row1[0], " for: ", fleet_name
reg = callsign_trans(callsign)
# if settings.FLOGGER_FLEET_CHECK <> "N":
if not test_YorN(settings.FLOGGER_FLEET_CHECK):
# if settings.FLOGGER_FLEET_LIST[reg] > 100 and settings.FLOGGER_FLEET_LIST[reg] < 200 and settings.FLOGGER_LOG_TUGS == "N":
if settings.FLOGGER_FLEET_LIST[reg] > 100 and settings.FLOGGER_FLEET_LIST[reg] < 200 and (not test_YorN(settings.FLOGGER_LOG_TUGS)):
print "Don't log tug: %s" % reg
return False
# At least 1 match for the callsign has been found
return True
def callsign_trans(callsign):
# Translates a callsign supplied as a flarm_id
# into the aircraft registration using a local db based on flarmnet or OGN
# Note if OGN db is being used then callsigns don't start with FLR or ICA, this is denoted by the 'Type' field
# cursor.execute('''SELECT registration, flarm_id FROM aircraft WHERE registration =? or flarm_id=? ''', (callsign,callsign,))
if callsign.startswith("FLR") or callsign.startswith("ICA") :
# Callsign starts with "FLR" or ICA so remove it
str = callsign[3:]
ncallsign = "%s" % str
print "Removing FLR or ICA string. Callsign is now: ", ncallsign
else:
ncallsign = "%s" % callsign
cursor.execute('''SELECT registration FROM flarm_db WHERE flarm_id=? ''', (ncallsign,))
row = cursor.fetchone()
if row <> None:
# Registration found for flarm_id so return registration
registration = "%s" % row
print "In flarm db return: ", registration
return registration
else:
# Registration not found for flarm_id so return flarm_id
print "Not in flarm db return: ", callsign
return ncallsign
def APRS_connect (settings):
#
#-----------------------------------------------------------------
# Connect to the APRS server to receive flarm data
#-----------------------------------------------------------------
#
# create socket & connect to server
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
set_keepalive(sock, after_idle_sec=60, interval_sec=3, max_fails=5)
sock.connect((settings.APRS_SERVER_HOST, settings.APRS_SERVER_PORT))
except Exception, e:
print "Socket failure on connect: ", e
print "Socket sock connected"
try:
# sock.send('user %s pass %s vers OGN_Flogger 0.0.2 filter r/+54.228833/-1.209639/25\n ' % (settings.APRS_USER, settings.APRS_PASSCODE))
APRSparm = ('user %s pass %s vers %s %s filter r/%s/%s/%s\n ' % (settings.APRS_USER,
settings.APRS_PASSCODE,
settings.FLOGGER_NAME,
settings.FLOGGER_VER,
settings.FLOGGER_LATITUDE,
settings.FLOGGER_LONGITUDE,
settings.FLOGGER_RAD))
# print "APRSparm is: ", APRSparm
# s = "user %s pass %s vers OGN_Flogger 0.2.2 filter r/%s/%s/25\n " % (settings.APRS_USER, settings.APRS_PASSCODE, settings.FLOGGER_LATITUDE, settings.FLOGGER_LONGITUDE)
# print "Socket connect string is: ", s
sock.send(APRSparm)
except Exception, e:
print "Socket send failure: ", e
exit()
print "Socket send ok"
# Make the connection to the server
# start_time = datetime.datetime.now()
# keepalive_time = time.time()
# sock_file = sock.makefile()
print "APRS connection made"
return sock
def addTrack(cursor,flight_no,track_no,longitude,latitude,altitude,course,speed,timeStamp):
#
#-----------------------------------------------------------------
# Add gps track data to track record if settings.FLOGGER_TRACK is "Y" ie yes
# and if flight_no != None which it will be if flight has not taken off at FLOGGER_AIRFIELD_NAME
#-----------------------------------------------------------------
#
# dt = str(datetime.datetime.now()) # Get the datetime this track point is created as string
# sdt = dt[0:10] + "T" + dt[11:19] + "Z" # Convert to string format for gpx, ie YYYY-MM-DDTHH:MM:SSZ
# sdt = "%sT%sZ" % (dt[0:10],dt[11:19]) # Convert to string format for gpx, ie YYYY-MM-DDTHH:MM:SSZ
if settings.FLOGGER_TRACKS == "Y" and flight_no != None:
print "Flight_no is: ", flight_no
print "Track point nos is: ", track_no
# dt = str(datetime.datetime.now()) # Get the datetime this track point is created as string
# sdt = dt[0:10] + "T" + dt[11:19] + "Z" # Convert to string format for gpx, ie YYYY-MM-DDTHH:MM:SSZ
# This print doesn't work as one of the values is of none-type, not sure why?
# print "Adding track data to: %i, %i, %f, %f, %f, %f %f " % (flight_no,track_no,latitude,longitude,altitude,course,speed)
try:
cursor.execute('''INSERT INTO track(flight_no,track_no,latitude,longitude,altitude,course,speed,timeStamp)
VALUES(:flight_no,:track_no,:latitude,:longitude,:altitude,:course,:speed,:timeStamp)''',
{'flight_no':flight_no,'track_no':track_no,'latitude':latitude,'longitude':longitude,'altitude':altitude,'course':course,'speed':speed,'timeStamp':timeStamp})
except:
print "Add trackpoint failed on insert: ignore trackpoint"
else:
print "Don't add track point"
return
def endTrack():
return
def CheckTrackData(cursor, flight_no, track_no, callsignKey):
# print "check flight_no if callsign in flight_no{}: ", flight_no, " Track_no is: ", track_no, " CallsignKey is: ", callsignKey
if flight_no.has_key(callsignKey) == 1:
print "flight_no already has entry: ", callsignKey
else:
try:
cursor.execute('''SELECT max(id) FROM flight_log2 WHERE src_callsign =?''', (callsignKey,))
except:
print "!!!ERROR - No record in flight_log2 for: ", callsignKey
# If this crashes need to think about adding record for flight_log2, but why?
exit()
row_id = cursor.fetchone()[0] # value of id for row just inserted use as flight_no for flight
print "Last row ID of flight_log2 for callsign: ", callsignKey, " inserted was: ", row_id
flight_no[src_callsign] = row_id
track_no[callsignKey] = 1
print "flight_no for callsignKey: ", callsignKey, " is: ", flight_no[callsignKey]
return
def check_position_packet (packet_str):
#
#-----------------------------------------------------------------
# This function determines if airfield is in the list of APRS
# base stations used for receiving position fixes.
#
# base_list should be set up as part of the main code initialisation
#-----------------------------------------------------------------
#
for base in APRS_base_list:
if string.find(str(packet_str), base) <> -1:
print "Found in list of APRS base stations: ", base
return base
print "Not found base station in packet"
return -1
def delete_table (table):
#
#-----------------------------------------------------------------
# This function deletes the SQLite3 table
# with the name supplied by "table".
#-----------------------------------------------------------------
#
if settings.FLOGGER_MODE == "test":
print "Test only. Table %s not deleted" % (table)
return
parm = "DELETE FROM %s" % (table)
try:
cursor.execute(parm)
print "New Delete %s table ok" % (table)
except:
print "New Delete %s table failed or no records in tables" % (table)
return
def delete_flogger_file(folder, filename, days):
#
#-----------------------------------------------------------------
# This function deletes the files whose name contain filename in folder folder
# if they were created up to and including the number of days in the past
# specified by the days parameter.
# If days is zero then no deletions are performed
#-----------------------------------------------------------------
#
if days <= 0:
print "Don't delete old files, return"
return
now = time.time()
flist = os.listdir(folder)
for f in flist:
# print "Pathname is: ", os.path.join(folder, f), " st_mtime is: ", os.stat(os.path.join(folder, f)).st_mtime
full_file = os.path.join(folder, f)
file_find = string.find(full_file, filename) <> -1
file_time = os.stat(full_file).st_mtime
# print "File_find is: ", file_find, ". File_time is: ", file_time, "Now is: ", now - days * 86400
if (file_find == True) and (file_time <= (now - days * 86400)):
print "Delete file: ", full_file
os.remove(full_file)
# else:
# print "File not deleted: %s" % full_file
return
def connect_APRS(sock):
#
#-----------------------------------------------------------------
#
# This function tries to shutdown the specified sock and if it
# fails closes it and then creates a new one and reconnects to the APRS system
#
#-----------------------------------------------------------------
#
try:
sock.shutdown(0)
except socket.error, e:
if 'not connected' in e:
print '*** Transport endpoint is not connected ***'
print "socket no longer open so can't be closed, create new one"
else:
print "Socket still open so close it"
sock.close()
print "Create new socket"
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect((settings.APRS_SERVER_HOST, settings.APRS_SERVER_PORT))
except Exception, e:
print "Connection refused. Errno: ", e
exit()
APRSparm = ('user %s pass %s vers %s %s filter r/%s/%s/%s\n ' % (settings.APRS_USER,
settings.APRS_PASSCODE,
settings.FLOGGER_NAME,
settings.FLOGGER_VER,
settings.FLOGGER_LATITUDE,
settings.FLOGGER_LONGITUDE,
settings.FLOGGER_RAD))
# print "APRSparm is: ", APRSparm
# sock.send('user %s pass %s vers Python_Example 0.0.1 filter r/+54.228833/-1.209639/25\n ' % (settings.APRS_USER, settings.APRS_PASSCODE))
sock.send(APRSparm)
# Make the connection to the server
sock_file = sock.makefile()
return sock_file
#
#-----------------------------------------------------------------
# Start of main code
#-----------------------------------------------------------------
#
settings = MyApp()
print "FLOGGER_AIRFIELD_NAME from class is: " + settings.FLOGGER_AIRFIELD_NAME
#
# User and passcode now mandatory positional parameters
# Mode is an optional positional parameter, default is "live"
#
parser = argparse.ArgumentParser()
parser.add_argument("user", help="user and passcode must be supplied, see http://www.george-smart.co.uk/wiki/APRS_Callpass for how to obtain")
parser.add_argument("passcode", help="user and passcode must be supplied", type=int)
parser.add_argument("mode", help="mode is test or live, test modifies behaviour to add output for testing", default="live")
parser.add_argument('-s', '--smtp', help="URL of smtp server")
parser.add_argument('-t', '--tx', help="email address of sender")
parser.add_argument('-r', '--rx', help="email address of receiver")
args = parser.parse_args()
print "user=", args.user, " passcode=", args.passcode, "mode=", args.mode, "smtp=", args.smtp, "tx=", args.tx, "rx=", args.rx
#
# Check parameters. If an smtp server address is specified then the sender and receiver email
# addresses must also be supplied either in the call line or the config file
#
if (args.smtp == None and settings.FLOGGER_SMTP_SERVER_URL == ""):
print "SMTP url not specified, don't send email"
else:
print "Set to send email"
if (args.smtp <> None):
settings.FLOGGER_SMTP_SERVER_URL = args.smtp
if (args.tx <> None):
settings.FLOGGER_SMTP_TX = args.tx
if (args.rx <> None):
print "args.rx is: ", args.rx
settings.FLOGGER_SMTP_RX = args.rx
elif ((args.tx == None or args.rx == None) and (settings.FLOGGER_SMTP_TX == "" or settings.FLOGGER_SMTP_RX == "")):
print "Email option parameters or config not valid. smtp=%s, SERVER_URL=%s, tx=%s, rx=%s, SMTP_TX=%s, SMTP_RX=%s" % \
(args.smtp, settings.FLOGGER_SMTP_SERVER_URL, args.tx, args.rx, settings.FLOGGER_SMTP_TX, settings.FLOGGER_SMTP_RX)
exit()
print "Email parameters are now: smtp=%s, SERVER_URL=%s, tx=%s, rx=%s, SMTP_TX=%s, SMTP_RX=%s" % \
(args.smtp, settings.FLOGGER_SMTP_SERVER_URL, args.tx, args.rx, settings.FLOGGER_SMTP_TX, settings.FLOGGER_SMTP_RX)
settings.APRS_USER = args.user
settings.APRS_PASSCODE = args.passcode
settings.FLOGGER_MODE = args.mode
# Creates or opens a file called flogger.sql3 as an SQLite3 DB
#
#-----------------------------------------------------------------
# Build flogger db using schema
# Delete SQLite3 database file if it already exists; stops it getting
# too large during testing
#-----------------------------------------------------------------
#
if os.path.isfile(settings.FLOGGER_DB_NAME):
print "SQLite3 db file exists so delete it"
os.remove(settings.FLOGGER_DB_NAME)
db = sqlite3.connect(settings.FLOGGER_DB_NAME)
cursor = db.cursor() # Get a cursor object
f = open(settings.FLOGGER_DB_SCHEMA, 'rt') # Open the db schema file for reading
schema = f.read()
cursor.executescript(schema) # Build flogger db from schema
print "End of building db: ", settings.FLOGGER_DB_NAME, " using schema: ", settings.FLOGGER_DB_SCHEMA
#
#-----------------------------------------------------------------
# Build local database from flarmnet of aircraft
#-----------------------------------------------------------------
#
if flarmdb(settings.FLOGGER_FLARMNET_DB_URL, cursor, db, "flarm_data") == True:
print "Flarmnet db built"
else:
print "Flarmnet db build failed, exit"
exit()
#
#-----------------------------------------------------------------
# Determine location details, latitude, longitude and elevation
#-----------------------------------------------------------------
#
if settings.FLOGGER_AIRFIELD_DETAILS <> "":
loc = get_coords(settings.FLOGGER_AIRFIELD_DETAILS)
if loc == False:
if settings.FLOGGER_LATITUDE <> "" and settings.FLOGGER_LONGITUDE <> "" and settings.FLOGGER_QNH >=0 :
print "Geolocator failed use values from settings"
else:
print "Geoloactor failed and no values in setting for lat, long, QNH"
exit(2)
else:
settings.FLOGGER_LATITUDE = str(loc[0]) # Held as string
settings.FLOGGER_LONGITUDE = str(loc[1]) # Held as string
settings.FLOGGER_QNH = loc[2] # Held as number
print "Location is: ", settings.FLOGGER_AIRFIELD_DETAILS, " latitude: ", loc[0], " longitude: ", loc[1], " elevation: ", loc[2]
else:
print "Use location data from settings"
#
#-----------------------------------------------------------------
# Set up list of APRS base stations to be used
# (Note this code could be nicer but will do for now)
#-----------------------------------------------------------------
#
#APRS_base_list = [settings.FLOGGER_APRS_BASE_1,
# settings.FLOGGER_APRS_BASE_2,
# settings.FLOGGER_APRS_BASE_3,
# settings.FLOGGER_APRS_BASE_4,]
APRS_base_list = settings.FLOGGER_APRS_BASES
#
#-----------------------------------------------------------------
# Initialise API for computing sunrise and sunset
#-----------------------------------------------------------------
#
location = ephem.Observer()
location.pressure = 0
#location.horizon = '-0:34' # Adjustments for angle to horizon
location.horizon = settings.FLOGGER_LOCATION_HORIZON # Adjustments for angle to horizon
location.lat = settings.FLOGGER_LATITUDE
location.lon = settings.FLOGGER_LONGITUDE
print "Location for ephem is: ", settings.FLOGGER_AIRFIELD_DETAILS, " latitude: ", location.lat, " longitude: ", location.lon, " elevation: ", settings.FLOGGER_QNH
date = datetime.datetime.now()
next_sunrise = location.next_rising(ephem.Sun(), date)
next_sunset = location.next_setting(ephem.Sun(), date)
print "Sunrise today: ", date, " is: ", next_sunrise
print "Sunset today: ", date, " is: ", next_sunset
#
#-----------------------------------------------------------------
# Make the connection to the APRS server
#-----------------------------------------------------------------
#
start_time = datetime.datetime.now()
keepalive_time = time.time()
#sock_file = sock.makefile()
sock = APRS_connect(settings)
sock_file = sock.makefile()
print "libfap_init"
rtn = libfap.fap_init()
if rtn <> 0:
print "Failed to connect to APRS, check parameters"
exit()
print "Libfap return: ", rtn
#
#-----------------------------------------------------------------
# Set up paths for data, logs and tracks
#-----------------------------------------------------------------
#
SB_DATA = "SB_data" + str(start_time)
SB_Log = "SB_Log" + str(start_time)
SB_DATA = str(SB_DATA).replace(" ","_")
SB_Log = str(SB_Log).replace(" ","_")
SB_DATA = str(SB_DATA).replace(":","-")
SB_Log = str(SB_Log).replace(":","-")
print "Checking log paths: ", settings.FLOGGER_LOG_PATH
if settings.FLOGGER_LOG_PATH <> "":
if not os.path.isdir(settings.FLOGGER_LOG_PATH):
print "Log path is not directory",
SB_DATA = os.path.abspath(settings.FLOGGER_LOG_PATH) + "/" + SB_DATA
SB_Log = os.path.abspath(settings.FLOGGER_LOG_PATH) + "/" + SB_Log
try:
#print "Creating log folder"
os.makedirs(settings.FLOGGER_LOG_PATH)
print "Created: ", settings.FLOGGER_LOG_PATH
except:
print "FLOGGER_LOG_PATH does not exist. Please check settings."
exit()
print "SB data file is: ", SB_DATA
print "SB log file is: ", SB_Log
#sys.stdout = open(SB_Log, 'w')
#print "Datafile open"
test = False
if test == True:
datafile = open (SB_DATA, 'rw')
print "In test mode"
else:
datafile = open (SB_DATA, 'w')
print "In live mode"
#
#-----------------------------------------------------------------
# Setup cntrl-c handler
#
#-----------------------------------------------------------------
#
print "Setup cntrl-c handler"
sig_handler(db, cursor)
#time.sleep(5) # Press Ctrl+c here # Just for testing
#
#-----------------------------------------------------------------
# Main loop reading data from APRS server and processing records
# This continues until sunset after which the data recorded is processed
#-----------------------------------------------------------------
#
i = 0
try:
while 1:
# for i in range(1000000):
i = i + 1
datetime_now = datetime.datetime.now()
previous_sunrise = location.previous_rising(ephem.Sun(), date).datetime()
next_sunrise = location.next_rising(ephem.Sun(), date).datetime()
previous_sunset = location.previous_setting(ephem.Sun(), date).datetime()
next_sunset = location.next_setting(ephem.Sun(), date).datetime()
# Set datetime to current time + FLOGGER_LOG_TIME_DELTA to start processing flight log
# that number of hours before sunset
log_datetime = datetime.datetime.now() + datetime.timedelta(hours=settings.FLOGGER_LOG_TIME_DELTA)
# print "Log datetime is: ", log_datetime
location.date = ephem.Date(log_datetime)
print "Ephem date is: ", location.date
s = ephem.Sun()
s.compute(location)
twilight = -6 * ephem.degree # Defn of Twilight is: Centre of Sun is 6, 12, 18 degrees below horizon (civil, nautical, astronomical)
if s.alt > twilight:
# t = False
# if t:
print "Is it light at Location? Yes", location, " Ephem date is: ", ephem.Date(location.date), " Next sunset at: ", location.next_setting(ephem.Sun())
else:
print "Is it light at Location? No", location, " Ephem date is: ", ephem.Date(location.date), " Next sunrise at: ", location.next_rising(ephem.Sun())
process_log(cursor,db)
#
# Dump tracks from flights table as .gpx
# This updates each flight in flights table with trackfile name
#
print "Dump tracks"
dump_tracks2(cursor, db)
dump_IGC(cursor, db)
#
# Experimental. Find tug used for each launch
#
find_tug(cursor, db)
print "Find tug phase end"
#
# Dump flights table as cvs file
# If no flights then returns ""
#
print "Dump flights table"
csv_file = dump_flights()
#
# Email flights csv file if required
# email_log2 sorts out if there are no flights on any one day
# FLOGGER_SMTP_SERVER_TX is either set in config by user or value taken from cmd line --smtp parm.
#
if settings.FLOGGER_SMTP_SERVER_URL <> "":
print "Email today's flight log"
email_log2(settings.FLOGGER_SMTP_TX, settings.FLOGGER_SMTP_RX, csv_file, datetime.date.today())
else:
print "Don't email flight log, no flights"
#
# Delete entries from daily flight logging tables etc
#
delete_table("flight_log")
delete_table("flight_log2")
delete_table("flight_log_final")
delete_table("flight_group")
delete_table("flights")
delete_table("track")
delete_table("trackFinal")
delete_table("flarm_db") # flarm_db should be rebuilt at start of each day
db.commit()
# Wait for sunrise
# wait_time = next_sunrise - datetime_now
datetime_now = datetime.datetime.now()
date = datetime.datetime.now()
location.date = ephem.Date(datetime.datetime.now())
next_sunrise = location.next_rising(ephem.Sun(), date).datetime()
print "Location Date now: ", location.date, " Next sunrise is: ", next_sunrise
wait_time = location.next_rising(ephem.Sun(), date).datetime() - datetime_now
print "Next sunrise at: ", location.next_rising(ephem.Sun(), date).datetime(), " Datetime now is: ", datetime_now
# Wait an additional 2 hours (in seconds) more before resuming.
# Just a bit of security, not an issue as unlikely to start flying so early
wait_time_secs = int(wait_time.total_seconds()) + (2 * 60 * 60)
# close socket -- not needed. Create new one at sunrise
try:
sock.shutdown(0)
except socket.error as msg:
print "Socket failed to shutdown, ignore. Msg is: " , msg
sock.close()
#
# Delete historic files as specified
#
print "+++++++Phase 4 Start Delete out of date files+++++++"
delete_flogger_file(settings.FLOGGER_TRACKS_FOLDER, "track", settings.FLOGGER_DATA_RETENTION)
delete_flogger_file(settings.FLOGGER_BS, "flights.csv", settings.FLOGGER_DATA_RETENTION)
print "-------Phase 4 End-------"
#
# Sleep till sunrise
# Then open new socket, set ephem date to new day
#
print "Wait till after sunrise at: ", next_sunrise, " Elapsed time: ", wait_time, ". Wait seconds: ", wait_time_secs
time.sleep(wait_time_secs)
# Sun has now risen so recommence logging flights
location.date = ephem.Date(datetime.datetime.now())
print "Woken up. Date time is now: ", datetime.datetime.now()
print "Ephem datetime on wakeup is: ", ephem.Date(location.date)
# Make new socket as old one will have timed out during the 'big' sleep, reset the timers
start_time = datetime.datetime.now()
keepalive_time = time.time()
sock = APRS_connect(settings)
sock_file = sock.makefile() # Note both sock & sock_file get used
#
#-----------------------------------------------------------------
# Build local database from flarmnet of aircraft for today
# Note source flarm_db may have changed during previous day
#-----------------------------------------------------------------
#
if flarmdb(settings.FLOGGER_FLARMNET_DB_URL, cursor, db, "flarm_data") == True:
print "Flarmnet db built for today"
else:
print "Flarmnet db re-build failed, exit"
exit()
i = 0 # Count of todays APRS reads reset
flight_no = {} # Re-initialise flight_no dictionary at start of day
track_no = {} # Re-initialise track_no dictionary at start of day
continue
current_time = time.time()
elapsed_time = int(current_time - keepalive_time)
print "Elapsed time is: ", elapsed_time
if (current_time - keepalive_time) > settings.FLOGGER_KEEPALIVE_TIME:
try:
print "Socket open for: ", (current_time - keepalive_time), " seconds, send keepalive"
rtn = sock_file.write("#Python Example App\n\n")
sock_file.flush() # Make sure it gets sent
print "Send keepalive", elapsed_time, " rtn is: ", rtn
keepalive_time = current_time
except Exception, e:
print ('something\'s wrong with socket write. Exception type is %s' % (`e`))
sock_file = connect_APRS(sock)
print "New connection to APRS made"
continue
else:
print "No keepalive sent"
print "In while loop. Count= ", i
try:
if test == False:
# In live mode so use socket read
print "Read socket"
packet_str = sock_file.readline()
# datafile.write(packet_str)
else:
# In test mode so file read
packet_str = datafile.readline()
except socket.error:
print "Socket error on readline"
print "packet string length is: ", len(packet_str), " packet is: ", packet_str
try:
len_packet_str = len(packet_str)
except TypeError:
packet_str_hex = ":".join("{:02x}".format(ord(c)) for c in packet_str)
len_packet_str = len(packet_str_hex) / 3
print "TypeError on packet_str length. Now is: ", len_packet_str
if len_packet_str == 0:
# create new socket & connect to server
print "Read returns zero length string on iteration: ", i
# Wait 20 seconds
time.sleep(20)
# continue
try:
sock.shutdown(0)
except socket.error, e:
if 'not connected' in e:
print '*** Transport endpoint is not connected ***'
print "socket no longer open so can't be closed, create new one"
else:
print "Socket still open so close it"
sock.close()
print "Create new socket"
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect((settings.APRS_SERVER_HOST, settings.APRS_SERVER_PORT))
except Exception, e:
print "Connection refused. Errno: ", e
exit()
APRSparm = ('user %s pass %s vers %s %s filter r/%s/%s/%s\n ' % (settings.APRS_USER,
settings.APRS_PASSCODE,
settings.FLOGGER_NAME,
settings.FLOGGER_VER,
settings.FLOGGER_LATITUDE,
settings.FLOGGER_LONGITUDE,
settings.FLOGGER_RAD))
# print "APRSparm is: ", APRSparm
# sock.send('user %s pass %s vers Python_Example 0.0.1 filter r/+54.228833/-1.209639/25\n ' % (settings.APRS_USER, settings.APRS_PASSCODE))
sock.send(APRSparm)
# Make the connection to the server
sock_file = sock.makefile()
# Delete following line when not running in test mode
# exit()
continue
#
# Parse the returned packet into fields
# Note this uses a modified version of libfap as the master on
# github contains an error
#
packet = libfap.fap_parseaprs(packet_str, len(packet_str), 0)
print 'Parse packet. Callsign: %s. Packet body: %s' % (packet[0].src_callsign, packet[0].body)
try:
error_code = packet[0].error_code[0]
except ValueError:
x = 0
# print "Error_code is NULL pointer ignore"
else:
x = 0
# print "Error_code is: ", packet[0].error_code[0]
# print "error_message is(type=c_char_p): ", packet[0].error_message
try:
type = packet[0].type[0]
except ValueError:
print "Type is NULL pointer ignore"
else:
x = 0
src_callsign = packet[0].src_callsign
dest_callsign = packet[0].dst_callsign
try:
path = packet[0].path[0]
except ValueError:
x = 0
# print "Path is NULL pointer ignore"
else:
x = 0
# print "Path is: ", packet[0].path[0]
try:
latitude = packet[0].latitude[0]
except ValueError:
x = 0
# print "Latitude is NULL pointer"
else:
# print "Latitude is: ", packet[0].latitude[0]
# nvalues[src_callsign]["latitude"] = latitude
CheckVals(src_callsign, "latitude", latitude)
try:
longitude = packet[0].longitude[0]
except ValueError:
x = 0
# print "Longitude is NULL pointer"
else:
# print "Longitude is: ", packet[0].longitude[0]
nvalues[src_callsign]["longitude"] = longitude
try:
altitude = packet[0].altitude[0]
except ValueError:
x = 0
# print "Altitude is NULL pointer"
else: