Project

General

Profile

New Model #2795 » icr6.py

John Bradshaw, 08/10/2024 09:16 AM

 
"""Icom IC-R6 Driver"""
# Copyright 2024 John Bradshaw Mi0SYN <john@johnbradshaw.org>
#
# 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 3 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, see <http://www.gnu.org/licenses/>.


# Notes:
# USA models have WX alert and block certain frequencies (see manual).

import logging

from chirp.drivers import icf
from chirp import chirp_common, directory, bitwise
from chirp.settings import RadioSettingGroup, RadioSetting, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueString, \
RadioSettingValueFloat, RadioSettings

LOG = logging.getLogger(__name__)

mem_format = """
// Channel memories: 1300x 16-byte blocks: 0x0000 to 0x513f inclusive
struct {
u8 freq0; // Freq: low byte
u8 freq1; // Freq: mid byte
u8 freq_flags:6, // Related to multiplier (step size)
freq2:2; // Freq: high bits - 18 bits total = step count
u8 af_filter:1, // AF Filter: 0=Off, 1=On
attenuator:1, // Attenuator: 0=Off, 1=On
mode:2, // Modulation: Index to "MODES"
tuning_step:4; // Freq tuning steps: Index to "STEPS"
u8 unknown4ab:2,
duplex:2, // 0 = None, 1 = Minus, 2 = Plus
unknown4e:1,
tmode:3; // TSQL/DTCS Setting: Index to "TONE_MODES"
u8 offset_l; // Offset - low value byte
u8 offset_h; // Offset - high value byte
u8 unknown7:2,
ctone:6; // TSQL Index. Valid range: 0 to 49
u8 unknown8;
u8 canceller_freq_h; // Canceller training freq index - high 8 bits
u8 canceller_freq_l:1, // - LSB
unknown10m:4,
vsc:1, // Voice Squelch Control: 0=Off, 1=On
canceller:2; // USA-only Canceller option: Index to "CANCELLER"
u8 name[5]; // 6 Chars coded into 5 bytes
} memory[1300];

#seekto 0x5dc0; // Unknown: 0x5140 to 0x5f7f inclusive

// 25x Scan Edges with names - 0x5dc0 to 0x5f4f inclusive
// Mulitply the 32-bit by 3 to get freq in Hz
struct {
u8 start0; // LSB
u8 start1;
u8 start2;
u8 start3; // MSB
u8 end0; // LSB
u8 end1;
u8 end2;
u8 end3; // MSB
u8 disabled:1,
mode:3, // 0=FM, 1=WFM, 2=AM, 4="-"
ts:4; // Same mapping as channel TS
u8 unknown9a:2,
attn:2, // 0=Off, 1=On, 2="-"
unknown9b:4;
char name[6];
} pgmscanedge[25];

#seekto 0x5f80; // Possibly padding

// Channel control flags: 0x5f80 to 0x69a7 inclusive
struct {
u8 hide_channel:1, // Channel enable/disable aka show/hide
skip:2, // Scan skip: 0=No, 1 = "Skip", 3 = mem&vfo ("P")
unknown0:5;
u8 unknown1;
} flags[1300];

#seekto 0x6bd0; // 8 bytes padding then 34x16 bytes unknown

// Device Settings: 0x6bd0 to 0x6c0f
struct {
u8 unknown[13]; // Bytes 0-12 inclusive
u8 unknown13_6bdd:6,
func_dial_step:2; // 00=100kHz, 01=1MHz, 02=10MHz
u8 unknown14;
u8 unknown15_6bdf:7,
key_beep:1; // 0=Off, 1=On
u8 unknown16_6be0:2,
beep_level:6; // 0x00=Volume, 0x01=00, 0x02=01, ..., 0x28=39
u8 unknown17_6be1:6,
back_light:2; // 00=Off, 01=On, 02=Auto1, 03=Auto2
u8 unknown18_6be2:7,
power_save:1; // 0=Off, 1=On
u8 unknown17_6be3:7,
am_ant:1; // 0=Ext, 1=Bar
u8 unknown20_6be4:7,
fm_ant:1; // 0=Ext, 1=Ear (headset lead)
u8 unknown21[13]; // Bytes 21-33 inclusive
u8 civ_address; // 6bf2: CI-V address, full byte
u8 unknown35_6bf3:5,
civ_baud_rate:3; // 6bf3: Index to CIV_BAUD_RATES, range 0-5
u8 unknown35_6bf4:7,
civ_transceive:1; // 6bf4: Report frequency and mode changes
u8 unknown37[15]; // Bytes 37-51
u8 unknown52h_6c04:3, // Fixed 001 seen during tests
dial_function:1, // 0=Tuning Dial, 1=Audio Volume
unknown52m_6c04:2, // Fixed 10 seen during tests
mem_display_type:2; // 00=Freq, 01=BankName, 02=MemName, 03=ChNum
u8 unknown54[11]; // Bytes 54-63 inclusive
} settings;

#seekto 0x6d00; // Unknown: 0x6c10 to 0x6cff

// Device comment string. Grab it from the ICF?
struct { // Start: 6d00, End: 6d0f
char comment[16];
} device_comment;

// 22x ASCII-coded bank names - 0x6d10 to 0x6dbf inclusive
struct {
char name[6];
u8 padding[2];
} bank_names[22];

// 10x ASCII-coded scan link names - 0x6dc0 to 0x6e0f
struct {
char name[6];
u8 padding[2];
} prog_scan_link_names[10];

#seekto 0x6e50; // Unknown - 0x6e10 to 0x6e4f

// The string "IcomCloneFormat3" at end of block
struct {
char footer[16];
} footer;
"""

TONE_MODES = ["", "TSQL", "TSQL-R", "DTCS", "DTCS-R"]
# Sub-audible tone settings:
# "TSQL" (with pocket beep), "TSQL" (no beep),
# "DTCS" (with pocket beep), "DTCS" (no beep),
# "TQQL-R" (Tone Squelch Reverse),
# "DTCS-R" (DTCS Reverse)”,
# "OFF"
TONES = list(chirp_common.TONES)

# USA model have a Canceller function with various options and
# training frequencies. Fields only show in CS-R6 after importing ICF from
# a USA model. Range 300-3000 in steps of 10 (AF Hz?)
CANCELLER = ("Off", "Train1", "Train2", "MSK")
# For the freq: 9 bits split over 2 bytes.
# Raw value is 30 (300Hz) to 300 (3000Hz)
# Default on European model: 228 (2280 Hz) which matches CS-R6

DUPLEX_DIRS = ["", "-", "+"] # Machine order

MODES = ["FM", "WFM", "AM", "Auto"]

STEPS = [5, 6.25, 8.333333, 9, 10, 12.5, 15, 20,
25, 30, 50, 100, 125, 200, "Auto"] # Index 15 is valid
# Note: 8.33k only within Air Band, 9k only within AM broadcast band

# Other per-channel settings from CS-R6
# DTCS_CODES = list(chirp_common.DTCS_CODES) - same 104 codes used
# DTCS Polarity:

# The IC-R6 manual has | and , but these aren't recognised by CS-R6 and
# the front panel shows : and . instead so we'll go those (per radio & CS-R6).
ICR6_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789()*+-./:= "

# Radio-coded alphabet. "^" is ivalid in IC-R6 so used here as a placeholder.
CODED_CHRS = " ^^^^^^^()*+^-./0123456789:^^=^^^ABCDEFGHIJKLMNOPQRSTUVWXYZ^^^^^"
NAME_LENGTH = 6

# Valid Rx Frequencies - for now using Global:
# USA: 0.1–821.995, 851–866.995, 896–1309.995 MHz
# France: 0.1–29.995, 50.2–51.2, 87–107.995, 144–146, 430–440, 1240–1300 MHz
# Global/Rest of World: 0.100–1309.995 MHz continuous

SQUELCH_LEVEL = ["Open", "Auto", "Level 1", "Level 2", "Level 3", "Level 4",
"Level 5", "Level 6", "Level 7", "Level 8", "Level 9"]


CIV_BAUD_RATES = ["300", "1200", "4800", "9600", "19200", "Auto"]

SKIPS = ["", "S", "?", "P"]


class ICR6Bank(icf.IcomBank):
"""ICR6 bank"""
def get_name(self):
_bank = self._model._radio._memobj.bank_names[self.index]
return str(_bank.name).rstrip()

def set_name(self, name):
if len(name) > 6:
return

# ASCII-coded but restricted to certain characters. Validate:
for c in name:
if c not in ICR6_CHARSET:
return

_bank = self._model._radio._memobj.bank_names[self.index]
_bank.name = name.rstrip()


@directory.register
class ICR6Radio(icf.IcomCloneModeRadio):
"""Icom IC-R6 Receiver - Global model"""
VENDOR = "Icom"
MODEL = "IC-R6"
_model = "\x32\x50\x00\x01"
_memsize = 0x6e60
_ranges = [(0x0000, _memsize, 32)]
_endframe = "Icom Inc\x2e73"

_num_banks = 22
_bank_class = ICR6Bank

@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = ("This radio driver is currently under development, "
"and not all the features or functions may work as"
"expected. You should proceed with caution.")
return rp

def get_features(self):
rf = chirp_common.RadioFeatures()
rf.memory_bounds = (0, 1299)
rf.valid_modes = list(MODES)
rf.valid_tmodes = list(TONE_MODES)
rf.valid_duplexes = list(DUPLEX_DIRS)
rf.valid_bands = [(100000, 1309995000)]
rf.valid_skips = ["", "S", "P"] # Not 1:1 mapping
rf.valid_characters = ICR6_CHARSET
rf.valid_name_length = NAME_LENGTH
rf.can_delete = True
rf.has_ctone = True
rf.has_dtcs = False # TODO
rf.has_dtcs_polarity = False # TODO
rf.has_bank = False # TODO
rf.has_bank_names = False # TODO
rf.has_name = True
rf.has_settings = True
rf.has_tuning_step = False # hide in GUI, manage by code
# rf.valid_tuning_steps = list(STEPS)
# Attenuator = True
return rf

def process_mmap(self):
self._memobj = bitwise.parse(mem_format, self._mmap)

def get_raw_memory(self, number):
return repr(self._memobj.memory[number])

def get_memory(self, number):
_mem = self._memobj.memory[number]
_flag = self._memobj.flags[number]

mem = chirp_common.Memory()
mem.number = number

if _flag.hide_channel == 1:
mem.empty = True
return mem
mem.empty = False

if _mem.freq_flags == 0:
mem.freq = 5000 * (_mem.freq2 * 256 * 256 +
_mem.freq1 * 256 +
int(_mem.freq0))
mem.offset = 5000 * (_mem.offset_h * 256 + _mem.offset_l)
elif _mem.freq_flags == 20:
mem.freq = 6250 * (_mem.freq2 * 256 * 256 +
_mem.freq1 * 256 +
int(_mem.freq0))
mem.offset = 6250 * ((_mem.offset_h * 256) + _mem.offset_l)
elif _mem.freq_flags == 40:
mem.freq = 8333.3333 * (_mem.freq2 * 256 * 256 +
_mem.freq1 * 256 +
int(_mem.freq0))
mem.offset = 8333.3333 * ((_mem.offset_h * 256) + _mem.offset_l)
elif _mem.freq_flags == 60:
mem.freq = 9000 * (_mem.freq2 * 256 * 256 +
_mem.freq1 * 256 +
int(_mem.freq0))
mem.offset = 9000 * ((_mem.offset_h * 256) + _mem.offset_l)
else:
LOG.exception(f"Unknown freq multiplier: {_mem.freq_flags}")
mem.freq = 1234567890
mem.offset = 0

# mem.tuning_step = STEPS[_mem.tuning_step]
mem.duplex = DUPLEX_DIRS[_mem.duplex]

mem.ctone = TONES[_mem.ctone]
mem.tmode = TONE_MODES[_mem.tmode]

mem.mode = MODES[_mem.mode]

# memory scan skip
if _flag.skip == 0:
mem.skip = "" # None
elif _flag.skip == 1:
mem.skip = "S" # memscan skip (aka "Skip")
elif _flag.skip == 3:
mem.skip = "P" # 3 = mem&vfo ("Pskip")

# Channel names are encoded in 6x 6-bit groups spanning 5 bytes.
# Mask only the needed bits and then lookup character for each group
# Mem format: 0000AAAA AABBBBBB CCCCCCDD DDDDEEEE EEFFFFFF
mem.name = CODED_CHRS[((_mem.name[0] & 0x0f) << 2)
| ((_mem.name[1] & 0xc0) >> 6)] + \
CODED_CHRS[_mem.name[1] & 0x3f] + \
CODED_CHRS[(_mem.name[2] & 0xfc) >> 2] + \
CODED_CHRS[((_mem.name[2] & 0x03) << 4)
| ((_mem.name[3] & 0xf0) >> 4)] + \
CODED_CHRS[((_mem.name[3] & 0x0f) << 2)
| ((_mem.name[4] & 0xc0) >> 6)] + \
CODED_CHRS[(_mem.name[4] & 0x3f)]
mem.name = mem.name.rstrip(" ").strip("^")

return mem

def get_settings(self):
"""Translate the MEM_FORMAT structs into UI settings"""
# Based on the Icom IC-2730 driver
# Define mem struct write-back shortcuts
_sets = self._memobj.settings
_pses = self._memobj.pgmscanedge

basic = RadioSettingGroup("basic", "Basic Settings")
edges = RadioSettingGroup("edges", "Program Scan Edges")
common = RadioSettingGroup("common", "Common Settings")

group = RadioSettings(basic, edges, common)

# ----------------------
# BASIC SETTINGS
# ----------------------

# ----------------------
# PROGRAM SCAN EDGES
# ----------------------
for kx in range(0, 25):
stx = ""
for i in range(0, 6):
stx += chr(int(_pses[kx].name[i]))
stx = stx.rstrip()
rx = RadioSettingValueString(0, 6, stx)
rset = RadioSetting("pgmscanedge/%d.name" % kx,
f"Program Scan {kx} Name", rx)
# rset.set_apply_callback(myset_psnam, _pses, kx, "name", 6)
edges.append(rset)

# Freq (Hz) is 1/3 the raw value (expect this allows N x 8.333kHz)
flow = 1.0 * (_pses[kx].start3 * 65536 * 256 +
_pses[kx].start2 * 65536 +
_pses[kx].start1 * 256 +
_pses[kx].start0) / 3e6
fhigh = 1.0 * (_pses[kx].end3 * 65536 * 256 +
_pses[kx].end2 * 65536 +
_pses[kx].end1 * 256 +
_pses[kx].end0) / 3e6

if (flow > 0) and (flow >= fhigh):
flow, fhigh = fhigh, flow

rx = RadioSettingValueFloat(0.1, 1309.995, flow, 0.010, 6)
rset = RadioSetting("pgmscanedge/%d.lofreq" % kx,
f"-- Scan {kx} Low Limit", rx)
# rset.set_apply_callback(myset_frqflgs, _pses, kx, "loflags",
# "lofreq")
edges.append(rset)

rx = RadioSettingValueFloat(0.1, 1309.995, fhigh, 0.010, 6)
rset = RadioSetting("pgmscanedge/%d.hifreq" % kx,
f"-- Scan {kx} High Limit", rx)
# rset.set_apply_callback(myset_frqflgs, _pses, kx, "hiflags",
# "hifreq")
edges.append(rset)

# -------------------------------
# COMMON SETTINGS - Incomplete
# -------------------------------

# Antenna - AM
options = ["Ext", "Bar"]
rx = RadioSettingValueList(options, options[_sets.am_ant])
rset = RadioSetting("settings.am_ant", "AM Antenna", rx)
common.append(rset)

# Antenna - FM
options = ["Ext", "Ear"]
rx = RadioSettingValueList(options, options[_sets.fm_ant])
rset = RadioSetting("settings.fm_ant", "FM Antenna", rx)
common.append(rset)

# CIV Address
stx = str(_sets.civ_address)[2:] # Hex value
rx = RadioSettingValueString(1, 2, stx)
rset = RadioSetting("settings.civ_address", "CI-V Address (7E)", rx)
# rset.set_apply_callback(hex_val, _sets, "civ_address")
common.append(rset)

# CIV Baud:
rx = RadioSettingValueList(CIV_BAUD_RATES,
CIV_BAUD_RATES[_sets.civ_baud_rate])
rset = RadioSetting("settings.civ_baud_rate", "CI-V Baud Rate", rx)
common.append(rset)

# CIV - Transmit frequency/mode changes
rx = RadioSettingValueBoolean(bool(_sets.civ_transceive))
rset = RadioSetting("settings.civ_transceive", "CI-V Transceive", rx)
common.append(rset)

return group

def set_memory(self, mem):
_mem = self._memobj.memory[mem.number]
_flag = self._memobj.flags[mem.number]

_flag.hide_channel = mem.empty
if mem.empty:
return

# Channel Names - 6x chars each coded via lookup and bitmapped...
name_str = mem.name.strip("'").ljust(6)
_mem.name[0] = (CODED_CHRS.index(name_str[0]) & 0x3c) >> 2
_mem.name[1] = ((CODED_CHRS.index(name_str[0]) & 0x03) << 6) | \
(CODED_CHRS.index(name_str[1]) & 0x3f)
_mem.name[2] = (CODED_CHRS.index(name_str[2]) << 2) | \
((CODED_CHRS.index(name_str[3]) & 0x30) >> 4)
_mem.name[3] = ((CODED_CHRS.index(name_str[3]) & 0x0f) << 4) | \
((CODED_CHRS.index(name_str[4]) & 0x3c) >> 2)
_mem.name[4] = ((CODED_CHRS.index(name_str[4]) & 0x03) << 6) | \
(CODED_CHRS.index(name_str[5]) & 0x3f)

if mem.ctone in TONES:
_mem.ctone = TONES.index(mem.ctone)
if mem.tmode in TONE_MODES:
_mem.tmode = TONE_MODES.index(mem.tmode)

_mem.mode = MODES.index(mem.mode)

_mem.duplex = DUPLEX_DIRS.index(mem.duplex)

# Frequency: step size and count:
# - Some common multiples of 5k and 9k (i.e. 45k)
# are stored as 9k multiples. However if duplex is
# a 5k multiple (normally is) we must set 5k.
# - Logic needs more mapping for the common cases.
# - 10/15/20/... k are stored as multiples of 5k.
# - Step size is independent of TS.
if mem.freq % 9000 == 0 and mem.freq % 5000 != 0:
# 9k multiple but not 5k - use 9k
_mem.freq_flags = 60

_mem.freq0 = int(mem.freq / 9000) & 0x00ff
_mem.freq1 = (int(mem.freq / 9000) & 0xff00) >> 8
_mem.freq2 = (int(mem.freq / 9000) & 0x30000) >> 16

_mem.offset_l = (int(mem.offset/9000) & 0x00ff)
_mem.offset_h = (int(mem.offset/9000) & 0xff00) >> 8
elif mem.freq % 5000 == 0 and mem.freq % 9000 != 0:
# 5k multiple but not 9k - use 9k
_mem.freq_flags = 0

_mem.freq0 = int(mem.freq / 5000) & 0x00ff
_mem.freq1 = (int(mem.freq / 5000) & 0xff00) >> 8
_mem.freq2 = (int(mem.freq / 5000) & 0x30000) >> 16

_mem.offset_l = (int(mem.offset/5000) & 0x00ff)
_mem.offset_h = (int(mem.offset/5000) & 0xff00) >> 8
elif mem.freq % 5000 == 0 and mem.freq % 9000 == 0:
# 45k case
if mem.offset/9000 != 0:
# Can't be 9k, must be 5k for duplex/offset
_mem.freq_flags = 0

_mem.freq0 = int(mem.freq / 5000) & 0x00ff
_mem.freq1 = (int(mem.freq / 5000) & 0xff00) >> 8
_mem.freq2 = (int(mem.freq / 5000) & 0x30000) >> 16

_mem.offset_l = (int(mem.offset/5000) & 0x00ff)
_mem.offset_h = (int(mem.offset/5000) & 0xff00) >> 8
else:
# Go with 9k for now
_mem.freq_flags = 60

_mem.freq0 = int(mem.freq / 9000) & 0x00ff
_mem.freq1 = (int(mem.freq / 9000) & 0xff00) >> 8
_mem.freq2 = (int(mem.freq / 9000) & 0x30000) >> 16

_mem.offset_l = (int(mem.offset/9000) & 0x00ff)
_mem.offset_h = (int(mem.offset/9000) & 0xff00) >> 8
elif mem.freq % 6250 == 0:
_mem.freq_flags = 20

_mem.freq0 = int(mem.freq / 6250) & 0x00ff
_mem.freq1 = (int(mem.freq / 6250) & 0xff00) >> 8
_mem.freq2 = (int(mem.freq / 6250) & 0x30000) >> 16

_mem.offset_l = (int(mem.offset/6250) & 0x00ff)
_mem.offset_h = (int(mem.offset/6250) & 0xff00) >> 8
elif (mem.freq * 3) % 25000 == 0: # 8333.3333333
_mem.freq_flags = 40

_mem.freq0 = int(mem.freq / 8330) & 0x00ff
_mem.freq1 = (int(mem.freq / 8330) & 0xff00) >> 8
_mem.freq2 = (int(mem.freq / 8330) & 0x30000) >> 16

_mem.offset_l = (int(mem.offset/8330) & 0x00ff)
_mem.offset_h = (int(mem.offset/8330) & 0xff00) >> 8

else:
LOG.exception(f"Can't find multiplier for freq {mem.freq} Hz")

# Memory scan skip
if mem.skip == "":
_flag.skip = 0
elif mem.skip == "S":
_flag.skip = 1 # memscan skip (aka "Skip")
elif mem.skip == "P":
_flag.skip = 3 # mem&vfo ("Pskip")
(7-7/7)