Project

General

Profile

New Model #11632 » Tidradio_H3_NicFw.py

Gonzalo Torró, 11/10/2024 12:22 PM

 
# Copyright 2024 Gonzalo EA5JQP <ea5jqp@proton.me>
#
# 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, see <http://www.gnu.org/licenses/>.


import logging

# noinspection PyUnresolvedReferences
from chirp import chirp_common, directory, bitwise, memmap, errors, util
# noinspection PyUnresolvedReferences
from chirp.settings import InvalidValueError, RadioSetting, RadioSettingGroup, \
RadioSettingValueBoolean, RadioSettingValueList, \
RadioSettingValueInteger, RadioSettingValueString, \
RadioSettings, RadioSettingSubGroup, RadioSettingValueFloat

LOG = logging.getLogger(__name__)

# Here is where we define the memory map for the radio. Since
# We often just know small bits of it, we can use #seekto to skip
# around as needed.

AIRBAND = (108000000, 135999999)

MEM_FORMAT = """

#seekto 0x0000;
struct {
ul32 rxfreq; // byte[4] - RX Frequency in 10Hz units 32 bit unsigned little endian
ul32 txfreq; // byte[4] - TX Frequency in 10Hz units 32 bit unsigned little endian
ul16 rxtone; // byte[2] - RX Sub Tone CTCSS: 0.1Hz units, DCS: codeword|0x8000[|0x4000 for reverse tone] . 16 bit unsigned little endian
ul16 txtone; // byte[2] - TX Sub Tone (as rx sub tone)
u8 txpower; // byte[1] - TX Power - 8 bit unsigned
u16 group2:4, // bit[4] - Group membership. 0=No group, 1-15=group A-O
group1:4, // bit[4] - Group membership. 0=No group, 1-15=group A-O
group4:4, // bit[4] - Group membership. 0=No group, 1-15=group A-O
group3:4; // bit[4] - Group membership. 0=No group, 1-15=group A-O
u8 unused1:1, // bit[1] - Other bits are reserved
unused2:2, // bit[2] - Other bits are reserved
unused3:1, // bit[1] - Other bits are reserved
unused4:1, // bit[1] - Other bits are reserved
modulation:2, // bit[2] - Modulation - 0=Auto, 1=FM, 2=AM, 3=USB
bandwidth:1; // bit[1] - Bandwith - 0=Wide, 1=Narrow
u32 reserved; // byte[4] - Reserved
char name[12]; // byte[12] - ASCII channel name, unused characters should be null (0)
} memory[199];

struct {
u16 magic; // byte[2] = 0x9BCF (magic value)
u8 squelch; // byte[1] = squelch, 8 bit unsigned but only valid values are 0-9
ul16 step; // byte[2] = step, 16 bit unsigned little endian. 10Hz units
u8 micgain; // byte[1] = mic gain, 8 bit unsigned but only valid values are 0-31
u8 lcd; // byte[1] = LCD brightness, 8 bit unsigned, valid 0-28
u8 subtonedev; // byte[1] = Sub Tone deviation, 8 bit unsigned, valid 0-127
u8 keytones; // byte[1] = Key tones, bool
u8 opmode; // byte[1] = Operation mode, 0=VFO, 1=Channel, 2=Group
u8 channel; // byte[1] = current channel 0-199 the channel currently being used (0 and 1 are VFO-A and VFO-B, 2 is channel1, 3 is channel2 etc..)
u8 lastchannel; // byte[1] = Last channel 2-199 the last channel used in channel or group mode.
u8 group; // byte[1] = current group 1-15
u8 scanlinger; // byte[1] = scan linger (0A = 10)
ul16 rxfilter; // byte[2] = RX vhf/uhf filter transition frequency, 16 bit unsigned little endian 100kHz units
ul16 txfilter; // byte[2] = TX vhf/uhf filter transition frequency, 16 bit unsigned little endian 100kHz units
u16 unknown_1; // byte[2] = reserved?
u16 unknown_2; // byte[2] = reserved?
u16 unknown_3; // byte[2] = reserved?
u16 unknown_4; // byte[2] = reserved?
u16 unknown_5; // byte[2] = reserved?
u16 unknown_6; // byte[2] = reserved?
u16 unknown_7; // byte[2] = reserved?
} settings;
"""

CMD_DISABLE_RADIO = b'\x45' # w/ Ack
CMD_ENABLE_RADIO = b'\x46' # w/ Ack
CMD_READ_EEPROM = b'\x30' # w/ Ack
CMD_WRITE_EEPROM = b'\x31' # w/ Ack
CMD_RESET_RADIO = b'\x49' # wo/ Ack
CMD_END_REMOTE_SESSION = b'\x4B' # w/ Ack

MAGIC_SETTINGS = 0x9BCF

BLOCK_DATA_SIZE = 0x0020
INIT_ADDR_CHANNELS = 0x0040
INIT_ADDR_SETTINGS = 0x1900
BLOCK_CHANNEL = range(1,200)
BLOCK_SETTINGS = 200

CTCSS_TONES = [
67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4,
88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9,
114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2,
151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8,
177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5,
203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8,
250.3, 254.1,
]

# Basic Settings
GROUPS_LIST = ["None","A", "B", "C","D","E","F","G","H","I","J","K","L","M","N","O"]
MODULATION_LIST = ["Auto", "FM", "AM", "USB"]
BANDWIDTH_LIST = ["Wide", "Narrow"]
SQUELCH_LIST = ['Off' if x == 0 else f'{x}' for x in range(0, 10)]
OP_MODES = ["VFO", "Channel", "Group"]
MICGAIN_LIST = [f'{x}' for x in range(0, 32)]
LCDBRIGHT_LIST = [f'{x}' for x in range(0, 29)]
SUBTONEDEV_LIST = [f'{x}' for x in range(0, 128)]
SCANLINGER_LIST = [f'{x}' for x in range(10, 128)]



def _do_status(radio, block):
status = chirp_common.Status()
status.msg = "Cloning"
status.cur = block
status.max = BLOCK_CHANNEL[-1]
radio.status_fn(status)

def write_cmd(radio, cmd, check_ack=False):
serial = radio.pipe
serial.write(cmd)
serial.timeout = 0.5
if check_ack == True:
ack = serial.read(1)
if ack != cmd:
LOG.debug("[ERR] Unable to communicate with nicFW -- there was no valid ACK for {} command ({} received).".format(cmd,ack))

def _enter_programming_mode(radio):
write_cmd(radio, CMD_ENABLE_RADIO, check_ack=True)

def _exit_programming_mode(radio):
write_cmd(radio, CMD_DISABLE_RADIO, check_ack= True)

def _reset_radio(radio):
write_cmd(radio, CMD_RESET_RADIO, check_ack= False)

def calc_checksum(bytes):
checksum = 0

for b in bytes:
checksum += b

return (checksum % 256).to_bytes(1,'little')

def _read_block(radio, block):
serial = radio.pipe
serial.timeout = 0.5
serial.write(CMD_READ_EEPROM)
serial.write([block])
ack = serial.read(1)
data = serial.read(BLOCK_DATA_SIZE)
checksum_r = serial.read(1)
if checksum_r != calc_checksum(data):
LOG.debug("Received data checksum mismatch!")
# else:
# LOG.debug("Received checksum OK")
return data

def _write_block(radio, block, data):
checksum = calc_checksum(data)

serial = radio.pipe
serial.timeout = 0.5

serial.write(CMD_WRITE_EEPROM)
serial.write([block])
serial.write(data)
serial.write(checksum)
LOG.debug("Bytes to write:{} checksum:{}".format(data,checksum))
ack = serial.read(1)

if ack != CMD_WRITE_EEPROM:
LOG.debug("Received {} expected {} checksum mismatch while writing!".format(ack,CMD_WRITE_EEPROM))
def do_download(radio):
"""This is your download channels function"""
_enter_programming_mode(radio)

data = b""
data = bytearray()

for i in range(1,BLOCK_SETTINGS+1):
block = _read_block(radio, i)
data.extend(block)
LOG.info("Block: %i",i)
LOG.info(util.hexprint(bytes(block)))
_do_status(radio, i)
_exit_programming_mode(radio)

return memmap.MemoryMapBytes(bytes(data))


def _do_upload(radio):
"""This is your download settings function"""
_enter_programming_mode(radio)

LOG.debug("Uploading...")

for i in range(1,BLOCK_SETTINGS+1):
addr = (i-1) * BLOCK_DATA_SIZE
data = radio.get_mmap()[addr:addr+BLOCK_DATA_SIZE]
_write_block(radio, i, data)
_do_status(radio, addr)
LOG.debug("writemem sent data offset=0x%4.4x len=0x%4.4x:\n%s" %
(addr, len(data), util.hexprint(data)))

_exit_programming_mode(radio)
_reset_radio(radio)



@directory.register
class TH3NicFw(chirp_common.CloneModeRadio):
VENDOR = "TIDRADIO" # Replace this with your vendor
MODEL = "H3 NicFW" # Replace this with your model
BAUD_RATE = 38400 # Replace this with your baud rate

VALID_BANDS = [(10000000, 136000000), # RX only (Air Band)
(136000000, 174000000), # TX/RX (VHF)
(174000000, 240000000), # TX/RX
(240000000, 320000000), # TX/RX
(320000000, 400000000), # TX/RX
(400000000, 480000000), # TX/RX (UHF)
(480000000, 1300000000)] # TX/RX


# Return information about this radio's features, including
# how many memories it has, what bands it supports, etc
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_tuning_step = False
rf.has_rx_dtcs = False
rf.has_ctone = False
rf.has_comment = False

rf.memory_bounds = (1, 198)
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_bands = self.VALID_BANDS
rf.valid_modes = MODULATION_LIST
rf.valid_duplexes = ["", "-", "+", "split", "off"]
rf.valid_skips = ["N"]
rf.valid_name_length = 12

return rf
# Do a download of the radio from the serial port
def sync_in(self):
try:
self._mmap = do_download(self)
self.process_mmap()
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)
# Do an upload of the radio to the serial port
def sync_out(self):
try:
_do_upload(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)

# Convert the raw byte array into a memory object structure
def process_mmap(self):
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
# Return a raw representation of the memory object, which
# is very helpful for development
def get_raw_memory(self, number):
return repr(self._memobj.memory[number])

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

def _get_nam(self, number):
return self._memobj.memory.name[number]
# Extract a high-level memory object from the low-level memory map
# This is called to populate a memory in the UI
def get_memory(self, number):
_mem = self._get_mem(number)
# Create a high-level memory object to return to the UI
mem = chirp_common.Memory()
mem.number = number # Set the memory number

# LOG.info("Doing channel %i ",number)
# We'll consider any blank (i.e. 0 MHz frequency) to be empty
if _mem.get_raw()[0] == 0xff:
mem.empty = True
# LOG.info("Channel %i is empty!",number)
return mem
# Convert your low-level frequency to Hertz
mem.freq = int(_mem.rxfreq) * 10

# Channel name
for char in _mem.name:
if "\x00" in str(char) or "\xFF" in str(char):
char = ""
mem.name += str(char)
mem.name = mem.name.rstrip()

mem.skip = ""

# tmode
# lin2 = int(_mem.rxtone)
# LOG.info()
# lin = int(_mem.txtone)
# txtone = self._decode_tone(lin)

# Offset
if int(_mem.rxfreq) == int(_mem.txfreq):
mem.duplex = ""
mem.offset = 0
else:
mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10


mem.mode = MODULATION_LIST[int(_mem.modulation)]

mem.tmode = ""
rxtone = (_mem.rxtone)
# txtone = _decode_tone(_mem.txtone)

# chirp_common.split_tone_decode(mem, txtone, rxtone)
mem.extra = RadioSettingGroup("Extra", "extra")


rs = RadioSettingValueInteger(0, 255, _mem.txpower)
rset = RadioSetting("txpower", "TX Power", rs)
mem.extra.append(rset)

rs = RadioSettingValueList(GROUPS_LIST, current_index = _mem.group1)
rset = RadioSetting("group1", "Grp Slot 1", rs)
mem.extra.append(rset)
rs = RadioSettingValueList(GROUPS_LIST, current_index = _mem.group2)
rset = RadioSetting("group2", "Grp Slot 2", rs)
mem.extra.append(rset)

rs = RadioSettingValueList(GROUPS_LIST, current_index =_mem.group3)
rset = RadioSetting("group3", "Grp Slot 3", rs)
mem.extra.append(rset)
rs = RadioSettingValueList(GROUPS_LIST, current_index = _mem.group4)
rset = RadioSetting("group4", "Grp Slot 4", rs)
mem.extra.append(rset)

bandwidth = "Narrow" if _mem.bandwidth else "Wide"
rs = RadioSettingValueList(BANDWIDTH_LIST, bandwidth)
rset = RadioSetting("bandwidth", "Bandwidth", rs)
mem.extra.append(rset)

msgs = self.validate_memory(mem)

if msgs != []:
LOG.info("Following warnings were generating while validating channels:")
LOG.info(msgs)

return mem
# Store details about a high-level memory to the memory map
# This is called when a user edits a memory in the UI
def set_memory(self, mem):
# Get a low-level memory object mapped to the image
_mem = self._get_mem(mem.number)

# if empty memory
if mem.empty:
_mem.set_raw("\xFF" * 22 + "\x20" * 10)
return
if mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = mem.freq / 10

_mem.name = mem.name.rstrip('\xFF').ljust(12, '\x20')

#extra
for element in mem.extra:
sname = element.get_name()
svalue = element.value.get_value()

if sname == 'txpower':
_mem.txpower = element.value

if sname == 'bandwidth':
_mem.bandwidth = 1 if element.value=="Narrow" else 0

if sname == "modulation":
_mem.modulation = MODULATION_LIST.index(svalue)

if sname == "group1":
_mem.group1 = GROUPS_LIST.index(svalue)

if sname == "group2":
_mem.group2 = GROUPS_LIST.index(svalue)

if sname == "group3":
_mem.group3 = GROUPS_LIST.index(svalue)

if sname == "group4":
_mem.group4 = GROUPS_LIST.index(svalue)


return mem

def set_settings(self, settings):
_settings = self._memobj.settings

for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
# Basic Settings

# Squelch
if element.get_name() == "squelch":
_settings.squelch = SQUELCH_LIST.index(str(element.value))

# Step
if element.get_name() == "step":
LOG.info("Before")
LOG.info(_settings.step)
LOG.info(element.value * 100)
_settings.step = element.value * 100
LOG.info(_settings.step)


# Mic Gain
if element.get_name() == "micgain":
_settings.micgain = MICGAIN_LIST.index(str(element.value))

# LCD Brightness
if element.get_name() == "lcd":
_settings.lcd = LCDBRIGHT_LIST.index(str(element.value))
# Key tone
if element.get_name() == "keytones":
_settings.keytones = element.value and 1 or 0

# Operation Mode
if element.get_name() == "opmode":
_settings.opmode = OP_MODES.index(str(element.value))

# Scan Linger
if element.get_name() == "scanlinger":
_settings.scanlinger = int(element.value)

# RX VHF/UHF Filter Transition Frequency
if element.get_name() == "rxfilter":
_settings.rxfilter = int(element.value * 10)
# TX VHF/UHF Filter Transition Frequency
if element.get_name() == "txfilter":
_settings.txfilter = int(element.value * 10)




def get_settings(self):
_mem = self._memobj
basic = RadioSettingGroup("basic", "Basic Settings")
bandplan = RadioSettingGroup("bandplan", "Band Plan")

group = RadioSettings(basic)

rs = RadioSettingValueList(SQUELCH_LIST, current_index = _mem.settings.squelch)
rset = RadioSetting("squelch", "Squelch Level", rs)
basic.append(rset)

step = float(_mem.settings.step) / 100
rs = RadioSettingValueFloat(0.01, 500, step, resolution=0.01, precision=2)
rset = RadioSetting("step", "Step Size", rs)
basic.append(rset)

rs = RadioSettingValueList(MICGAIN_LIST, current_index = _mem.settings.micgain)
rset = RadioSetting("micgain", "Mic Gain", rs)
basic.append(rset)

rs = RadioSettingValueList(LCDBRIGHT_LIST, current_index = _mem.settings.lcd)
rset = RadioSetting("lcd", "LCD Brightness", rs)
basic.append(rset)

rs = RadioSettingValueList(SUBTONEDEV_LIST, current_index = _mem.settings.subtonedev)
rset = RadioSetting("subtonedev", "Sub Tone Deviation", rs)
basic.append(rset)

rs = RadioSettingValueBoolean(bool(_mem.settings.keytones))
rset = RadioSetting("keytones", "Key Tones", rs)
basic.append(rset)

rs = RadioSettingValueList(OP_MODES, current_index = _mem.settings.opmode)
rset = RadioSetting("opmode", "Operation Mode", rs)
basic.append(rset)

rs = RadioSettingValueInteger(10,127,int(_mem.settings.scanlinger))
rset = RadioSetting("scanlinger", "Scan Tail [10 - 127]", rs)
basic.append(rset)

rxfilter = float(_mem.settings.rxfilter) / 10
rs = RadioSettingValueFloat(150, 400, rxfilter, resolution= 0.1, precision=1)
rset = RadioSetting("rxfilter", "RX VHF/UHF Filter Transition Frequency", rs)
basic.append(rset)

txfilter = float(_mem.settings.txfilter) / 10
rs = RadioSettingValueFloat(150, 400, txfilter, resolution= 0.1, precision=1)
rset = RadioSetting("txfilter", "TX VHF/UHF Filter Transition Frequency", rs)
basic.append(rset)

return group
(3-3/3)