#!/usr/bin/ruby -w=begin/*************************************************************************** * Copyright (C) 2008, Paul Lutus * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/=endDEBUG=falsePVERSION=1.2=beginInput/output file format for GarminReadWrite is aplain-text comma-separated-value database:latitude,longitude,altitude,name,description1. No header in the data file.2. Longitude and latitude are expressed in decimal degrees.3. Altitude is expressed in meters.Be sure to set GPS unit to garmin/garmin mode=endclassGarminReadWritedef initialize()@garmin_unit_type=-1@garmin_serial=nil@read_mode=true@file_mode=false@windows= (PLATFORM=~/mswin/)@baud_rate=9600 # standard garmin mode baud rate # default serial port, can be changed on command line@ser_port= (@windows)?"COM1:":"/dev/ttyUSB0"at_exit{ do_at_exit() }end # initializedef do_at_exit()@garmin_serial.closeif@garmin_serialend # do_at_exitdef setup()unless@garmin_serial # unless already set upunlessFileTest.exist?@ser_portputs"Error: input port #{@ser_port} doesn't exist, quitting."exit0end@ser_config= (@windows)? \"MODE #{@ser_port} baud=#{@baud_rate} parity=n data=8 stop=1" \ :"stty -F #{@ser_port}#{@baud_rate} raw -echo" # configure the serial port`#{@ser_config}` # open it for reading and writing@garmin_serial=File.open(@ser_port,"rb+") # find out which Garmin unit str,id = get_garmin_id()case str.downcasewhen/gps iii/@garmin_unit_type=1when/gps 5/@garmin_unit_type=2when/gpsmap76/@garmin_unit_type=2 else # let's assume unknown units are new@garmin_unit_type=2endend # unless port already openedend # setupdef get_garmin_id() setup() # alert garmin: send ID send_garmin(0xfe,[0x00,0x02]) data,type = read_garmin() id = data[2,2].pack('C*').unpack('s')[0]puts"ID number: #{id}"ifDEBUG idstr = extract_strings(data,4)[0]puts"ID String: #{idstr}"ifDEBUGreturn idstr,idend # get garmin iddef debug_print(s)print s ifDEBUGenddef send_garmin_char(b,ck =0) b &=0xff; ck += b debug_print sprintf("%02x ",b)@garmin_serial.syswrite(b.chr)return ckend # send garmin char # garmin requires a double send of 0x10 # for all data except the initial 0x10 # and the end mark 0x1003def send_garmin_char_escaped(b,ck =0) ck = send_garmin_char(b,ck) send_garmin_char(b) if(b ==0x10) # send 0x10 twicereturn ckend # send garmin char escapeddef recv_garmin_char() b =@garmin_serial.sysread(1)[0] debug_print sprintf("%02x ",b)return bend # recv garmin char=beginread_garmin state machine:state explanation---------------------------0 start of message1 type of message2 data length (always 1 byte)3 data4 checksum5 end mark=enddef read_garmin() debug_print "read_garmin: " state =0 type =-1 length =-1 data =[] datacount =-1 prior =-1 cksum =-1 cksum_valid =falsebegin c = recv_garmin_char()case statewhen0 # start # skip bytes until 0x10 seenif(c ==0x10) state +=1endwhen1 # type byte type = c state +=1 prior =0 cksum = c # start checksum herewhen2 # length byteif((c !=0x10) || (prior ==0x10)) length = c datacount =0 cksum += c state +=1endwhen3 # data blockif((c !=0x10) || (prior ==0x10)) data << c cksum += c datacount +=1 state +=1if(datacount == length)endwhen4 # checksum byteif((c !=0x10) || (prior ==0x10)) cksum =0x100- (cksum &0xff) cksum_valid = (cksum == c) state +=1endwhen5 # first byte of end markif(c ==0x10) state +=1endwhen6 # second byte of end markif(c ==0x03) state +=1endend prior = cendwhile state <7 debug_print "checksum: #{cksum_valid}\n"if(cksum_valid) # don't ACK the other side's ACK send_garmin_ack(type) unless(type ==0x06)return data,type else # error condition: checksums don't matchputs"checksum error."returnnilendend # read garmindef read_garmin_ack() data,type = read_garmin()return type ==0x06end # read garmin ackdef send_garmin(type,data) debug_print "send_garmin: " send_garmin_char(0x10) cksum = send_garmin_char(type) cksum = send_garmin_char_escaped(data.size,cksum) data.eachdo|b| cksum = send_garmin_char_escaped(b,cksum)end send_garmin_char_escaped(0x100- (cksum &0xff)) send_garmin_char(0x10) send_garmin_char(0x03) debug_print "\n" # don't expect ACK for ACK read_garmin_ack() unless type ==0x06end # send garmindef send_garmin_ack(type) send_garmin(0x06,[type,0])end # send garmin ackdef convert_int_bytes_to_float(data,n,res=5) # create int, convert v = data[n,4].pack('C*').unpack('i')[0] # from internal representation to radians v /= 683565275.576431632 # radians to degrees v *=180.0/Math::PI # specify decimal places s =sprintf("%.#{res}f",v)return send # convert int bytes to floatdef convert_float_to_int_bytes(v) # to radians v *=Math::PI/180.0 # to internal representation v *=683565275.576431632 # to byte arrayreturn[v.to_i].pack('i').unpack('C*')end # convert float to int bytesdef convert_float_bytes_to_float(data,n,res=5) v = data[n,4].pack('C*').unpack('e')[0] # specify decimal places s =sprintf("%.#{res}f",v)return send # convert float bytes to floatdef convert_float_to_float_bytes(v)return[v].pack('e').unpack('C*')end # convert float to float bytes # split string data on zero terminationsdef extract_strings(data,n)return data[n..-1].pack('C*').split(/\0+/)end # extract strings # remove leading and trailing whitesoacedef trim(s)return s.sub(%r{^\s*(.*?)\s*$},"\\1")end # for Garmin units before "GPS V"def decode_old_waypoint(data) # waypoint ID string starts at block position 0 name = trim(data[0...6].pack('C*')) # lat (int) begins at block position 6 lat = convert_int_bytes_to_float(data,6) # lng (int) begins at block position 10 lng = convert_int_bytes_to_float(data,10) # alt (short float) begins at block position 28 alt =0 # no altitude from the older units # name and desc, zero-terminated strings, # start at block position 18 desc = extract_strings(data,18)[0]return"#{lat},#{lng},#{alt},#{name},#{desc}"end # decode old waypoint data # for Garmin units "GPS V" and newerdef decode_new_waypoint(data) # lat (int) begins at block position 24 lat = convert_int_bytes_to_float(data,24) # lng (int) begins at block position 28 lng = convert_int_bytes_to_float(data,28) # alt (short float) begins at block position 28 alt = convert_float_bytes_to_float(data,32) # name and desc, zero-terminated strings, # start at block position 52 name,desc = extract_strings(data,52)return"#{lat},#{lng},#{alt},#{name},#{desc}"end # decode new waypoint datadef read_garmin_waypoint_set() setup() output =[] send_garmin(0x0a,[0x07,0])begin data,type = read_garmin()if(type ==0x23)if(@garmin_unit_type==1) output << decode_old_waypoint(data) else output << decode_new_waypoint(data)endendendwhile type !=0x0creturn output.join("\n") +"\n"end # read garmin waypoint setdef create_old_garmin_block(s_lat,s_lng,s_alt,name,desc) block =[] ns = name[0,6]while (ns.length<6) ns +=' 'end block << ns.unpack('C*') block << convert_float_to_int_bytes(s_lat.to_f) block << convert_float_to_int_bytes(s_lng.to_f) block <<[0,0,0,0] # bogus date/time block << desc.unpack('C*') <<0 # string with terminating zero block.flatten!return blockenddef create_new_garmin_block(s_lat,s_lng,s_alt,name,desc) block =[] # unknown block block <<[0x01,0x00,0x1f,0x70,0x12,0x00,0x00,0x00,0x00,0x00,0x00,0x00] # unknown block of 12 0xff1.upto(12) do block <<0xffend block << convert_float_to_int_bytes(s_lat.to_f) block << convert_float_to_int_bytes(s_lng.to_f) block << convert_float_to_float_bytes(s_alt.to_f) # the value "0x51,0x59,0x04,0x69" is the default # and creates a data-unavailable display block <<[0x51,0x59,0x04,0x69] # depth, unused, float block <<[0x51,0x59,0x04,0x69] # proximity distance, unused, float block <<[0x20,0x20,0x20,0x20] # unknown block <<[0xff,0xff,0xff,0xff] # unknown block << name.unpack('C*') <<0 # string with terminating zero block << desc.unpack('C*') <<0 # string with terminating zero block.flatten!return blockenddef write_garmin_waypoint_set(stream) setup() array = stream.readlines # must know count in advance count = array.size n =0 msb = count/256 lsb = count % 256 # alert garmin: sending (count) records send_garmin(0x1b,[lsb,msb]) array.eachdo|record| n +=1 # clean up record leading and trailing whitespace record = record.sub(%r{\s*(.*?)\s*},"\\1");if(record.length>0) fields = record.split(",")if(fields.size !=5)puts"Error: record #{n} doesn't have 5 fields: \"#{record}\", quitting."return;end s_lat,s_lng,s_alt,name,desc = fields debug_print "#{s_lat}|#{s_lng}|#{s_alt}|#{name}|#{desc}\n" # build a waypoint data blockif(@garmin_unit_type==1) block = create_old_garmin_block(s_lat,s_lng,s_alt,name,desc) else block = create_new_garmin_block(s_lat,s_lng,s_alt,name,desc)end # send garmin waypoint block send_garmin(0x23,block)end # if record.length > 0end # each record # send garmin end-of-records frame send_garmin(0x0c,[0x23,0])end # write garmin waypointdef read_write_file(fn)if(@read_mode) data = read_garmin_waypoint_set()File.open(fn,"w") {|f| f.write data } else fh =File.open(fn,"r") write_garmin_waypoint_set(fh) fh.closeendenddef process(args)if(!args[0])puts"usage: -p(ort), default #{@ser_port}"puts" -r(read from Garmin) [filename]"puts" -w(write to Garmin) [filename(s)]"puts" if no filename given,"puts" data will be read/written from stdin/stdout" elsewhile(arg = args.shift)case arg.downcasewhen/^-p/@ser_port= args.shiftwhen/^-r/@read_mode=truewhen/^-w/@read_mode=false else@file_mode=true read_write_file(arg)endendunless(@file_mode)if(@read_mode)print read_garmin_waypoint_set() else write_garmin_waypoint_set($stdin)endend # default stdin/stdout modeendend # processend # class GarminReadWritegrw =GarminReadWrite.newgrw.process(ARGV)