#!/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)