Home | Ruby | Programming Icom Radios |     Share This Page
IcomProgrammer Ruby Listing
All content Copyright © 2007, P. LutusMessage Page
Version 1.4, 12/10/2010

Click here for a plain-text copy of the Ruby script.
The Listing
#!/usr/bin/ruby -w

=begin
/***************************************************************************
 *   Copyright (C) 2010, 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.5

class IcomProgrammer

   def initialize()

      @windows = (PLATFORM =~ /mswin/)

      @baud_rate = "19200" # fastest baud rate Icom radios will tolerate

      if(@windows) # Windows setup
         @ser_port = "COM1:" # default serial port, change to suit your needs
         @ser_config = "MODE #{@ser_port} baud=#{@baud_rate} parity=n data=8 stop=1"
      else # Linux setup
         @ser_port = "/dev/ttyUSB0" # default serial port, change to suit your needs
         @ser_config = "stty -F #{@ser_port} #{@baud_rate} raw -echo"
      end

      @data_directory = "frequency_data" # a subdirectory of the program's directory

=begin

Record format for radio_info_hash:
	"radio_name" => [hex_radio_code,["table_name_1","table_name_2","etc.."]],

The special name "E" means "erase remaining unused memory locations"

The file paths are constructed like this: data_directory + "/" + table_name + ".csv"

Each table is a comma-separated-value (csv) data table, like this:

Mode,RxFreq,TxFreq,RxTone,TxTone
fm,145.37,144.77,110.9,100.0

Not all data tables need all these fields. Here is a minimal table header and entry:

Mode,RxFreq
am,0.540

Icom hex radio codes:

Ham Radios:

IC-703		0x68
IC-706  	0x4e
IC-706MKIIG 	0x58
IC-718		0x5e
IC-725		0x28
IC-726		0x30
IC-728		0x38
IC-729		0x3a
IC-735		0x04
IC-736		0x40
IC-746		0x56
IC-746Pro	0x66
IC-751		0x1c
IC-756PRO	0x5c
IC-756PRO-II	0x64
IC-756Pro-III	0x6e
IC-761		0x1e
IC-765		0x2c	
IC-775  	0x46
IC-781		0x26
IC-970		0x2e
IC-7000 	0x70
IC-7200		0x76
IC-7600		0x7a
IC-7700		0x74
IC-7800		0x6a
IC-R71		0x1A
IC-R72		0x32
IC-R75		0x5a
IC-R7000 	0x08
IC-R7100 	0x34
IC-R8500 	0x4a
IC-R9000 	0x2a

Marine Radios

M-7000Pro	0x02
M-710		0x01
M-710RT		0x03
M-802		0x08
Any		0x00 (any Icom marine radio)

=end

      @radio_info_hash = {
         "IC-706-Boat" => [0x4e,["ham_hf","marine_hf","marine_vhf_short","E"]],
         "IC-706-Home" => [0x4e,["ham_hf","marine_hf","vhf_repeaters_short","E"]],
         "IC-746" => [0x56,["ham_hf","marine_hf","marine_vhf_short","vhf_repeaters_short","E"]],
         "IC-756" => [0x5c,["ham_hf","marine_hf","E"]],
         "IC-R8500" => [0x4a,["ham_hf","marine_hf","cb","marine_vhf_long","vhf_repeaters_long","E"]]
      }

      @radio_serial = nil

      @modeNames = [
         "lsb",
         "usb",
         "am",
         "cw",
         "rtty",
         "fm",
         "wfm"
      ]

      # create a hash of mode names and numbers
      @modes = {}
      i = 0
      @modeNames.each do |s|
         @modes[s] = i
         i += 1
      end
      @radio_name = ""
      @mem_bank = -1
      @mem_loc = -1

      # hook the exit routine
      at_exit { do_at_exit() }
   end # initialize

   def do_at_exit()
      @radio_serial.close if @radio_serial
   end # do_at_exit

   def setup()
      unless @radio_serial # unless already set up
         unless FileTest.exist? @ser_port
            puts "Error: input port #{@ser_port} doesn't exist, quitting."
            exit 0
         end

         # configure the serial port
         system(@ser_config)

         # now open it for reading and writing
         @radio_serial = File.open(@ser_port,"rb+")
         # must always reinitialize after opening
         system(@ser_config)
      end
   end

   def debug_print(s)
      print s if DEBUG
   end

   def read_radio(n)
      debug_print "Read radio: "
      count = 0
      reply = []
      begin
         c = @radio_serial.sysread(1)[0]
         reply << c
         debug_print sprintf("%02x ",c)
         count += 1
      end while count < n
      debug_print "\n"
      return reply
   end # read_radio

   def write_radio(com)
      debug_print "Write radio: "
      com.each do |b| # send the com a byte at a time
         debug_print sprintf("%02x ",b)
         @radio_serial.syswrite b.chr()
      end
      debug_print "\n"
      read_radio(com.length) # absorb the radio's echo
   end #write_radio

   def read_response()
      reply = read_radio(6)
      return reply[4] == 0xfb # meaning no errors
   end # read_response

   def convert_bcd(n,count)
      bcd_array = []
      1.upto(count) do
         bcd_array << ((n % 10) | ((n/10) % 10) << 4)
         n /= 100
      end
      return bcd_array
   end # convert_bcd

   # send a formatted command to a particular radio
   def send_com(c,data = nil)
      com = [ 0xfe,0xfe,@radio_hex_id,0xe0 ]
      com << c
      if(data)
         data.each do |b|
            com << b
         end
      end
      com << 0xfd
      write_radio(com)
      unless read_response()
         err = "Error: "
         com.each do |b|
            err += sprintf("%02x ",b)
         end
         err += "\n"
         debug_print err
      end # response error printing block
   end # send_com

   # just to give it a name
   def set_memory_mode()
      send_com(0x08)
   end

   def set_vfo(n)
      if(@current_vfo != n)
         @current_vfo = n
         send_com(0x07) # select VFO mode (required for IC-756)
         send_com(0x07,[ 0xd0 + n ]) # select VFO main/sub (required for IC-756)
         send_com(0x07,[ n ]) # select VFO
      end
   end # set_vfo

   def set_split(state)
      @split = state
      send_com(0x0f,[ @split?1:0 ]) # split off
   end # set_split

   # "set_memory_bank" is only needed for receivers IC-R8500 and IC-7000.

   def set_memory_bank(mb)
      if(mb != @mem_bank)
         bcd = convert_bcd(mb,1)
         send_com(0x08,bcd.reverse.unshift(0xA0))
         @mem_bank = mb
      end
   end # set_memory_bank

   def set_memory_addr(m)
      if @radio_name =~ /(IC-R8500|IC-R7000)/
         mi = (m.to_i)
         ma = mi % 40
         mb = mi / 40
         set_memory_bank(mb)
         bcd = convert_bcd(ma,2)
      else
         bcd = convert_bcd((m.to_i)+1,2)
      end
      send_com(0x08,bcd.reverse)
      print "." # user feedback
      $stdout.flush # user feedback
   end # set_memory_addr

   def set_vfo_freq(n)
      n = (n.to_f * 1e6) + 0.5
      n = n.to_i
      bcd = convert_bcd(n,5)
      send_com(0x05,bcd)
   end # set_vfo_freq

   def set_vfo_tone(n,f)
      f = (f.to_f * 10) + 0.5
      f = f.to_i
      bcd = convert_bcd(f,2)
      bcd = bcd.reverse
      bcd.unshift n
      send_com(0x1b,bcd)
   end # set_vfo_tone

   def set_vfo_mode(s)
      s = s.gsub(/"/,"")
      send_com(0x06,[ @modes[s] ])
   end # set_vfo_mode

   def get_field_by_name(name_hash,fields,name)
      r = nil
      if(name_hash.has_key?(name))
         n = name_hash[name]
         if(n < fields.size && fields[n].length > 0)
            r = fields[n]
         end
      end
      return r
   end # get_field_by_name

   def process_file(file)
      debug_print file + "\n"
      if (file == "E") # erase unused locations
         set_memory_mode()
         mod = 100 # for most radios
         mod = 40  if @radio_name =~ /(IC-R8500|IC-R7000)/

         while(@mem_loc == 0 || @mem_loc % mod != 0)
            set_memory_addr(@mem_loc)
            send_com(0x0b)
            @mem_loc += 1
         end

      else # normal data file
         data = File.read(@data_directory + "/" + file + ".csv")
         data.gsub!(%r{"},"") # remove all quotes

         records = data.split("\n")

         header = records.shift # get header line

         # create hash to translate field names into numbers
         name_hash = {}
         n = 0
         header.split(",").each do |name|
            if(name && name.length > 0)
               name_hash[name] = n
            end
            n += 1
         end

         @current_vfo = -1
         set_split(false)

         records.each do |record|
            fields = record.split(",")

            # these don't all have to be defined in each table

            mode = get_field_by_name(name_hash,fields,"Mode")
            rxf = get_field_by_name(name_hash,fields,"RxFreq")
            txf = get_field_by_name(name_hash,fields,"TxFreq")
            rxt = get_field_by_name(name_hash,fields,"RxTone")
            txt = get_field_by_name(name_hash,fields,"TxTone")

            if(rxf && mode) # minimum required information
               set_memory_addr(@mem_loc)
               if(txf) # if transmit frequency specified
                  set_split(true) unless @split
                  set_vfo(1)
                  set_vfo_freq(txf)
                  set_vfo_mode(mode)
                  if(txt) # transmit repeater tone
                     send_com(0x16,[ 0x42,0x1 ]) # repeater tone on
                     set_vfo_tone(0,txt)
                  else
                     send_com(0x16,[ 0x42,0x0 ]) # repeater tone off
                  end
               else
                  set_split(false) if @split
               end
               set_vfo(0)
               set_vfo_freq(rxf)
               set_vfo_mode(mode)
               if(rxt) # receiver tone squelch
                  send_com(0x16,[ 0x43,0x1 ]) # tone squelch on
                  set_vfo_tone(1,rxt)
               else
                  send_com(0x16,[ 0x43,0x0 ]) # tone squelch off
               end
               send_com(0x09) # write mem
               @mem_loc += 1
            end # defined rxf and mode
         end # record
      end # normal file read
   end # process_file

   def program_radio(radio_name)

      unless @radio_info_hash.has_key?(radio_name)
         puts "Error: don't recognize radio name \"#{radio_name}\", stopping."
         return
      end
      print "Programming #{radio_name} "
      @radio_name = radio_name
      @radio_info = @radio_info_hash[radio_name]

      @radio_hex_id = @radio_info[0]
      file_list = @radio_info[1]
      @mem_loc = 0
      file_list.each do |fn|
         process_file(fn)
      end

      # go to memory location 0 on exit
      set_memory_addr(0)
      puts "" # user feedback
   end # program_radio

   # "generate_lists" creates master CSV data files for each radio,
   # with memory locations, as programmed by IcomProgrammer.
   # This is not as easy as it might sound --
   # different tables for the same radio are allowed
   # to have different field names and positions.

   # /add/remove/change order of/ field names in this list to suit your needs

   FIELD_NAMES = [ "Bank","Mem","Name","Mode","RxFreq",
      "TxFreq","RxTone","TxTone","Comment",
      "Place","Call","Sponsor","Region"
   ]

   def generate_lists()
      data_path = "radio_lists"
      Dir.mkdir(data_path) unless FileTest.exists?(data_path)
      @radio_info_hash.keys.sort.each do |key|
         # create a hash of field names to numbers
         header_hash = {}
         used_hash = {}
         n = 0
         FIELD_NAMES.each do |fieldname|
            header_hash[fieldname] = n
            used_hash[fieldname] = false
            n += 1
         end
         banked_mem_radio = (key =~ /(IC-R8500|IC-R7000)/)
         field_hash = {} # for global scope
         table = []
         table << FIELD_NAMES # add header to table
         mem_loc = 0
         bank_num = 0
         bmem_num = 0
         @radio_info_hash[key][1].each do |file|
            unless (file == "E")
               data = File.read(@data_directory + "/" + file + ".csv")
               data.gsub!(%r{"},"") # strip all quotes
               recn = 0
               data.split("\n").each do |record|
                  if(banked_mem_radio)
                     mi = (mem_loc.to_i)
                     bmem_num = mi % 40
                     bank_num = mi / 40
                  end
                  fields = record.split(",")
                  if(recn == 0) # header
                     n = 0
                     field_hash = {}
                     fields.each do |fieldname|
                        field_hash[fieldname] = n
                        n += 1
                     end
                  else # a data record
                     rec_arr = []
                     FIELD_NAMES.each do |name|
                        case name
                        when "Bank"
                           if(banked_mem_radio)
                              rec_arr << "#{bank_num}"
                              used_hash[name] = true
                           else
                              rec_arr << ""
                           end
                        when "Mem"
                           if(banked_mem_radio)
                              rec_arr << "#{bmem_num}"
                           else
                              rec_arr << "#{mem_loc+1}"
                           end
                           used_hash[name] = true
                        else
                           if(field_hash.has_key?(name))
                              item = fields[field_hash[name]]
                              if(item)
                                 rec_arr << item
                                 used_hash[name] = true
                              else
                                 rec_arr << ""
                              end
                           else
                              rec_arr << ""
                           end
                        end
                     end # each header name
                     table << rec_arr
                     mem_loc += 1
                  end # header/record
                  recn += 1
               end # each record
            end # unless filename == "E"
         end # each file in file list
         # now open and write data file
         fn = data_path + "/" + key.gsub(/\s/,"_") + ".csv"
         f = File.open(fn,"w")
         table.each do |record|
            outrec = []
            n = 0
            # only add fields that were used
            FIELD_NAMES.each do |fieldname|
               if(used_hash[fieldname])
                  outrec << record[n]
               end
               n += 1
            end # each field
            f.write "\"" + outrec.join("\",\"") + "\"\n"
         end # each record
         f.close
         puts "Created data file #{fn}."
      end # each radio
   end # generate lists

   def process_list(list)
      list.each do |radio_name|
         if(radio_name == "-g")
            generate_lists()
         else
            setup()
            program_radio(radio_name)
         end
      end
   end # process list

   def process(args)
      # if one or more radio names are
      # specified on the command line
      if(args[0])
         # process command-line radio names
         process_list(args)
      else # show a menu of choices
         begin
            puts "Choose an action:"
            menu_list = []
            @radio_info_hash.keys.sort.each do |key|
               menu_list << "Program " + key
            end
            menu_list << "Generate Memory Lists"
            menu_list << "Quit"
            index = 1
            menu_list.each do |name|
               puts "   #{index}) #{name}"
               index += 1
            end
            print "Choose (1 - #{index-1}):"
            line = readline.chomp
            if(line =~ /\d+/)
               choice = (line.to_i)-1
               if(choice >= 0 && choice < index)
                  option = menu_list[choice]
                  case option
                  when "Quit" # no action
                  when "Generate Memory Lists"
                     generate_lists()
                  else # program a radio
                     process_list([option.sub(/Program /,"")])
                  end
               end
            end
         end while option != "Quit"
      end
   end # process args

end # class IcomProgrammer

ip = IcomProgrammer.new
ip.process(ARGV)
 

Home | Ruby | Programming Icom Radios |     Share This Page