diff --git a/Icom_IC-R6.img b/Icom_IC-R6.img new file mode 100644 index 00000000..4c82337d Binary files /dev/null and b/Icom_IC-R6.img differ diff --git a/chirp/drivers/icr6.py b/chirp/drivers/icr6.py new file mode 100644 index 00000000..bf54ca32 --- /dev/null +++ b/chirp/drivers/icr6.py @@ -0,0 +1,528 @@ +"""Icom IC-R6 Driver""" +# Copyright 2024 John Bradshaw Mi0SYN +# +# 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 . + + +# 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 +from chirp.drivers.icf import warp_byte_size + +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 # TO + 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("Unknown freq multiplier %d", _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 name - packed into 6x 6-bit groups with 4 bits padding + mem.name = ''.join(CODED_CHRS[x] for x in warp_byte_size(_mem.name, + obw=6, ibw=8, + iskip=4)) + 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 - 6 chars Coded via lookup and packed into 6-bits each + coded_chars = ''.join(chr(CODED_CHRS.index(x)) + for x in mem.name.strip("'").ljust(6)) + _mem.name = list(warp_byte_size(coded_chars, 8, 6, opad=4)) + + 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 use 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 5k + _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("Can't find multiplier for freq %d Hz", mem.freq) + + # 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") diff --git a/tests/Python3_Driver_Testing.md b/tests/Python3_Driver_Testing.md index 5f7dd1ec..1819e466 100644 --- a/tests/Python3_Driver_Testing.md +++ b/tests/Python3_Driver_Testing.md @@ -126,6 +126,7 @@ | Icom_IC-E90 | [Probably works](https://github.com/kk7ds/chirp/blob/py3/chirp/drivers/icf.py) | 12-Dec-2022 | Yes | 0.04% | | Icom_IC-P7 | [Probably works](https://github.com/kk7ds/chirp/blob/py3/chirp/drivers/icf.py) | 12-Dec-2022 | Yes | 0.01% | | Icom_IC-Q7A | [@KC9HI](https://github.com/KC9HI) | 20-Nov-2022 | Yes | 0.01% | +| Icom_IC-R6 | Probably works(https://github.com/kk7ds/chirp/blob/py3/chirp/drivers/icf.py) | 10-Aug-2024 | Yes | 0.01% | | Icom_IC-T70 | [Probably works](https://github.com/kk7ds/chirp/blob/py3/chirp/drivers/icf.py) | 12-Dec-2022 | Yes | 0.03% | | Icom_IC-T7H | [Reported working](https://chirp.danplanet.com/issues/10752) | 30-Jul-2023 | Yes | 0.02% | | Icom_IC-T8A | [Probably works](https://github.com/kk7ds/chirp/blob/py3/chirp/drivers/icf.py) | 12-Dec-2022 | Yes | 0.01% | diff --git a/tests/images/Icom_IC-R6.img b/tests/images/Icom_IC-R6.img new file mode 100644 index 00000000..59ce2c50 Binary files /dev/null and b/tests/images/Icom_IC-R6.img differ