#!/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. * ***************************************************************************/ =end DEBUG = false PVERSION = 1.2 =begin Input/output file format for GarminReadWrite is a plain-text comma-separated-value database: latitude,longitude,altitude,name,description 1. 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 =end class GarminReadWrite def 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 # initialize def do_at_exit() @garmin_serial.close if @garmin_serial end # do_at_exit def setup() unless @garmin_serial # unless already set up unless FileTest.exist? @ser_port puts "Error: input port #{@ser_port} doesn't exist, quitting." exit 0 end @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.downcase when /gps iii/ @garmin_unit_type = 1 when /gps 5/ @garmin_unit_type = 2 when /gpsmap76/ @garmin_unit_type = 2 else # let's assume unknown units are new @garmin_unit_type = 2 end end # unless port already opened end # setup def 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}" if DEBUG idstr = extract_strings(data,4)[0] puts "ID String: #{idstr}" if DEBUG return idstr,id end # get garmin id def debug_print(s) print s if DEBUG end def send_garmin_char(b,ck = 0) b &= 0xff; ck += b debug_print sprintf("%02x ",b) @garmin_serial.syswrite(b.chr) return ck end # send garmin char # garmin requires a double send of 0x10 # for all data except the initial 0x10 # and the end mark 0x1003 def send_garmin_char_escaped(b,ck = 0) ck = send_garmin_char(b,ck) send_garmin_char(b) if(b == 0x10) # send 0x10 twice return ck end # send garmin char escaped def recv_garmin_char() b = @garmin_serial.sysread(1)[0] debug_print sprintf("%02x ",b) return b end # recv garmin char =begin read_garmin state machine: state explanation --------------------------- 0 start of message 1 type of message 2 data length (always 1 byte) 3 data 4 checksum 5 end mark =end def read_garmin() debug_print "read_garmin: " state = 0 type = -1 length = -1 data = [] datacount = -1 prior = -1 cksum = -1 cksum_valid = false begin c = recv_garmin_char() case state when 0 # start # skip bytes until 0x10 seen if(c == 0x10) state += 1 end when 1 # type byte type = c state += 1 prior = 0 cksum = c # start checksum here when 2 # length byte if((c != 0x10) || (prior == 0x10)) length = c datacount = 0 cksum += c state += 1 end when 3 # data block if((c != 0x10) || (prior == 0x10)) data << c cksum += c datacount += 1 state += 1 if(datacount == length) end when 4 # checksum byte if((c != 0x10) || (prior == 0x10)) cksum = 0x100 - (cksum & 0xff) cksum_valid = (cksum == c) state += 1 end when 5 # first byte of end mark if(c == 0x10) state += 1 end when 6 # second byte of end mark if(c == 0x03) state += 1 end end prior = c end while 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 match puts "checksum error." return nil end end # read garmin def read_garmin_ack() data,type = read_garmin() return type == 0x06 end # read garmin ack def 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.each do |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 == 0x06 end # send garmin def send_garmin_ack(type) send_garmin(0x06,[type,0]) end # send garmin ack def 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 s end # convert int bytes to float def convert_float_to_int_bytes(v) # to radians v *= Math::PI/180.0 # to internal representation v *= 683565275.576431632 # to byte array return [v.to_i].pack('i').unpack('C*') end # convert float to int bytes def 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 s end # convert float bytes to float def convert_float_to_float_bytes(v) return [v].pack('e').unpack('C*') end # convert float to float bytes # split string data on zero terminations def extract_strings(data,n) return data[n..-1].pack('C*').split(/\0+/) end # extract strings # remove leading and trailing whitesoace def 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 newer def 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 data def 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) end end end while type != 0x0c return output.join("\n") + "\n" end # read garmin waypoint set def 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 block end def 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 0xff 1.upto(12) do block << 0xff end 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 block end def 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.each do |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 block if(@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 > 0 end # each record # send garmin end-of-records frame send_garmin(0x0c,[0x23,0]) end # write garmin waypoint def 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.close end end def 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" else while(arg = args.shift) case arg.downcase when /^-p/ @ser_port = args.shift when /^-r/ @read_mode = true when /^-w/ @read_mode = false else @file_mode = true read_write_file(arg) end end unless(@file_mode) if(@read_mode) print read_garmin_waypoint_set() else write_garmin_waypoint_set($stdin) end end # default stdin/stdout mode end end # process end # class GarminReadWrite grw = GarminReadWrite.new grw.process(ARGV)