''' =============================================================================== H O V E R C R A F T I N S T R U M E N T A T I O N L o g g i n g o f G P S - a n d E c h o S o u n d e r d a t a Features: * Auto-detection of serial ports. * Auto-detection of serial speed. * Auto-detection of GPS and Echo Sounder data transmission. * Logging to file with automatic file name. * Courtesy printing of LATITUDE / LONGITUDE / TIME every minute, easily visible. * Echo Sounder data logged continuously, including position- and timestamp. Logging resumed silently if data transmission interrupted for a period. * GPS data logged at user specified interval. * Sound indications: - Normal logging of GPS data accompanied by tiny pleasant beep. - Alarm sound: Loss of GPS fix, data from GPS. * Extensive alarm reporting - but not interrupting logging process: - Loss of GPS fix - Loss of GPS data - Echo Sounder: Loss of data will not generate alarm - it is seen as normal part of data acquisition from this unit. Instead, the normal logging audio indication is changed slightly when Echo Sounder data is present. - If Echo sounder data rate exceeds processing capacity warning is given. Echo sounder data will be lost, but logging continues. This should not hapen - Echo sounder transmits data every two second. Notes: $GPRMC NMEA telegram must be present in data stream from GPS. If necessary, configure GPS to output this telegram (other telegrams ignored). Two external Python modules are needed to run the script: a) "win32all" extensions from: http://starship.python.net/crew/mhammond/win32/Downloads.html b) Serial communication: PySerial module: http://pyserial.sourceforge.net/ ASCII art online generator: http://patorjk.com/software/taag/ Font used: JS Stick Letters (font #158) Bug reports: ole.meyer at geo.uib.no Revisions: -------------------------------------------------------------------- Rev Date By Description -------------------------------------------------------------------- 0.1 18 May 2010 O.M. Initial version. Department of Earth Science University of Bergen Norway =============================================================================== ''' #-------------------------------------------------------------------------- # I M P O R T P Y T H O N M O D U L E S #-------------------------------------------------------------------------- import time, datetime import winsound from sys import exit import os import os.path import thread import Queue #---- Modules above are part of standard Python installation. #---- When we next attempt to import the PySerial module, #---- it might not exist, so provide error message in that case. try: import serial except: print ''' ----------------------------------------------------------------------- Two external Python library modules are needed to run this program: a) "win32all" extensions b) "pySerial" serial COM port handling First install "win32all" extensions from: http://starship.python.net/crew/mhammond/win32/Downloads.html Then fetch "pySerial" from: http://pyserial.sourceforge.net/ ----------------------------------------------------------------------- ''' print "Bye" exit() #-------------------------------------------------------------------------- # C O N S T A N T S #-------------------------------------------------------------------------- VERSION = '''Ver. 0.1 Date: 18 May 2010 By: O.M. ''' LOG_INTERVAL = 10 # seconds MAX_LOG_INTERVAL = 600 # seconds NMEA_TARGETS = ['$GPRMC', '$GPGGA'] # First element is default target ECHO_SOUNDER_SERIAL_SPEED = 4800 ECHO_SOUNDER_SERIAL_TIMEOUT = 5 # in seconds GPS_SERIAL_TIMEOUT = 2 # in seconds BAUD_RATES = [4800, 9600, 19200] LOG_DIRECTORY = 'C:/HOVERCRAFT_TRACK_LOGGER' SOUND_EXIT = 1 SOUND_LOG_MAX = 2 SOUND_LOG_MIN = 3 SOUND_FOUND_DEVICE = 4 SOUND_GPS_NO_FIX = 5 SOUND_GPS_NO_DATA = 6 SOUND_LOG_ERROR = 7 SOUND_GPS_HUNTING_FIX = 8 #-------------------------------------------------------------------------- # G L O B A L V A R I A B L E S #-------------------------------------------------------------------------- quit_logging = False echo_sounder_queue = Queue.LifoQueue() # Construct LIFO queue that will act as a pipe between Echo Sounder thread and main loop #--------------------------------------------------------- # CLASSES #--------------------------------------------------------- class c_serialport: pass #------------------------------------------------------------ # S U B R O U T I N E S #------------------------------------------------------------ #----------------------------------------------- # KEYBOARD MONITOR #----------------------------------------------- def keyboard_thread(): global quit_logging answer = raw_input() quit_logging = True print "Bye from keyboard monitor thread" #----------------------------------------------- # ECHO SOUNDER READING OF SERIAL DATA #----------------------------------------------- def echo_sounder_thread(): global echo_sounder global quit_logging #print "Echo Sounder thread attempts to open COM port ..." ser_echosounder = serial.Serial(port=echo_sounder.port, baudrate=echo_sounder.speed, timeout=echo_sounder.timeout) while not quit_logging: echo_sounder.data = ser_echosounder.readline() if ( (not echo_sounder.data) & echo_sounder.alive ): ###print "Warning: No data from Echo Sounder" echo_sounder.alive = False if '$SDDPT' in echo_sounder.data: #print echo_sounder.data.strip() echo_sounder.alive = True echo_sounder_queue.put(echo_sounder.data.strip()) ser_echosounder.close() print "Bye from Echo Sounder data reading thread" #----------------------------------------------- # SCAN FOR SERIAL PORTS #----------------------------------------------- def scan(): """scan for available ports. return a list of tuples (num, name)""" available = [] for i in range(256): try: s = serial.Serial(i) available.append((i, s.portstr)) s.close() # explicit close 'cause of delayed GC in java except serial.SerialException: pass return available #----------------------------------------------------------------------------------------------------------------------------- # SOUND OUTPUT # "winsound" call will cause system crash when user switches user account (message: winsound - SOUND OUTPUT NOT POSSIBLE) # So we must have one place for sound output, encapsulated in TRY .. EXCEPT section. #----------------------------------------------------------------------------------------------------------------------------- def sound(melody): global SOUND_EXIT, SOUND_LOG_MAX, SOUND_LOG_MIN, SOUND_FOUND_DEVICE, SOUND_GPS_NO_FIX, SOUND_GPS_NO_DATA, SOUND_LOG_ERROR, SOUND_GPS_HUNTING_FIX try: if melody == SOUND_EXIT: winsound.Beep(1800, 100) winsound.Beep(2000, 100) winsound.Beep(2200, 100) winsound.Beep(2500, 100) elif melody == SOUND_LOG_MAX: #------ Logging of BOTH GPS and Echo Sounder winsound.Beep(600, 50) #for t in range(2): # winsound.Beep(2200, 20) # winsound.Beep(2000, 10) elif melody == SOUND_LOG_MIN: #------ Logging of ONLY GPS winsound.Beep(400, 40) elif melody == SOUND_FOUND_DEVICE: winsound.Beep(2000, 70) winsound.Beep(2200, 70) winsound.Beep(3000, 300) elif melody == SOUND_GPS_NO_FIX: winsound.Beep(800, 40) elif melody == SOUND_GPS_NO_DATA: winsound.Beep(800, 80) elif melody == SOUND_LOG_ERROR: winsound.Beep(800, 70) elif melody == SOUND_GPS_HUNTING_FIX: winsound.Beep(1800, 40) except: print "Warning: Sound output not possible" #------------------------------------------------------------------------------- # M A I N P R O G R A M #------------------------------------------------------------------------------- print ''' =============================================================================== H O V E R C R A F T I N S T R U M E N T A T I O N L o g g i n g o f G P S - a n d E c h o S o u n d e r d a t a %s Department of Earth Science University of Bergen, Norway Terminate program by pressing <ENTER> key =============================================================================== ''' % (VERSION) #---- Find serial ports free_com_ports = scan() # Will return list of tuples, each tuple is pair of parameters for valid serial port, e.g. (0, 'COM1') if not free_com_ports: print ''' ------------------------------------------------------------------------------- ___ __ __ __ __ |__ |__) |__) / \ |__) |___ | \ | \ \__/ | \ No COM ports available (could also be used by other applications). Close other applications using COM ports. Check to verify that COM ports exist: Control Panel => System => Hardware => Device Manager => Ports Also start Terminal Program (like TeraTerm) to verify that COM port is active. Simply removing and re-attaching USB GPS can often solve the problem. NOTE: Use of USB extension cable can cause strange error symptoms: COM port may be present in device manager, but not visible in Terminal Program. ------------------------------------------------------------------------------- ''' exit() else: print ''' ---------------------------------------------------- Found COM ports, now scanning for GPS data ports... ---------------------------------------------------- ''' gps_com_ports = [] for n,s in free_com_ports: print " %5s" % (s) for baud in BAUD_RATES: ser = serial.Serial(port=n, baudrate=baud, timeout=2) print " Trying %6d bit/s ..." % (ser.baudrate), Data = ser.read(400) ser.close() if not Data: print "Quiet" elif ( '$GPRMC' in Data ): print "OK - GPS detected - $GPRMC telegram found!" gps_com_ports.append((n, s, baud, Data)) break else: print "Data, but no GPS" #--------------------------------------------------------------------------------------------- # Serial port(s) (GPS) found. # Now we have three choices: # a) No GPS attached to COM port # b) More than one GPS found # c) Exactly one GPS found #--------------------------------------------------------------------------------------------- if not gps_com_ports: print "No GPS found" print "Bye" exit() elif len(gps_com_ports) > 1: print print "More then one GPS seem to be attached:" print for n,s,Baud,Data in gps_com_ports: print "%s - data sample:" % (s) print "-" * 80 #for line in Data: # --- Note: Does not work - Data is not an array of lines. # if ( (line[0] == '$') and (len(line)>40) ): # print line print Data.strip() print "-" * 80 print print "Which GPS do you want to use?" k = 1 for n,s,Baud,Data in gps_com_ports: print "%5d = %s" % (k,s) k += 1 #-- Get response from user, check it first .. while True: response = raw_input("Your choice: ") try: if 1 <= int(response) <= len(gps_com_ports): (n,s,Baud,Data) = gps_com_ports[int(response)-1] break else: print "Input not within valid range [1..%d]" % len(gps_com_ports) except: print "Invalid input" else: (n,s,Baud,Data) = gps_com_ports[0] #------ GPS port is now determined! gps = c_serialport() gps.speed = Baud gps.port = n gps.description = s gps.timeout = GPS_SERIAL_TIMEOUT gps.data = Data print ''' GPS: Using %s at %d bit/s ''' % (gps.description, gps.speed) sound(SOUND_FOUND_DEVICE) time.sleep(1) #-------------------------------------------------------------------------------------------------------- # Must now confirm that $GPRMC telegram is present. # NEXT VERSION: Could also accept e.g. $GPGGA telegram that does not contain DATE information; # in that case we must use PC clock as date source, after asking the user to verify # that PC clock date is correct. #-------------------------------------------------------------------------------------------------------- if len(gps_com_ports) == 1: # Only provide GPS data sample when there is one and only one GPS attached print ''' Sample GPS data: ''' #----- Scan for NMEA telegrams .... nmea_telegrams_found = [] for item in NMEA_TARGETS: if item in gps.data: #print "%s telegram found" % item nmea_telegrams_found.append(item) if '$GPRMC' in nmea_telegrams_found: #print "Found $GPRMC, using this telegram as NMEA target" nmea_target = '$GPRMC' else: print "Did not find $GPRMC telegram. Configure GPS to output this telegram." print "Bye" exit() ###elif '$GPGGA' in nmea_telegrams_found: ### print "Did not find Found $GPGGA, using this telegram as NMEA target" ### nmea_target = '$GPGGA' ### print "This telegram does not contain DATE information. Confirm that current date is:" #---- Print data sample ONLY if there is one GPS (if more, we have already displayed samples) if len(gps_com_ports) == 1: #print "Data sample:" if gps.data[0] <> '$': s = gps.data.split('$') for k in range(len(s)): if (k > 0): print '$' + s[k].strip() else: print gps.data.strip() #-------------------------------------------------------------------------------- #------------------------- E C H O S O U N D E R --------------------------- #-------------------------------------------------------------------------------- print ''' -------------------------------------------- Now scanning for Echo Sounder com port ... -------------------------------------------- ''' echo_sounder = c_serialport() echo_sounder.speed = ECHO_SOUNDER_SERIAL_SPEED echo_sounder.timeout = ECHO_SOUNDER_SERIAL_TIMEOUT echo_sounder.data = '' echo_sounder.port = -1 #--- Default; means that no port present (or detected) echo_sounder.alive = False if len(free_com_ports) == 1: print "Only one COM port, used by the GPS -> not logging Echo Sounder Data" else: possible_echo_sounder_ports = [] for n,s in free_com_ports: if n <> gps.port: possible_echo_sounder_ports.append((n,s)) #print n, s, print " Trying %5s at %d bit/s ..." % (s, echo_sounder.speed), ser_echosounder = serial.Serial(port=n, baudrate=echo_sounder.speed, timeout=echo_sounder.timeout) echo_sounder.data = ser_echosounder.read(300) ser_echosounder.close() if not echo_sounder.data: print "Quiet" elif ( '$SDDPT' in echo_sounder.data): echo_sounder.port = n print "OK - Echo Sounder detected - $SDDPT telegram found!" echo_sounder.alive = True print print "Sample Echo Sounder data:" print print echo_sounder.data.strip() break else: print "Data, but no Echo Sounder" if echo_sounder.port < 0: print print "Echo Sounder data not detected." print "However, it's possible to monitor COM ports and capture data when Echo Sounder starts transmitting." if len(possible_echo_sounder_ports) > 1: print "There seems to be several possible COM ports." print "Which one to use?" k = 1 for n,s in possible_echo_sounder_ports: print " %d = %s" % (k, s) k += 1 response = raw_input("Your choice: ") #----- Error checking of user input is not so extensive as in GPS port selection above. #----- Can be elaborated in later versions. try: if ( 1 <= int(response) <= len(possible_echo_sounder_ports) ): n,s = possible_echo_sounder_ports[int(response) - 1] print "Selected: %s" % s echo_sounder.port = n else: print "Not within valid range [1 .. %d]" % len(possible_echo_sounder_ports) exit() except: #print "Illegal number" exit() else: print "There is only one possible COM port for the Echo Sounder:" n,s = possible_echo_sounder_ports[0] print " %s" % s echo_sounder.port = n if echo_sounder.port >= 0: print print "Echo Sounder: Using COM%d at %d bits/s" % (echo_sounder.port+1, ECHO_SOUNDER_SERIAL_SPEED) sound(SOUND_FOUND_DEVICE) time.sleep(1) #------------------------------------------------------------------------------ # Determine Log interval setting #------------------------------------------------------------------------------ interval = LOG_INTERVAL # First assume default value print ''' ------------------------------------------------------------------------------- __ __ ___ ___ __ | / \ / _` | |\ | | |__ |__) \ / /\ | |___ \__/ \__> | | \| | |___ | \ \/ /~~\ |___ Echo Sounder data (if present) captured continuously. Default GPS track log interval is %d seconds. ''' % (LOG_INTERVAL) while True: response = raw_input( "Change Y/N? [N] ") if not response: break if response.upper() == "Y": while True: interval = raw_input("New log interval: ") if interval.isdigit(): if 1 <= int(interval) <= MAX_LOG_INTERVAL: interval = int(interval) break else: print "Input not within valid range [1..%d] seconds" % MAX_LOG_INTERVAL else: print "Invalid input" break elif response.upper() == "N": break else: print "Invalid input" print "Log interval: %d seconds" % interval #------------------------------------------------------------------------------------------------ #----- Before doing anything else, start thread that monitors keyboard for <ENTER> key press. #----- Thread is implemented as subroutine, placed in top section of this program. #----- If user hits <ENTER> key, a shared flag will be set, and program will exit. #------------------------------------------------------------------------------------------------ thread.start_new(keyboard_thread,()) #-------------------------------------------------------------------------------------------------------- # S T O R A G E O F D A T A # First determine filename # Need to find GPRMC telegram with valid date and time #-------------------------------------------------------------------------------------------------------- ser_gps = serial.Serial(port=gps.port, baudrate=gps.speed, timeout=gps.timeout) Hunting = True while Hunting: # Looking for complete $GPRMC telegram (at least 10 fields) ... Nmea = ser_gps.readline() if quit_logging: # Did user hit <ENTER> key (see note above)? ser_gps.close() print print "Bye" exit() if '$GPRMC' in Nmea: L = Nmea.split(',') if len(L) > 10: Hunting = False # Found it! if L[2] == 'V': print "ERROR (NOT LOGGING): NO FIX ->", Nmea.strip() Hunting = True sound(SOUND_GPS_HUNTING_FIX) ser_gps.close() #-------------------------------------------------------------------- #---- Now build filename, like '2010_05_18_053753.txt' #-------------------------------------------------------------------- Year = '20' + L[9][4] + L[9][5] # We've created a year 2100 problem here .. :) Month = L[9][2] + L[9][3] Day = L[9][0] + L[9][1] Time = L[1] if '.' in Time: Time = Time.split('.')[0] # Dump fraction of seconds, if present DateObject = datetime.date(int(Year), int(Month), int(Day)) DayOfYear = DateObject.timetuple()[7] FileName = "%d_%02d_%02d_%s.txt" % (int(Year), int(Month), int(Day), Time) #--------------------------------------------------------------------- #---- First check that log directory exists - and if not, create it #--------------------------------------------------------------------- if not os.path.exists(LOG_DIRECTORY): print print "Log directory %s does not exist, create it ..." % LOG_DIRECTORY try: os.makedirs(LOG_DIRECTORY) except: print''' ------------------------------------------------------------------------------- ___ __ __ __ __ |__ |__) |__) / \ |__) |___ | \ | \ \__/ | \ Could not create log directory. Perhaps you do not have access rights; if so, either execute script with administrative rights, or change log directory name in script heading so you create folder within your own home directory. The disk could also be full. ------------------------------------------------------------------------------- ''' exit() try: LogFile = open(LOG_DIRECTORY + '/' + FileName, 'w') except: print''' ------------------------------------------------------------------------------- ___ __ __ __ __ |__ |__) |__) / \ |__) |___ | \ | \ \__/ | \ Could not create log file. ------------------------------------------------------------------------------- ''' exit() print ''' ------------------------------------------------------------------------------- __ __ __ __ _____ __ ___ __ __ | / \/ _ / _ ||\ |/ _ (_ | /\ |__) | |_ | \ |__\__/\__)\__)|| \|\__) __) | /--\| \ | |__|__/ T E R M I N A T E B Y P R E S S I N G < E N T E R > Status: File name: %s Directory: %s Log interval GPS track data: %d seconds Echo Sounder data (if present) captured continuously. ------------------------------------------------------------------------------- ''' % (FileName, LOG_DIRECTORY, interval) #--- Open GPS COM port ser_gps = serial.Serial(port=gps.port, baudrate=gps.speed, timeout=gps.timeout) #--- Only start thread looking for Echo Sounder data when port exists ... if echo_sounder.port >= 0: thread.start_new(echo_sounder_thread,()) no_of_echosounder_records = 0 no_of_gps_records = 0 k = interval - 1 #-------------------------------------------------------------------------------- #------- Logging loop starts here #-------------------------------------------------------------------------------- while not quit_logging: # Keyboard thread can change this global variable Nmea = ser_gps.readline() # Note: COM port is opened with 2 second time-out, thus data loss will be detected. if not Nmea: print "Warning: No data from GPS" sound(SOUND_GPS_NO_DATA) if '$GPRMC' in Nmea: #---- First get any Echo Sounder data being sent through queue, from Echo Sounder thread ... #---- IMPORTANT: This is a LIFO queue. #---- Provide warning if there are items left in the queue after we popped the most recent one. #---- It should not happen, as Furuno Echo Sounder data rep period is 2 seconds, and this main loop #---- executes once per second (if the GPS sends data). #---- Have to be careful in this Producer - Consumer problem ... echo_sounder_item = '' queue_overflow = False try: echo_sounder_item = echo_sounder_queue.get_nowait() # Don't block on missing data - just continue while not echo_sounder_queue.empty(): discard = echo_sounder_queue.get_nowait() queue_overflow = True except: pass if queue_overflow: print "Warning: Echo Sounder data rate exceeds processing capacity, missing data ..." if echo_sounder_item: # This section executes if we got Echo Sounder data print "E.S.: %s @ %s" % (echo_sounder_item, Nmea.strip()) try: LogFile.write('%s @ %s\n' % (echo_sounder_item, Nmea.strip())) LogFile.flush() no_of_echosounder_records += 1 except: print "WARNING: Could not write Echo Sounder data to file" k += 1 #----- Counter for file log control L = Nmea.split(',') #----- Example telegram: $GPRMC,104241.000,A,6023.0473,N,00519.8006,E,0.20,306.12,160510,,*02 if len(L) > 10: #----- NOTE: Trial GPS also provides fraction of seconds (not usual) if L[2] == 'V': print "Fix is lost --> ", Nmea.strip() sound(SOUND_GPS_NO_FIX) else: #---- Courtesy: Provide current LAT/LON/TIME information every minute, at the minute: Latitude = L[3] Longitude = L[5] NS_Hemisphere = L[4] EW_Greenwich = L[6] Time = L[1] if '.' in Time: Time = Time.split('.')[0] # Get integer part of time (some GPS provide fraction of seconds) try: if int(Time[4:6]) == 0: print ''' ******************************************************************* *** *** L A T I T U D E : %3s deg %s min %s *** *** L O N G I T U D E : %3s deg %s min %s *** *** T I M E : %s:%s:%s UTC *** ******************************************************************* ''' % ( Latitude[:2], Latitude[2:], NS_Hemisphere, Longitude[:3], Longitude[3:], EW_Greenwich, Time[:2], Time[2:4], Time[4:] ) except: print "Warning: Format error GPS NMEA telegram - cannot print LAT/LON/TIME block." #----- Write GPS telegram to file? if k == interval: print "GPS: ", Nmea.strip() try: LogFile.write(Nmea.strip() + '\n') LogFile.flush() Nmea = '' except: sound(SOUND_LOG_ERROR) print''' ------------------------------------------------------------------------------- ___ __ __ __ __ |__ |__) |__) / \ |__) |___ | \ | \ \__/ | \ Could not save data to file. Check free space on disk. ------------------------------------------------------------------------------- ''' k = 0 no_of_gps_records += 1 if echo_sounder.alive: sound(SOUND_LOG_MAX) else: sound(SOUND_LOG_MIN) ser_gps.close() LogFile.close() sound(SOUND_EXIT) print ''' ------------------------------------------------------------------------------- __ __ __ __ __ ___ __ __ __ ___ __ | / \ / _` / _` | |\ | / _` /__` | / \ |__) |__) |__ | \ |___ \__/ \__> \__> | | \| \__> .__/ | \__/ | | |___ |__/ Status: File name ............: %s Directory ............: %s GPS Records ..........: %d GPS Log Interval .....: %d seconds Echo Sounder Records .: %d ------------------------------------------------------------------------------- ''' % (FileName, LOG_DIRECTORY, no_of_gps_records, interval, no_of_echosounder_records) time.sleep(1) exit() #------------------------------------------------ # END OF PROGRAM #------------------------------------------------