[chirp_devel] [PATCH] [TH-UV88] New Model: TYT TH-UV88
Attached: - factory image to test against - patch file in case the "hg email tip" version does not apply
Jim KC9HI
On Mon, Nov 16, 2020 at 4:08 PM Jim Unroe via chirp_devel < chirp_devel@intrepid.danplanet.com> wrote:
# HG changeset patch # User Jim Unroe # Date 1605557938 18000 # Mon Nov 16 15:18:58 2020 -0500 # Node ID 3fd7ed9a0de01f7897286c9ac274237a45cc7831 # Parent d5e496f563fdfc9ea89dea5f119357235b82db6f [TH-UV88] New Model: TYT TH-UV88
This patch adds support for the TYT TH-UV88
Initial radio protocol decode, channels and memory layout by James Berry , Summer 2020
Related to #7817
diff -r d5e496f563fd -r 3fd7ed9a0de0 chirp/drivers/th_uv88.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/drivers/th_uv88.py Mon Nov 16 15:18:58 2020 -0500 @@ -0,0 +1,917 @@ +# Version 1.0 for TYT-UV88 +# Initial radio protocol decode, channels and memory layout +# by James Berry , Summer 2020 +# Additional configuration and help, Jim Unroe +# +# 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/ https://cgafehc.r.bh.d.sendibt3.com/tr/cl/QpgoPSH5r0NV4hvIlBzrvEcq4oc9bR7ML-hY3nUq1zcKpuB6PoNuxRO5sk1uyvjY6xNti_2BBKjXrXX0srodnvuyJCJoyWI2-Yvxp23eOYPzyGJ9YGcERkgxudTdeOKMmfxyCn3t2CYacNXoU-fUTH2l-Z72EkLeHxcFk6bRAjewSghXhQJGJrU87xwkmenKqH8swQaX2VY .
+import time +import struct +import logging +import re +import math +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \
- RadioSettingValueBoolean, RadioSettingValueList, \
- RadioSettingValueString, RadioSettingValueInteger, \
- RadioSettingValueFloat, RadioSettings, InvalidValueError
+from textwrap import dedent
+LOG = logging.getLogger(__name__)
+MEM_FORMAT = """ +struct chns {
- ul32 rxfreq;
- ul32 txfreq;
- ul16 scramble:4
- rxtone:12; //decode:12
- ul16 decodeDSCI:1
- encodeDSCI:1
- unk1:1
- unk2:1
- txtone:12; //encode:12
- u8 power:2
- wide:2
- b_lock:2
- unk3:2;
- u8 unk4:3
- signal:2
- displayName:1
- unk5:2;
- u8 unk6:2
- pttid:2
- step:4; // not required
- u8 name[6];
+};
+struct vfo {
- ul32 rxfreq;
- ul32 txfreq; // displayed as an offset
- ul16 scramble:4
- rxtone:12; //decode:12
- ul16 decodeDSCI:1
- encodeDSCI:1
- unk1:1
- unk2:1
- txtone:12; //encode:12
- u8 power:2
- wide:2
- b_lock:2
- unk3:2;
- u8 unk4:3
- signal:2
- displayName:1
- unk5:2;
- u8 unk6:2
- pttid:2
- step:4;
- u8 name[6];
+};
+struct chname {
- u8 extra_name[10];
+};
+#seekto 0x0000; +struct chns chan_mem[199];
+#seekto 0x1960; +struct chname chan_name[199];
+#seekto 0x1180; +struct {
- u8 bitmap[26]; // one bit for each channel marked in use
+} chan_avail;
+#seekto 0x11A0; +struct {
- u8 bitmap[26]; // one bit for each channel skipped
+} chan_skip;
+#seekto 0x1140; +struct {
- u8 autoKeylock:1, // 0x1140 [18] *OFF, On
- unk_bit6_5:2, //
- vfomrmode:1, // *VFO, MR
- unk_bit3_0:4; //
- u8 unk_1141; // 0x1141
- u8 unk_1142; // 0x1142
- u8 unk_bit7_3:5, //
- ab:1, // * A, B
- unk_bit1_0:2; //
+} workmodesettings;
+#seekto 0x1160; +struct {
- u8 introScreen1[12]; // 0x1160 *Intro Screen Line 1(truncated to 12 alpha
- // text characters)
- u8 offFreqVoltage : 3, // 0x116C unknown referred to in code but not on
- // screen
- unk_bit4 : 1, //
- sqlLevel : 4; // [05] *OFF, 1-9
- u8 beep : 1 // 0x116D [09] *OFF, On
- callKind : 2, // code says 1750,2100,1000,1450 as options
- // not on screen
- introScreen: 2, // [20] *OFF, Voltage, Char String
- unkstr2: 2, //
- txChSelect : 1; // [02] *Last CH, Main CH
- u8 autoPowOff : 3, // 0x116E not on screen? OFF, 30Min, 1HR, 2HR
- unk : 1, //
- tot : 4; // [11] *OFF, 30 Second, 60 Second, 90 Second,
- // ... , 270 Second
- u8 unk_bit7:1, // 0x116F
- roger:1, // [14] *OFF, On
- dailDef:1, // Unknown - 'Volume, Frequency'
- language:1, // ?Chinese, English
- unk_bit3:1, //
- endToneElim:1, // *OFF, Frequency
- unkCheckBox1:1, //
- unkCheckBox2:1; //
- u8 scanResumeTime : 2, // 0x1170 2S, 5S, 10S, 15S (not on screen)
- disMode : 2, // [33] *Frequency, Channel, Name
- scanType: 2, // [17] *To, Co, Se
- ledMode: 2; // [07] *Off, On, Auto
- u8 unky; // 0x1171
- u8 str6; // 0x1172 Has flags to do with logging - factory
- // enabled (bits 16,64,128)
- u8 unk; // 0x1173
- u8 swAudio : 1, // 0x1174 [19] *OFF, On
- radioMoni : 1, // [34]*OFF, On
- keylock : 1, // *OFF, Auto
- dualWait : 1, // [06] *OFF, On
- unk_bit3 : 1, //
- light : 3; // [08] *1, 2, 3, 4, 5, 6, 7
- u8 voxSw : 1, // 0x1175 [13] *OFF, On
- voxDelay: 4, // *0.5S, 1.0S, 1.5S, 2.0S, 2.5S, 3.0S, 3.5S,
- // 4.0S, 4.5S, 5.0S
- voxLevel : 3; // [03] *1, 2, 3, 4, 5, 6, 7
- u8 str9 : 4, // 0x1176
- saveMode : 2, // [16] *OFF, 1:1, 1:2, 1:4
- keyMode : 2; // [32] *ALL, PTT, KEY, Key & Side Key
- u8 unk2; // 0x1177
- u8 unk3; // 0x1178
- u8 unk4; // 0x1179
- u8 name2[6]; // 0x117A unused
+} basicsettings;
+#seekto 0x1940; +struct {
- u8 name1[16]; // Intro Screen Line 1 (16 alpha text characters)
- u8 name2[16]; // Intro Screen Line 2 (16 alpha text characters)
+} openradioname;
+"""
+MEM_SIZE = 0x22A0 +BLOCK_SIZE = 0x20 +STIMEOUT = 2 +BAUDRATE = 57600
+# Channel power: 3 levels +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
- chirp_common.PowerLevel("Mid", watts=2.50),
- chirp_common.PowerLevel("Low", watts=0.50)]
+SCRAMBLE_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8"] +B_LOCK_LIST = ["OFF", "Sub", "Carrier"] +OPTSIG_LIST = ["OFF", "DTMF", "2TONE", "5TONE"] +PTTID_LIST = ["Off", "BOT", "EOT", "Both"] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0] +LIST_STEPS = [str(x) for x in STEPS]
+def _clean_buffer(radio):
- radio.pipe.timeout = 0.005
- junk = radio.pipe.read(256)
- radio.pipe.timeout = STIMEOUT
- if junk:
- LOG.debug("Got %i bytes of junk before starting" % len(junk))
+def _rawrecv(radio, amount):
- """Raw read from the radio device"""
- data = ""
- try:
- data = radio.pipe.read(amount)
- except Exception:
- _exit_program_mode(radio)
- msg = "Generic error reading data from radio; check your cable."
- raise errors.RadioError(msg)
- if len(data) != amount:
- _exit_program_mode(radio)
- msg = "Error reading from radio: not the amount of data we want."
- raise errors.RadioError(msg)
- return data
+def _rawsend(radio, data):
- """Raw send to the radio device"""
- try:
- radio.pipe.write(data)
- except Exception:
- raise errors.RadioError("Error sending data to radio")
+def _make_read_frame(addr, length):
- frame = "\xFE\xFE\xEE\xEF\xEB"
- """Pack the info in the header format"""
- frame += struct.pack(">ih", addr, length)
- frame += "\xFD"
- # Return the data
- return frame
+def _make_write_frame(addr, length, data=""):
- frame = "\xFE\xFE\xEE\xEF\xE4"
- """Pack the info in the header format"""
- output = struct.pack(">ih", addr, length)
- # Add the data if set
- if len(data) != 0:
- output += data
- frame += output
- frame += _calculate_checksum(output)
- frame += "\xFD"
- # Return the data
- return frame
+def _calculate_checksum(data):
- num = 0
- for x in range(0, len(data)):
- num = (num + ord(data[x])) % 256
- if num == 0:
- return chr(0)
- return chr(256 - num)
+def _recv(radio, addr, length):
- """Get data from the radio """
- data = _rawrecv(radio, length)
- # DEBUG
- LOG.info("Response:")
- LOG.debug(util.hexprint(data))
- return data
+def _do_ident(radio):
- """Put the radio in PROGRAM mode & identify it"""
- radio.pipe.baudrate = BAUDRATE
- radio.pipe.parity = "N"
- radio.pipe.timeout = STIMEOUT
- # Flush input buffer
- _clean_buffer(radio)
- # Ident radio
- magic = "\xFE\xFE\xEE\xEF\xE0\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 36)
- if not ack.startswith("\xFE\xFE\xEF\xEE\xE1\x55\x56\x38\x38"
- ) or not ack.endswith("\xFD"):
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond as expected (A)")
- return True
+def _exit_program_mode(radio):
- # This may be the last part of a read
- magic = "\xFE\xFE\xEE\xEF\xE5\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 7)
- if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond as expected (B)")
+def _download(radio):
- """Get the memory map"""
- # Put radio in program mode and identify it
- _do_ident(radio)
- # Enter read mode
- magic = "\xFE\xFE\xEE\xEF\xE2\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 7)
- if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond to enter read mode")
- # UI progress
- status = chirp_common.Status()
- status.cur = 0
- status.max = MEM_SIZE / BLOCK_SIZE
- status.msg = "Cloning from radio..."
- radio.status_fn(status)
- data = ""
- for addr in range(0, MEM_SIZE, BLOCK_SIZE):
- frame = _make_read_frame(addr, BLOCK_SIZE)
- # DEBUG
- LOG.debug("Frame=" + util.hexprint(frame))
- # Sending the read request
- _rawsend(radio, frame)
- # Now we read data
- d = _recv(radio, addr, BLOCK_SIZE + 13)
- LOG.debug("Response Data= " + util.hexprint(d))
- if not d.startswith("\xFE\xFE\xEF\xEE\xE4"):
- LOG.warning("Incorrect start")
- if not d.endswith("\xFD"):
- LOG.warning("Incorrect end")
- # could validate the block data
- # Aggregate the data
- data += d[11:-2]
- # UI Update
- status.cur = addr / BLOCK_SIZE
- status.msg = "Cloning from radio..."
- radio.status_fn(status)
- _exit_program_mode(radio)
- return data
+def _upload(radio):
- """Upload procedure"""
- # Put radio in program mode and identify it
- _do_ident(radio)
- magic = "\xFE\xFE\xEE\xEF\xE3\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 7)
- if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond to enter write mode")
- # UI progress
- status = chirp_common.Status()
- status.cur = 0
- status.max = MEM_SIZE / BLOCK_SIZE
- status.msg = "Cloning to radio..."
- radio.status_fn(status)
- # The fun starts here
- for addr in range(0, MEM_SIZE, BLOCK_SIZE):
- # Official programmer skips writing these memory locations
- if addr >= 0x1680 and addr < 0x1940:
- continue
- # Sending the data
- data = radio.get_mmap()[addr:addr + BLOCK_SIZE]
- frame = _make_write_frame(addr, BLOCK_SIZE, data)
- LOG.warning("Frame:%s:" % util.hexprint(frame))
- _rawsend(radio, frame)
- ack = _rawrecv(radio, 7)
- LOG.debug("Response Data= " + util.hexprint(ack))
- if not ack.startswith("\xFE\xFE\xEF\xEE\xE6\x00\xFD"):
- LOG.warning("Unexpected response")
- _exit_program_mode(radio)
- msg = "Bad ack writing block 0x%04x" % addr
- raise errors.RadioError(msg)
- # UI Update
- status.cur = addr / BLOCK_SIZE
- status.msg = "Cloning to radio..."
- radio.status_fn(status)
- _exit_program_mode(radio)
+def _do_map(chn, sclr, mary):
- """Set or Clear the chn (1-128) bit in mary[] word array map"""
- # chn is 1-based channel, sclr:1 = set, 0= = clear, 2= return state
- # mary[] is u8 array, but the map is by nibbles
- ndx = int(math.floor((chn - 1) / 8))
- bv = (chn - 1) % 8
- msk = 1 << bv
- mapbit = sclr
- if sclr == 1: # Set the bit
- mary[ndx] = mary[ndx] | msk
- elif sclr == 0: # clear
- mary[ndx] = mary[ndx] & (~ msk) # ~ is complement
- else: # return current bit state
- mapbit = 0
- if (mary[ndx] & msk) > 0:
- mapbit = 1
- return mapbit
+@directory.register +class THUV88Radio(chirp_common.CloneModeRadio):
- """TYT UV88 Radio"""
- VENDOR = "TYT"
- MODEL = "TH-UV88"
- MODES = ['WFM', 'FM', 'NFM']
- TONES = chirp_common.TONES
- DTCS_CODES = chirp_common.DTCS_CODES
- NAME_LENGTH = 10
- DTMF_CHARS = list("0123456789ABCD*#")
- # 136-174, 400-480
- VALID_BANDS = [(136000000, 174000000), (400000000, 480000000)]
- # Valid chars on the LCD
- VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
- "`!"#$%&'()*+,-./:;<=>?@[]^_"
- @classmethod
- def get_prompts(cls):
- rp = chirp_common.RadioPrompts()
- rp.info = \
- ('TYT UV-88\n')
- rp.pre_download = _(dedent("""\
- This is an early stage beta driver
- """))
- rp.pre_upload = _(dedent("""\
- This is an early stage beta driver - upload at your own risk
- """))
- return rp
- def get_features(self):
- rf = chirp_common.RadioFeatures()
- rf.has_settings = True
- rf.has_bank = False
- rf.has_comment = False
- rf.has_tuning_step = False # Not as chan feature
- rf.valid_tuning_steps = STEPS
- rf.can_odd_split = False
- rf.has_name = True
- rf.has_offset = True
- rf.has_mode = True
- rf.has_dtcs = True
- rf.has_rx_dtcs = True
- rf.has_dtcs_polarity = True
- rf.has_ctone = True
- rf.has_cross = True
- rf.has_sub_devices = False
- rf.valid_name_length = self.NAME_LENGTH
- rf.valid_modes = self.MODES
- rf.valid_characters = self.VALID_CHARS
- rf.valid_duplexes = ["-", "+", "off", ""]
- rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
- rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS",
- "Tone->DTCS", "DTCS->Tone", "->Tone",
- "DTCS->DTCS"]
- rf.valid_skips = []
- rf.valid_power_levels = POWER_LEVELS
- rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES # this is just to
- # get it working, not sure this is right
- rf.valid_bands = self.VALID_BANDS
- rf.memory_bounds = (1, 199)
- rf.valid_skips = ["", "S"]
- return rf
- def sync_in(self):
- """Download from radio"""
- try:
- data = _download(self)
- except errors.RadioError:
- # Pass through any real errors we raise
- raise
- except Exception:
- # If anything unexpected happens, make sure we raise
- # a RadioError and log the problem
- LOG.exception('Unexpected error during download')
- raise errors.RadioError('Unexpected error communicating '
- 'with the radio')
- self._mmap = memmap.MemoryMap(data)
- self.process_mmap()
- def sync_out(self):
- """Upload to radio"""
- try:
- _upload(self)
- except Exception:
- # If anything unexpected happens, make sure we raise
- # a RadioError and log the problem
- LOG.exception('Unexpected error during upload')
- raise errors.RadioError('Unexpected error communicating '
- 'with the radio')
- def process_mmap(self):
- """Process the mem map into the mem object"""
- self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
- def get_raw_memory(self, number):
- return repr(self._memobj.memory[number - 1])
- def set_memory(self, memory):
- """A value in a UI column for chan 'number' has been modified."""
- # update all raw channel memory values (_mem) from UI (mem)
- _mem = self._memobj.chan_mem[memory.number - 1]
- _name = self._memobj.chan_name[memory.number - 1]
- if memory.empty:
- _do_map(memory.number, 0, self._memobj.chan_avail.bitmap)
- return
- _do_map(memory.number, 1, self._memobj.chan_avail.bitmap)
- if memory.skip == "":
- _do_map(memory.number, 1, self._memobj.chan_skip.bitmap)
- else:
- _do_map(memory.number, 0, self._memobj.chan_skip.bitmap)
- return self._set_memory(memory, _mem, _name)
- def get_memory(self, number):
- # radio first channel is 1, mem map is base 0
- _mem = self._memobj.chan_mem[number - 1]
- _name = self._memobj.chan_name[number - 1]
- mem = chirp_common.Memory()
- mem.number = number
- # Determine if channel is empty
- if _do_map(number, 2, self._memobj.chan_avail.bitmap) == 0:
- mem.empty = True
- return mem
- if _do_map(mem.number, 2, self._memobj.chan_skip.bitmap) > 0:
- mem.skip = ""
- else:
- mem.skip = "S"
- return self._get_memory(mem, _mem, _name)
- def _get_memory(self, mem, _mem, _name):
- """Convert raw channel memory data into UI columns"""
- mem.extra = RadioSettingGroup("extra", "Extra")
- mem.empty = False
- # This function process both 'normal' and Freq up/down' entries
- mem.freq = int(_mem.rxfreq) * 10
- if _mem.txfreq == 0xFFFFFFFF:
- # TX freq not set
- mem.duplex = "off"
- mem.offset = 0
- elif 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.name = ""
- for i in range(6): # 0 - 6
- mem.name += chr(_mem.name[i])
- for i in range(10):
- mem.name += chr(_name.extra_name[i])
- mem.name = mem.name.rstrip() # remove trailing spaces
- # ########## TONE ##########
- if _mem.txtone > 2600:
- # All off
- txmode = ""
- elif _mem.txtone > 511:
- txmode = "Tone"
- mem.rtone = int(_mem.txtone) / 10.0
- else:
- # DTSC
- txmode = "DTCS"
- mem.dtcs = int(format(int(_mem.txtone), 'o'))
- if _mem.rxtone > 2600:
- rxmode = ""
- elif _mem.rxtone > 511:
- rxmode = "Tone"
- mem.ctone = int(_mem.rxtone) / 10.0
- else:
- rxmode = "DTCS"
- mem.rx_dtcs = int(format(int(_mem.rxtone), 'o'))
- mem.dtcs_polarity = ("N", "R")[_mem.encodeDSCI] + (
- "N", "R")[_mem.decodeDSCI]
- mem.tmode = ""
- if txmode == "Tone" and not rxmode:
- mem.tmode = "Tone"
- elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
- mem.tmode = "TSQL"
- elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
- mem.tmode = "DTCS"
- elif rxmode or txmode:
- mem.tmode = "Cross"
- mem.cross_mode = "%s->%s" % (txmode, rxmode)
- # ########## TONE ##########
- mem.mode = self.MODES[_mem.wide]
- mem.power = POWER_LEVELS[int(_mem.power)]
- b_lock = RadioSetting("b_lock", "B_Lock",
- RadioSettingValueList(B_LOCK_LIST,
- B_LOCK_LIST[_mem.b_lock]))
- mem.extra.append(b_lock)
- b_lock = RadioSetting("step", "Step",
- RadioSettingValueList(LIST_STEPS,
- LIST_STEPS[_mem.step]))
- mem.extra.append(b_lock)
- scramble_value = _mem.scramble
- if scramble_value >= 8: # Looks like OFF is 0x0f ** CONFIRM
- scramble_value = 0
- scramble = RadioSetting("scramble", "Scramble",
- RadioSettingValueList(SCRAMBLE_LIST,
- SCRAMBLE_LIST[
- scramble_value]))
- mem.extra.append(scramble)
- optsig = RadioSetting("signal", "Optional signaling",
- RadioSettingValueList(
- OPTSIG_LIST,
- OPTSIG_LIST[_mem.signal]))
- mem.extra.append(optsig)
- rs = RadioSetting("pttid", "PTT ID",
- RadioSettingValueList(PTTID_LIST,
- PTTID_LIST[_mem.pttid]))
- mem.extra.append(rs)
- return mem
- def _set_memory(self, mem, _mem, _name):
- # """Convert UI column data (mem) into MEM_FORMAT memory (_mem)."""
- _mem.rxfreq = mem.freq / 10
- if mem.duplex == "off":
- _mem.txfreq = 0xFFFFFFFF
- elif mem.duplex == "+":
- _mem.txfreq = (mem.freq + mem.offset) / 10
- elif mem.duplex == "-":
- _mem.txfreq = (mem.freq - mem.offset) / 10
- else:
- _mem.txfreq = _mem.rxfreq
- out_name = mem.name.ljust(16)
- for i in range(6): # 0 - 6
- _mem.name[i] = ord(out_name[i])
- for i in range(10):
- _name.extra_name[i] = ord(out_name[i+6])
- if mem.name != "":
- _mem.displayName = 1 # Name only displayed if this is set on
- else:
- _mem.displayName = 0
- rxmode = ""
- txmode = ""
- if mem.tmode == "Tone":
- txmode = "Tone"
- elif mem.tmode == "TSQL":
- rxmode = "Tone"
- txmode = "TSQL"
- elif mem.tmode == "DTCS":
- rxmode = "DTCSSQL"
- txmode = "DTCS"
- elif mem.tmode == "Cross":
- txmode, rxmode = mem.cross_mode.split("->", 1)
- if mem.dtcs_polarity[1] == "N":
- _mem.decodeDSCI = 0
- else:
- _mem.decodeDSCI = 1
- if rxmode == "":
- _mem.rxtone = 0xFFF
- elif rxmode == "Tone":
- _mem.rxtone = int(float(mem.ctone) * 10)
- elif rxmode == "DTCSSQL":
- _mem.rxtone = int(str(mem.dtcs), 8)
- elif rxmode == "DTCS":
- _mem.rxtone = int(str(mem.rx_dtcs), 8)
- if mem.dtcs_polarity[0] == "N":
- _mem.encodeDSCI = 0
- else:
- _mem.encodeDSCI = 1
- if txmode == "":
- _mem.txtone = 0xFFF
- elif txmode == "Tone":
- _mem.txtone = int(float(mem.rtone) * 10)
- elif txmode == "TSQL":
- _mem.txtone = int(float(mem.ctone) * 10)
- elif txmode == "DTCS":
- _mem.txtone = int(str(mem.dtcs), 8)
- _mem.wide = self.MODES.index(mem.mode)
- _mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
- for element in mem.extra:
- setattr(_mem, element.get_name(), element.value)
- return
- def get_settings(self):
- """Translate the MEM_FORMAT structs into setstuf in the UI"""
- _settings = self._memobj.basicsettings
- _workmode = self._memobj.workmodesettings
- basic = RadioSettingGroup("basic", "Basic Settings")
- group = RadioSettings(basic)
- # Menu 02 - TX Channel Select
- options = ["Last Channel", "Main Channel"]
- rx = RadioSettingValueList(options, options[_settings.txChSelect])
- rset = RadioSetting("basicsettings.txChSelect",
- "Priority Transmit", rx)
- basic.append(rset)
- # Menu 03 - VOX Level
- rx = RadioSettingValueInteger(1, 7, _settings.voxLevel - 1)
- rset = RadioSetting("basicsettings.voxLevel", "Vox Level", rx)
- basic.append(rset)
- # Menu 05 - Squelch Level
- options = ["OFF"] + ["%s" % x for x in range(1, 10)]
- rx = RadioSettingValueList(options, options[_settings.sqlLevel])
- rset = RadioSetting("basicsettings.sqlLevel", "Squelch Level", rx)
- basic.append(rset)
- # Menu 06 - Dual Wait
- rx = RadioSettingValueBoolean(_settings.dualWait)
- rset = RadioSetting("basicsettings.dualWait", "Dual Wait/Standby", rx)
- basic.append(rset)
- # Menu 07 - LED Mode
- options = ["Off", "On", "Auto"]
- rx = RadioSettingValueList(options, options[_settings.ledMode])
- rset = RadioSetting("basicsettings.ledMode", "LED Display Mode", rx)
- basic.append(rset)
- # Menu 08 - Light
- options = ["%s" % x for x in range(1, 8)]
- rx = RadioSettingValueList(options, options[_settings.light])
- rset = RadioSetting("basicsettings.light",
- "Background Light Color", rx)
- basic.append(rset)
- # Menu 09 - Beep
- rx = RadioSettingValueBoolean(_settings.beep)
- rset = RadioSetting("basicsettings.beep", "Keypad Beep", rx)
- basic.append(rset)
- # Menu 11 - TOT
- options = ["Off"] + ["%s seconds" % x for x in range(30, 300, 30)]
- rx = RadioSettingValueList(options, options[_settings.tot])
- rset = RadioSetting("basicsettings.tot",
- "Transmission Time-out Timer", rx)
- basic.append(rset)
- # Menu 13 - VOX Switch
- rx = RadioSettingValueBoolean(_settings.voxSw)
- rset = RadioSetting("basicsettings.voxSw", "Vox Switch", rx)
- basic.append(rset)
- # Menu 14 - Roger
- rx = RadioSettingValueBoolean(_settings.roger)
- rset = RadioSetting("basicsettings.roger", "Roger Beep", rx)
- basic.append(rset)
- # Menu 16 - Save Mode
- options = ["Off", "1:1", "1:2", "1:4"]
- rx = RadioSettingValueList(options, options[_settings.saveMode])
- rset = RadioSetting("basicsettings.saveMode", "Battery Save Mode", rx)
- basic.append(rset)
- # Menu 33 - Display Mode
- options = ['Frequency', 'Channel', 'Name']
- rx = RadioSettingValueList(options, options[_settings.disMode])
- rset = RadioSetting("basicsettings.disMode", "LED Display Mode", rx)
- basic.append(rset)
- advanced = RadioSettingGroup("advanced", "Advanced Settings")
- group.append(advanced)
- # software only
- options = ['0.5S', '1.0S', '1.5S', '2.0S', '2.5S', '3.0S', '3.5S',
- '4.0S', '4.5S', '5.0S']
- rx = RadioSettingValueList(options, options[_settings.voxDelay])
- rset = RadioSetting("basicsettings.voxDelay", "VOX Delay", rx)
- advanced.append(rset)
- # software only
- name = ""
- for i in range(16): # 0 - 16
- name += chr(self._memobj.openradioname.name1[i])
- name = name.rstrip() # remove trailing spaces
- rx = RadioSettingValueString(0, 16, name)
- rset = RadioSetting("openradioname.name1", "Intro Line 1", rx)
- advanced.append(rset)
- # software only
- name = ""
- for i in range(16): # 0 - 16
- name += chr(self._memobj.openradioname.name2[i])
- name = name.rstrip() # remove trailing spaces
- rx = RadioSettingValueString(0, 16, name)
- rset = RadioSetting("openradioname.name2", "Intro Line 2", rx)
- advanced.append(rset)
- workmode = RadioSettingGroup("workmode", "Work Mode Settings")
- group.append(workmode)
- # Toggle with [#] key
- options = ["Frequency", "Channel"]
- rx = RadioSettingValueList(options, options[_workmode.vfomrmode])
- rset = RadioSetting("workmodesettings.vfomrmode", "VFO/MR Mode", rx)
- workmode.append(rset)
- # Toggle with [A/B] key
- options = ["A", "B"]
- rx = RadioSettingValueList(options, options[_workmode.ab])
- rset = RadioSetting("workmodesettings.ab", "A/B Select", rx)
- workmode.append(rset)
- return group # END get_settings()
- def set_settings(self, settings):
- return
- _settings = self._memobj.settings
- _mem = self._memobj
- for element in settings:
- if not isinstance(element, RadioSetting):
- self.set_settings(element)
- continue
- else:
- try:
- name = element.get_name()
- if "." in name:
- bits = name.split(".")
- obj = self._memobj
- for bit in bits[:-1]:
- if "/" in bit:
- bit, index = bit.split("/", 1)
- index = int(index)
- obj = getattr(obj, bit)[index]
- else:
- obj = getattr(obj, bit)
- setting = bits[-1]
- else:
- obj = _settings
- setting = element.get_name()
- if element.has_apply_callback():
- LOG.debug("Using apply callback")
- element.run_apply_callback()
- elif setting == "voxLevel":
- setattr(obj, setting, int(element.value) + 1)
- elif element.value.get_mutable():
- LOG.debug("Setting %s = %s" % (setting, element.value))
- setattr(obj, setting, element.value)
- except Exception, e:
- LOG.debug(element.get_name())
- raise
diff -r d5e496f563fd -r 3fd7ed9a0de0 tools/cpep8.manifest --- a/tools/cpep8.manifest Fri Nov 13 08:07:04 2020 -0500 +++ b/tools/cpep8.manifest Mon Nov 16 15:18:58 2020 -0500 @@ -77,6 +77,7 @@ ./chirp/drivers/th_uv3r.py ./chirp/drivers/th_uv3r25.py ./chirp/drivers/th_uv8000.py +./chirp/drivers/th_uv88.py ./chirp/drivers/th_uvf8d.py ./chirp/drivers/thd72.py ./chirp/drivers/thuv1f.py _______________________________________________ chirp_devel mailing list chirp_devel@intrepid.danplanet.com http://intrepid.danplanet.com/mailman/listinfo/chirp_devel Developer docs: http://chirp.danplanet.com/projects/chirp/wiki/Developers
Dan,
Please do not apply this patch. A couple of bugs were discovered, so I will soon be submitting a patch to replace this one.
Thanks, Jim
On Mon, Nov 16, 2020 at 4:08 PM Jim Unroe via chirp_devel < chirp_devel@intrepid.danplanet.com> wrote:
# HG changeset patch # User Jim Unroe # Date 1605557938 18000 # Mon Nov 16 15:18:58 2020 -0500 # Node ID 3fd7ed9a0de01f7897286c9ac274237a45cc7831 # Parent d5e496f563fdfc9ea89dea5f119357235b82db6f [TH-UV88] New Model: TYT TH-UV88
This patch adds support for the TYT TH-UV88
Initial radio protocol decode, channels and memory layout by James Berry , Summer 2020
Related to #7817
diff -r d5e496f563fd -r 3fd7ed9a0de0 chirp/drivers/th_uv88.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/drivers/th_uv88.py Mon Nov 16 15:18:58 2020 -0500 @@ -0,0 +1,917 @@ +# Version 1.0 for TYT-UV88 +# Initial radio protocol decode, channels and memory layout +# by James Berry , Summer 2020 +# Additional configuration and help, Jim Unroe +# +# 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/ https://cgafehc.r.bh.d.sendibt3.com/tr/cl/QpgoPSH5r0NV4hvIlBzrvEcq4oc9bR7ML-hY3nUq1zcKpuB6PoNuxRO5sk1uyvjY6xNti_2BBKjXrXX0srodnvuyJCJoyWI2-Yvxp23eOYPzyGJ9YGcERkgxudTdeOKMmfxyCn3t2CYacNXoU-fUTH2l-Z72EkLeHxcFk6bRAjewSghXhQJGJrU87xwkmenKqH8swQaX2VY .
+import time +import struct +import logging +import re +import math +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \
- RadioSettingValueBoolean, RadioSettingValueList, \
- RadioSettingValueString, RadioSettingValueInteger, \
- RadioSettingValueFloat, RadioSettings, InvalidValueError
+from textwrap import dedent
+LOG = logging.getLogger(__name__)
+MEM_FORMAT = """ +struct chns {
- ul32 rxfreq;
- ul32 txfreq;
- ul16 scramble:4
- rxtone:12; //decode:12
- ul16 decodeDSCI:1
- encodeDSCI:1
- unk1:1
- unk2:1
- txtone:12; //encode:12
- u8 power:2
- wide:2
- b_lock:2
- unk3:2;
- u8 unk4:3
- signal:2
- displayName:1
- unk5:2;
- u8 unk6:2
- pttid:2
- step:4; // not required
- u8 name[6];
+};
+struct vfo {
- ul32 rxfreq;
- ul32 txfreq; // displayed as an offset
- ul16 scramble:4
- rxtone:12; //decode:12
- ul16 decodeDSCI:1
- encodeDSCI:1
- unk1:1
- unk2:1
- txtone:12; //encode:12
- u8 power:2
- wide:2
- b_lock:2
- unk3:2;
- u8 unk4:3
- signal:2
- displayName:1
- unk5:2;
- u8 unk6:2
- pttid:2
- step:4;
- u8 name[6];
+};
+struct chname {
- u8 extra_name[10];
+};
+#seekto 0x0000; +struct chns chan_mem[199];
+#seekto 0x1960; +struct chname chan_name[199];
+#seekto 0x1180; +struct {
- u8 bitmap[26]; // one bit for each channel marked in use
+} chan_avail;
+#seekto 0x11A0; +struct {
- u8 bitmap[26]; // one bit for each channel skipped
+} chan_skip;
+#seekto 0x1140; +struct {
- u8 autoKeylock:1, // 0x1140 [18] *OFF, On
- unk_bit6_5:2, //
- vfomrmode:1, // *VFO, MR
- unk_bit3_0:4; //
- u8 unk_1141; // 0x1141
- u8 unk_1142; // 0x1142
- u8 unk_bit7_3:5, //
- ab:1, // * A, B
- unk_bit1_0:2; //
+} workmodesettings;
+#seekto 0x1160; +struct {
- u8 introScreen1[12]; // 0x1160 *Intro Screen Line 1(truncated to 12 alpha
- // text characters)
- u8 offFreqVoltage : 3, // 0x116C unknown referred to in code but not on
- // screen
- unk_bit4 : 1, //
- sqlLevel : 4; // [05] *OFF, 1-9
- u8 beep : 1 // 0x116D [09] *OFF, On
- callKind : 2, // code says 1750,2100,1000,1450 as options
- // not on screen
- introScreen: 2, // [20] *OFF, Voltage, Char String
- unkstr2: 2, //
- txChSelect : 1; // [02] *Last CH, Main CH
- u8 autoPowOff : 3, // 0x116E not on screen? OFF, 30Min, 1HR, 2HR
- unk : 1, //
- tot : 4; // [11] *OFF, 30 Second, 60 Second, 90 Second,
- // ... , 270 Second
- u8 unk_bit7:1, // 0x116F
- roger:1, // [14] *OFF, On
- dailDef:1, // Unknown - 'Volume, Frequency'
- language:1, // ?Chinese, English
- unk_bit3:1, //
- endToneElim:1, // *OFF, Frequency
- unkCheckBox1:1, //
- unkCheckBox2:1; //
- u8 scanResumeTime : 2, // 0x1170 2S, 5S, 10S, 15S (not on screen)
- disMode : 2, // [33] *Frequency, Channel, Name
- scanType: 2, // [17] *To, Co, Se
- ledMode: 2; // [07] *Off, On, Auto
- u8 unky; // 0x1171
- u8 str6; // 0x1172 Has flags to do with logging - factory
- // enabled (bits 16,64,128)
- u8 unk; // 0x1173
- u8 swAudio : 1, // 0x1174 [19] *OFF, On
- radioMoni : 1, // [34]*OFF, On
- keylock : 1, // *OFF, Auto
- dualWait : 1, // [06] *OFF, On
- unk_bit3 : 1, //
- light : 3; // [08] *1, 2, 3, 4, 5, 6, 7
- u8 voxSw : 1, // 0x1175 [13] *OFF, On
- voxDelay: 4, // *0.5S, 1.0S, 1.5S, 2.0S, 2.5S, 3.0S, 3.5S,
- // 4.0S, 4.5S, 5.0S
- voxLevel : 3; // [03] *1, 2, 3, 4, 5, 6, 7
- u8 str9 : 4, // 0x1176
- saveMode : 2, // [16] *OFF, 1:1, 1:2, 1:4
- keyMode : 2; // [32] *ALL, PTT, KEY, Key & Side Key
- u8 unk2; // 0x1177
- u8 unk3; // 0x1178
- u8 unk4; // 0x1179
- u8 name2[6]; // 0x117A unused
+} basicsettings;
+#seekto 0x1940; +struct {
- u8 name1[16]; // Intro Screen Line 1 (16 alpha text characters)
- u8 name2[16]; // Intro Screen Line 2 (16 alpha text characters)
+} openradioname;
+"""
+MEM_SIZE = 0x22A0 +BLOCK_SIZE = 0x20 +STIMEOUT = 2 +BAUDRATE = 57600
+# Channel power: 3 levels +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
- chirp_common.PowerLevel("Mid", watts=2.50),
- chirp_common.PowerLevel("Low", watts=0.50)]
+SCRAMBLE_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8"] +B_LOCK_LIST = ["OFF", "Sub", "Carrier"] +OPTSIG_LIST = ["OFF", "DTMF", "2TONE", "5TONE"] +PTTID_LIST = ["Off", "BOT", "EOT", "Both"] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0] +LIST_STEPS = [str(x) for x in STEPS]
+def _clean_buffer(radio):
- radio.pipe.timeout = 0.005
- junk = radio.pipe.read(256)
- radio.pipe.timeout = STIMEOUT
- if junk:
- LOG.debug("Got %i bytes of junk before starting" % len(junk))
+def _rawrecv(radio, amount):
- """Raw read from the radio device"""
- data = ""
- try:
- data = radio.pipe.read(amount)
- except Exception:
- _exit_program_mode(radio)
- msg = "Generic error reading data from radio; check your cable."
- raise errors.RadioError(msg)
- if len(data) != amount:
- _exit_program_mode(radio)
- msg = "Error reading from radio: not the amount of data we want."
- raise errors.RadioError(msg)
- return data
+def _rawsend(radio, data):
- """Raw send to the radio device"""
- try:
- radio.pipe.write(data)
- except Exception:
- raise errors.RadioError("Error sending data to radio")
+def _make_read_frame(addr, length):
- frame = "\xFE\xFE\xEE\xEF\xEB"
- """Pack the info in the header format"""
- frame += struct.pack(">ih", addr, length)
- frame += "\xFD"
- # Return the data
- return frame
+def _make_write_frame(addr, length, data=""):
- frame = "\xFE\xFE\xEE\xEF\xE4"
- """Pack the info in the header format"""
- output = struct.pack(">ih", addr, length)
- # Add the data if set
- if len(data) != 0:
- output += data
- frame += output
- frame += _calculate_checksum(output)
- frame += "\xFD"
- # Return the data
- return frame
+def _calculate_checksum(data):
- num = 0
- for x in range(0, len(data)):
- num = (num + ord(data[x])) % 256
- if num == 0:
- return chr(0)
- return chr(256 - num)
+def _recv(radio, addr, length):
- """Get data from the radio """
- data = _rawrecv(radio, length)
- # DEBUG
- LOG.info("Response:")
- LOG.debug(util.hexprint(data))
- return data
+def _do_ident(radio):
- """Put the radio in PROGRAM mode & identify it"""
- radio.pipe.baudrate = BAUDRATE
- radio.pipe.parity = "N"
- radio.pipe.timeout = STIMEOUT
- # Flush input buffer
- _clean_buffer(radio)
- # Ident radio
- magic = "\xFE\xFE\xEE\xEF\xE0\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 36)
- if not ack.startswith("\xFE\xFE\xEF\xEE\xE1\x55\x56\x38\x38"
- ) or not ack.endswith("\xFD"):
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond as expected (A)")
- return True
+def _exit_program_mode(radio):
- # This may be the last part of a read
- magic = "\xFE\xFE\xEE\xEF\xE5\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 7)
- if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond as expected (B)")
+def _download(radio):
- """Get the memory map"""
- # Put radio in program mode and identify it
- _do_ident(radio)
- # Enter read mode
- magic = "\xFE\xFE\xEE\xEF\xE2\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 7)
- if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond to enter read mode")
- # UI progress
- status = chirp_common.Status()
- status.cur = 0
- status.max = MEM_SIZE / BLOCK_SIZE
- status.msg = "Cloning from radio..."
- radio.status_fn(status)
- data = ""
- for addr in range(0, MEM_SIZE, BLOCK_SIZE):
- frame = _make_read_frame(addr, BLOCK_SIZE)
- # DEBUG
- LOG.debug("Frame=" + util.hexprint(frame))
- # Sending the read request
- _rawsend(radio, frame)
- # Now we read data
- d = _recv(radio, addr, BLOCK_SIZE + 13)
- LOG.debug("Response Data= " + util.hexprint(d))
- if not d.startswith("\xFE\xFE\xEF\xEE\xE4"):
- LOG.warning("Incorrect start")
- if not d.endswith("\xFD"):
- LOG.warning("Incorrect end")
- # could validate the block data
- # Aggregate the data
- data += d[11:-2]
- # UI Update
- status.cur = addr / BLOCK_SIZE
- status.msg = "Cloning from radio..."
- radio.status_fn(status)
- _exit_program_mode(radio)
- return data
+def _upload(radio):
- """Upload procedure"""
- # Put radio in program mode and identify it
- _do_ident(radio)
- magic = "\xFE\xFE\xEE\xEF\xE3\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 7)
- if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond to enter write mode")
- # UI progress
- status = chirp_common.Status()
- status.cur = 0
- status.max = MEM_SIZE / BLOCK_SIZE
- status.msg = "Cloning to radio..."
- radio.status_fn(status)
- # The fun starts here
- for addr in range(0, MEM_SIZE, BLOCK_SIZE):
- # Official programmer skips writing these memory locations
- if addr >= 0x1680 and addr < 0x1940:
- continue
- # Sending the data
- data = radio.get_mmap()[addr:addr + BLOCK_SIZE]
- frame = _make_write_frame(addr, BLOCK_SIZE, data)
- LOG.warning("Frame:%s:" % util.hexprint(frame))
- _rawsend(radio, frame)
- ack = _rawrecv(radio, 7)
- LOG.debug("Response Data= " + util.hexprint(ack))
- if not ack.startswith("\xFE\xFE\xEF\xEE\xE6\x00\xFD"):
- LOG.warning("Unexpected response")
- _exit_program_mode(radio)
- msg = "Bad ack writing block 0x%04x" % addr
- raise errors.RadioError(msg)
- # UI Update
- status.cur = addr / BLOCK_SIZE
- status.msg = "Cloning to radio..."
- radio.status_fn(status)
- _exit_program_mode(radio)
+def _do_map(chn, sclr, mary):
- """Set or Clear the chn (1-128) bit in mary[] word array map"""
- # chn is 1-based channel, sclr:1 = set, 0= = clear, 2= return state
- # mary[] is u8 array, but the map is by nibbles
- ndx = int(math.floor((chn - 1) / 8))
- bv = (chn - 1) % 8
- msk = 1 << bv
- mapbit = sclr
- if sclr == 1: # Set the bit
- mary[ndx] = mary[ndx] | msk
- elif sclr == 0: # clear
- mary[ndx] = mary[ndx] & (~ msk) # ~ is complement
- else: # return current bit state
- mapbit = 0
- if (mary[ndx] & msk) > 0:
- mapbit = 1
- return mapbit
+@directory.register +class THUV88Radio(chirp_common.CloneModeRadio):
- """TYT UV88 Radio"""
- VENDOR = "TYT"
- MODEL = "TH-UV88"
- MODES = ['WFM', 'FM', 'NFM']
- TONES = chirp_common.TONES
- DTCS_CODES = chirp_common.DTCS_CODES
- NAME_LENGTH = 10
- DTMF_CHARS = list("0123456789ABCD*#")
- # 136-174, 400-480
- VALID_BANDS = [(136000000, 174000000), (400000000, 480000000)]
- # Valid chars on the LCD
- VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
- "`!"#$%&'()*+,-./:;<=>?@[]^_"
- @classmethod
- def get_prompts(cls):
- rp = chirp_common.RadioPrompts()
- rp.info = \
- ('TYT UV-88\n')
- rp.pre_download = _(dedent("""\
- This is an early stage beta driver
- """))
- rp.pre_upload = _(dedent("""\
- This is an early stage beta driver - upload at your own risk
- """))
- return rp
- def get_features(self):
- rf = chirp_common.RadioFeatures()
- rf.has_settings = True
- rf.has_bank = False
- rf.has_comment = False
- rf.has_tuning_step = False # Not as chan feature
- rf.valid_tuning_steps = STEPS
- rf.can_odd_split = False
- rf.has_name = True
- rf.has_offset = True
- rf.has_mode = True
- rf.has_dtcs = True
- rf.has_rx_dtcs = True
- rf.has_dtcs_polarity = True
- rf.has_ctone = True
- rf.has_cross = True
- rf.has_sub_devices = False
- rf.valid_name_length = self.NAME_LENGTH
- rf.valid_modes = self.MODES
- rf.valid_characters = self.VALID_CHARS
- rf.valid_duplexes = ["-", "+", "off", ""]
- rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
- rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS",
- "Tone->DTCS", "DTCS->Tone", "->Tone",
- "DTCS->DTCS"]
- rf.valid_skips = []
- rf.valid_power_levels = POWER_LEVELS
- rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES # this is just to
- # get it working, not sure this is right
- rf.valid_bands = self.VALID_BANDS
- rf.memory_bounds = (1, 199)
- rf.valid_skips = ["", "S"]
- return rf
- def sync_in(self):
- """Download from radio"""
- try:
- data = _download(self)
- except errors.RadioError:
- # Pass through any real errors we raise
- raise
- except Exception:
- # If anything unexpected happens, make sure we raise
- # a RadioError and log the problem
- LOG.exception('Unexpected error during download')
- raise errors.RadioError('Unexpected error communicating '
- 'with the radio')
- self._mmap = memmap.MemoryMap(data)
- self.process_mmap()
- def sync_out(self):
- """Upload to radio"""
- try:
- _upload(self)
- except Exception:
- # If anything unexpected happens, make sure we raise
- # a RadioError and log the problem
- LOG.exception('Unexpected error during upload')
- raise errors.RadioError('Unexpected error communicating '
- 'with the radio')
- def process_mmap(self):
- """Process the mem map into the mem object"""
- self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
- def get_raw_memory(self, number):
- return repr(self._memobj.memory[number - 1])
- def set_memory(self, memory):
- """A value in a UI column for chan 'number' has been modified."""
- # update all raw channel memory values (_mem) from UI (mem)
- _mem = self._memobj.chan_mem[memory.number - 1]
- _name = self._memobj.chan_name[memory.number - 1]
- if memory.empty:
- _do_map(memory.number, 0, self._memobj.chan_avail.bitmap)
- return
- _do_map(memory.number, 1, self._memobj.chan_avail.bitmap)
- if memory.skip == "":
- _do_map(memory.number, 1, self._memobj.chan_skip.bitmap)
- else:
- _do_map(memory.number, 0, self._memobj.chan_skip.bitmap)
- return self._set_memory(memory, _mem, _name)
- def get_memory(self, number):
- # radio first channel is 1, mem map is base 0
- _mem = self._memobj.chan_mem[number - 1]
- _name = self._memobj.chan_name[number - 1]
- mem = chirp_common.Memory()
- mem.number = number
- # Determine if channel is empty
- if _do_map(number, 2, self._memobj.chan_avail.bitmap) == 0:
- mem.empty = True
- return mem
- if _do_map(mem.number, 2, self._memobj.chan_skip.bitmap) > 0:
- mem.skip = ""
- else:
- mem.skip = "S"
- return self._get_memory(mem, _mem, _name)
- def _get_memory(self, mem, _mem, _name):
- """Convert raw channel memory data into UI columns"""
- mem.extra = RadioSettingGroup("extra", "Extra")
- mem.empty = False
- # This function process both 'normal' and Freq up/down' entries
- mem.freq = int(_mem.rxfreq) * 10
- if _mem.txfreq == 0xFFFFFFFF:
- # TX freq not set
- mem.duplex = "off"
- mem.offset = 0
- elif 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.name = ""
- for i in range(6): # 0 - 6
- mem.name += chr(_mem.name[i])
- for i in range(10):
- mem.name += chr(_name.extra_name[i])
- mem.name = mem.name.rstrip() # remove trailing spaces
- # ########## TONE ##########
- if _mem.txtone > 2600:
- # All off
- txmode = ""
- elif _mem.txtone > 511:
- txmode = "Tone"
- mem.rtone = int(_mem.txtone) / 10.0
- else:
- # DTSC
- txmode = "DTCS"
- mem.dtcs = int(format(int(_mem.txtone), 'o'))
- if _mem.rxtone > 2600:
- rxmode = ""
- elif _mem.rxtone > 511:
- rxmode = "Tone"
- mem.ctone = int(_mem.rxtone) / 10.0
- else:
- rxmode = "DTCS"
- mem.rx_dtcs = int(format(int(_mem.rxtone), 'o'))
- mem.dtcs_polarity = ("N", "R")[_mem.encodeDSCI] + (
- "N", "R")[_mem.decodeDSCI]
- mem.tmode = ""
- if txmode == "Tone" and not rxmode:
- mem.tmode = "Tone"
- elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
- mem.tmode = "TSQL"
- elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
- mem.tmode = "DTCS"
- elif rxmode or txmode:
- mem.tmode = "Cross"
- mem.cross_mode = "%s->%s" % (txmode, rxmode)
- # ########## TONE ##########
- mem.mode = self.MODES[_mem.wide]
- mem.power = POWER_LEVELS[int(_mem.power)]
- b_lock = RadioSetting("b_lock", "B_Lock",
- RadioSettingValueList(B_LOCK_LIST,
- B_LOCK_LIST[_mem.b_lock]))
- mem.extra.append(b_lock)
- b_lock = RadioSetting("step", "Step",
- RadioSettingValueList(LIST_STEPS,
- LIST_STEPS[_mem.step]))
- mem.extra.append(b_lock)
- scramble_value = _mem.scramble
- if scramble_value >= 8: # Looks like OFF is 0x0f ** CONFIRM
- scramble_value = 0
- scramble = RadioSetting("scramble", "Scramble",
- RadioSettingValueList(SCRAMBLE_LIST,
- SCRAMBLE_LIST[
- scramble_value]))
- mem.extra.append(scramble)
- optsig = RadioSetting("signal", "Optional signaling",
- RadioSettingValueList(
- OPTSIG_LIST,
- OPTSIG_LIST[_mem.signal]))
- mem.extra.append(optsig)
- rs = RadioSetting("pttid", "PTT ID",
- RadioSettingValueList(PTTID_LIST,
- PTTID_LIST[_mem.pttid]))
- mem.extra.append(rs)
- return mem
- def _set_memory(self, mem, _mem, _name):
- # """Convert UI column data (mem) into MEM_FORMAT memory (_mem)."""
- _mem.rxfreq = mem.freq / 10
- if mem.duplex == "off":
- _mem.txfreq = 0xFFFFFFFF
- elif mem.duplex == "+":
- _mem.txfreq = (mem.freq + mem.offset) / 10
- elif mem.duplex == "-":
- _mem.txfreq = (mem.freq - mem.offset) / 10
- else:
- _mem.txfreq = _mem.rxfreq
- out_name = mem.name.ljust(16)
- for i in range(6): # 0 - 6
- _mem.name[i] = ord(out_name[i])
- for i in range(10):
- _name.extra_name[i] = ord(out_name[i+6])
- if mem.name != "":
- _mem.displayName = 1 # Name only displayed if this is set on
- else:
- _mem.displayName = 0
- rxmode = ""
- txmode = ""
- if mem.tmode == "Tone":
- txmode = "Tone"
- elif mem.tmode == "TSQL":
- rxmode = "Tone"
- txmode = "TSQL"
- elif mem.tmode == "DTCS":
- rxmode = "DTCSSQL"
- txmode = "DTCS"
- elif mem.tmode == "Cross":
- txmode, rxmode = mem.cross_mode.split("->", 1)
- if mem.dtcs_polarity[1] == "N":
- _mem.decodeDSCI = 0
- else:
- _mem.decodeDSCI = 1
- if rxmode == "":
- _mem.rxtone = 0xFFF
- elif rxmode == "Tone":
- _mem.rxtone = int(float(mem.ctone) * 10)
- elif rxmode == "DTCSSQL":
- _mem.rxtone = int(str(mem.dtcs), 8)
- elif rxmode == "DTCS":
- _mem.rxtone = int(str(mem.rx_dtcs), 8)
- if mem.dtcs_polarity[0] == "N":
- _mem.encodeDSCI = 0
- else:
- _mem.encodeDSCI = 1
- if txmode == "":
- _mem.txtone = 0xFFF
- elif txmode == "Tone":
- _mem.txtone = int(float(mem.rtone) * 10)
- elif txmode == "TSQL":
- _mem.txtone = int(float(mem.ctone) * 10)
- elif txmode == "DTCS":
- _mem.txtone = int(str(mem.dtcs), 8)
- _mem.wide = self.MODES.index(mem.mode)
- _mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
- for element in mem.extra:
- setattr(_mem, element.get_name(), element.value)
- return
- def get_settings(self):
- """Translate the MEM_FORMAT structs into setstuf in the UI"""
- _settings = self._memobj.basicsettings
- _workmode = self._memobj.workmodesettings
- basic = RadioSettingGroup("basic", "Basic Settings")
- group = RadioSettings(basic)
- # Menu 02 - TX Channel Select
- options = ["Last Channel", "Main Channel"]
- rx = RadioSettingValueList(options, options[_settings.txChSelect])
- rset = RadioSetting("basicsettings.txChSelect",
- "Priority Transmit", rx)
- basic.append(rset)
- # Menu 03 - VOX Level
- rx = RadioSettingValueInteger(1, 7, _settings.voxLevel - 1)
- rset = RadioSetting("basicsettings.voxLevel", "Vox Level", rx)
- basic.append(rset)
- # Menu 05 - Squelch Level
- options = ["OFF"] + ["%s" % x for x in range(1, 10)]
- rx = RadioSettingValueList(options, options[_settings.sqlLevel])
- rset = RadioSetting("basicsettings.sqlLevel", "Squelch Level", rx)
- basic.append(rset)
- # Menu 06 - Dual Wait
- rx = RadioSettingValueBoolean(_settings.dualWait)
- rset = RadioSetting("basicsettings.dualWait", "Dual Wait/Standby", rx)
- basic.append(rset)
- # Menu 07 - LED Mode
- options = ["Off", "On", "Auto"]
- rx = RadioSettingValueList(options, options[_settings.ledMode])
- rset = RadioSetting("basicsettings.ledMode", "LED Display Mode", rx)
- basic.append(rset)
- # Menu 08 - Light
- options = ["%s" % x for x in range(1, 8)]
- rx = RadioSettingValueList(options, options[_settings.light])
- rset = RadioSetting("basicsettings.light",
- "Background Light Color", rx)
- basic.append(rset)
- # Menu 09 - Beep
- rx = RadioSettingValueBoolean(_settings.beep)
- rset = RadioSetting("basicsettings.beep", "Keypad Beep", rx)
- basic.append(rset)
- # Menu 11 - TOT
- options = ["Off"] + ["%s seconds" % x for x in range(30, 300, 30)]
- rx = RadioSettingValueList(options, options[_settings.tot])
- rset = RadioSetting("basicsettings.tot",
- "Transmission Time-out Timer", rx)
- basic.append(rset)
- # Menu 13 - VOX Switch
- rx = RadioSettingValueBoolean(_settings.voxSw)
- rset = RadioSetting("basicsettings.voxSw", "Vox Switch", rx)
- basic.append(rset)
- # Menu 14 - Roger
- rx = RadioSettingValueBoolean(_settings.roger)
- rset = RadioSetting("basicsettings.roger", "Roger Beep", rx)
- basic.append(rset)
- # Menu 16 - Save Mode
- options = ["Off", "1:1", "1:2", "1:4"]
- rx = RadioSettingValueList(options, options[_settings.saveMode])
- rset = RadioSetting("basicsettings.saveMode", "Battery Save Mode", rx)
- basic.append(rset)
- # Menu 33 - Display Mode
- options = ['Frequency', 'Channel', 'Name']
- rx = RadioSettingValueList(options, options[_settings.disMode])
- rset = RadioSetting("basicsettings.disMode", "LED Display Mode", rx)
- basic.append(rset)
- advanced = RadioSettingGroup("advanced", "Advanced Settings")
- group.append(advanced)
- # software only
- options = ['0.5S', '1.0S', '1.5S', '2.0S', '2.5S', '3.0S', '3.5S',
- '4.0S', '4.5S', '5.0S']
- rx = RadioSettingValueList(options, options[_settings.voxDelay])
- rset = RadioSetting("basicsettings.voxDelay", "VOX Delay", rx)
- advanced.append(rset)
- # software only
- name = ""
- for i in range(16): # 0 - 16
- name += chr(self._memobj.openradioname.name1[i])
- name = name.rstrip() # remove trailing spaces
- rx = RadioSettingValueString(0, 16, name)
- rset = RadioSetting("openradioname.name1", "Intro Line 1", rx)
- advanced.append(rset)
- # software only
- name = ""
- for i in range(16): # 0 - 16
- name += chr(self._memobj.openradioname.name2[i])
- name = name.rstrip() # remove trailing spaces
- rx = RadioSettingValueString(0, 16, name)
- rset = RadioSetting("openradioname.name2", "Intro Line 2", rx)
- advanced.append(rset)
- workmode = RadioSettingGroup("workmode", "Work Mode Settings")
- group.append(workmode)
- # Toggle with [#] key
- options = ["Frequency", "Channel"]
- rx = RadioSettingValueList(options, options[_workmode.vfomrmode])
- rset = RadioSetting("workmodesettings.vfomrmode", "VFO/MR Mode", rx)
- workmode.append(rset)
- # Toggle with [A/B] key
- options = ["A", "B"]
- rx = RadioSettingValueList(options, options[_workmode.ab])
- rset = RadioSetting("workmodesettings.ab", "A/B Select", rx)
- workmode.append(rset)
- return group # END get_settings()
- def set_settings(self, settings):
- return
- _settings = self._memobj.settings
- _mem = self._memobj
- for element in settings:
- if not isinstance(element, RadioSetting):
- self.set_settings(element)
- continue
- else:
- try:
- name = element.get_name()
- if "." in name:
- bits = name.split(".")
- obj = self._memobj
- for bit in bits[:-1]:
- if "/" in bit:
- bit, index = bit.split("/", 1)
- index = int(index)
- obj = getattr(obj, bit)[index]
- else:
- obj = getattr(obj, bit)
- setting = bits[-1]
- else:
- obj = _settings
- setting = element.get_name()
- if element.has_apply_callback():
- LOG.debug("Using apply callback")
- element.run_apply_callback()
- elif setting == "voxLevel":
- setattr(obj, setting, int(element.value) + 1)
- elif element.value.get_mutable():
- LOG.debug("Setting %s = %s" % (setting, element.value))
- setattr(obj, setting, element.value)
- except Exception, e:
- LOG.debug(element.get_name())
- raise
diff -r d5e496f563fd -r 3fd7ed9a0de0 tools/cpep8.manifest --- a/tools/cpep8.manifest Fri Nov 13 08:07:04 2020 -0500 +++ b/tools/cpep8.manifest Mon Nov 16 15:18:58 2020 -0500 @@ -77,6 +77,7 @@ ./chirp/drivers/th_uv3r.py ./chirp/drivers/th_uv3r25.py ./chirp/drivers/th_uv8000.py +./chirp/drivers/th_uv88.py ./chirp/drivers/th_uvf8d.py ./chirp/drivers/thd72.py ./chirp/drivers/thuv1f.py _______________________________________________ chirp_devel mailing list chirp_devel@intrepid.danplanet.com http://intrepid.danplanet.com/mailman/listinfo/chirp_devel Developer docs: http://chirp.danplanet.com/projects/chirp/wiki/Developers
Dan,
Attached is the replacement "good" patch. I am also attaching the "image" again. It is the same as the previous one.
Also I have been having trouble again with my SMTP servers. The one I was using apparently added Barracuda and the "hg email tip" address is on a block list. :(
Jim KC9HI
On Mon, Nov 16, 2020 at 4:08 PM Jim Unroe via chirp_devel chirp_devel@intrepid.danplanet.com wrote:
# HG changeset patch # User Jim Unroe # Date 1605557938 18000 # Mon Nov 16 15:18:58 2020 -0500 # Node ID 3fd7ed9a0de01f7897286c9ac274237a45cc7831 # Parent d5e496f563fdfc9ea89dea5f119357235b82db6f [TH-UV88] New Model: TYT TH-UV88
This patch adds support for the TYT TH-UV88
Initial radio protocol decode, channels and memory layout by James Berry , Summer 2020
Related to #7817
diff -r d5e496f563fd -r 3fd7ed9a0de0 chirp/drivers/th_uv88.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/drivers/th_uv88.py Mon Nov 16 15:18:58 2020 -0500 @@ -0,0 +1,917 @@ +# Version 1.0 for TYT-UV88 +# Initial radio protocol decode, channels and memory layout +# by James Berry , Summer 2020 +# Additional configuration and help, Jim Unroe +# +# 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 time +import struct +import logging +import re +import math +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \
- RadioSettingValueBoolean, RadioSettingValueList, \
- RadioSettingValueString, RadioSettingValueInteger, \
- RadioSettingValueFloat, RadioSettings, InvalidValueError
+from textwrap import dedent
+LOG = logging.getLogger(__name__)
+MEM_FORMAT = """ +struct chns {
- ul32 rxfreq;
- ul32 txfreq;
- ul16 scramble:4
- rxtone:12; //decode:12
- ul16 decodeDSCI:1
- encodeDSCI:1
- unk1:1
- unk2:1
- txtone:12; //encode:12
- u8 power:2
- wide:2
- b_lock:2
- unk3:2;
- u8 unk4:3
- signal:2
- displayName:1
- unk5:2;
- u8 unk6:2
- pttid:2
- step:4; // not required
- u8 name[6];
+};
+struct vfo {
- ul32 rxfreq;
- ul32 txfreq; // displayed as an offset
- ul16 scramble:4
- rxtone:12; //decode:12
- ul16 decodeDSCI:1
- encodeDSCI:1
- unk1:1
- unk2:1
- txtone:12; //encode:12
- u8 power:2
- wide:2
- b_lock:2
- unk3:2;
- u8 unk4:3
- signal:2
- displayName:1
- unk5:2;
- u8 unk6:2
- pttid:2
- step:4;
- u8 name[6];
+};
+struct chname {
- u8 extra_name[10];
+};
+#seekto 0x0000; +struct chns chan_mem[199];
+#seekto 0x1960; +struct chname chan_name[199];
+#seekto 0x1180; +struct {
- u8 bitmap[26]; // one bit for each channel marked in use
+} chan_avail;
+#seekto 0x11A0; +struct {
- u8 bitmap[26]; // one bit for each channel skipped
+} chan_skip;
+#seekto 0x1140; +struct {
- u8 autoKeylock:1, // 0x1140 [18] *OFF, On
- unk_bit6_5:2, //
- vfomrmode:1, // *VFO, MR
- unk_bit3_0:4; //
- u8 unk_1141; // 0x1141
- u8 unk_1142; // 0x1142
- u8 unk_bit7_3:5, //
- ab:1, // * A, B
- unk_bit1_0:2; //
+} workmodesettings;
+#seekto 0x1160; +struct {
- u8 introScreen1[12]; // 0x1160 *Intro Screen Line 1(truncated to 12 alpha
- // text characters)
- u8 offFreqVoltage : 3, // 0x116C unknown referred to in code but not on
- // screen
- unk_bit4 : 1, //
- sqlLevel : 4; // [05] *OFF, 1-9
- u8 beep : 1 // 0x116D [09] *OFF, On
- callKind : 2, // code says 1750,2100,1000,1450 as options
- // not on screen
- introScreen: 2, // [20] *OFF, Voltage, Char String
- unkstr2: 2, //
- txChSelect : 1; // [02] *Last CH, Main CH
- u8 autoPowOff : 3, // 0x116E not on screen? OFF, 30Min, 1HR, 2HR
- unk : 1, //
- tot : 4; // [11] *OFF, 30 Second, 60 Second, 90 Second,
- // ... , 270 Second
- u8 unk_bit7:1, // 0x116F
- roger:1, // [14] *OFF, On
- dailDef:1, // Unknown - 'Volume, Frequency'
- language:1, // ?Chinese, English
- unk_bit3:1, //
- endToneElim:1, // *OFF, Frequency
- unkCheckBox1:1, //
- unkCheckBox2:1; //
- u8 scanResumeTime : 2, // 0x1170 2S, 5S, 10S, 15S (not on screen)
- disMode : 2, // [33] *Frequency, Channel, Name
- scanType: 2, // [17] *To, Co, Se
- ledMode: 2; // [07] *Off, On, Auto
- u8 unky; // 0x1171
- u8 str6; // 0x1172 Has flags to do with logging - factory
- // enabled (bits 16,64,128)
- u8 unk; // 0x1173
- u8 swAudio : 1, // 0x1174 [19] *OFF, On
- radioMoni : 1, // [34]*OFF, On
- keylock : 1, // *OFF, Auto
- dualWait : 1, // [06] *OFF, On
- unk_bit3 : 1, //
- light : 3; // [08] *1, 2, 3, 4, 5, 6, 7
- u8 voxSw : 1, // 0x1175 [13] *OFF, On
- voxDelay: 4, // *0.5S, 1.0S, 1.5S, 2.0S, 2.5S, 3.0S, 3.5S,
- // 4.0S, 4.5S, 5.0S
- voxLevel : 3; // [03] *1, 2, 3, 4, 5, 6, 7
- u8 str9 : 4, // 0x1176
- saveMode : 2, // [16] *OFF, 1:1, 1:2, 1:4
- keyMode : 2; // [32] *ALL, PTT, KEY, Key & Side Key
- u8 unk2; // 0x1177
- u8 unk3; // 0x1178
- u8 unk4; // 0x1179
- u8 name2[6]; // 0x117A unused
+} basicsettings;
+#seekto 0x1940; +struct {
- u8 name1[16]; // Intro Screen Line 1 (16 alpha text characters)
- u8 name2[16]; // Intro Screen Line 2 (16 alpha text characters)
+} openradioname;
+"""
+MEM_SIZE = 0x22A0 +BLOCK_SIZE = 0x20 +STIMEOUT = 2 +BAUDRATE = 57600
+# Channel power: 3 levels +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
- chirp_common.PowerLevel("Mid", watts=2.50),
- chirp_common.PowerLevel("Low", watts=0.50)]
+SCRAMBLE_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8"] +B_LOCK_LIST = ["OFF", "Sub", "Carrier"] +OPTSIG_LIST = ["OFF", "DTMF", "2TONE", "5TONE"] +PTTID_LIST = ["Off", "BOT", "EOT", "Both"] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0] +LIST_STEPS = [str(x) for x in STEPS]
+def _clean_buffer(radio):
- radio.pipe.timeout = 0.005
- junk = radio.pipe.read(256)
- radio.pipe.timeout = STIMEOUT
- if junk:
- LOG.debug("Got %i bytes of junk before starting" % len(junk))
+def _rawrecv(radio, amount):
- """Raw read from the radio device"""
- data = ""
- try:
- data = radio.pipe.read(amount)
- except Exception:
- _exit_program_mode(radio)
- msg = "Generic error reading data from radio; check your cable."
- raise errors.RadioError(msg)
- if len(data) != amount:
- _exit_program_mode(radio)
- msg = "Error reading from radio: not the amount of data we want."
- raise errors.RadioError(msg)
- return data
+def _rawsend(radio, data):
- """Raw send to the radio device"""
- try:
- radio.pipe.write(data)
- except Exception:
- raise errors.RadioError("Error sending data to radio")
+def _make_read_frame(addr, length):
- frame = "\xFE\xFE\xEE\xEF\xEB"
- """Pack the info in the header format"""
- frame += struct.pack(">ih", addr, length)
- frame += "\xFD"
- # Return the data
- return frame
+def _make_write_frame(addr, length, data=""):
- frame = "\xFE\xFE\xEE\xEF\xE4"
- """Pack the info in the header format"""
- output = struct.pack(">ih", addr, length)
- # Add the data if set
- if len(data) != 0:
- output += data
- frame += output
- frame += _calculate_checksum(output)
- frame += "\xFD"
- # Return the data
- return frame
+def _calculate_checksum(data):
- num = 0
- for x in range(0, len(data)):
- num = (num + ord(data[x])) % 256
- if num == 0:
- return chr(0)
- return chr(256 - num)
+def _recv(radio, addr, length):
- """Get data from the radio """
- data = _rawrecv(radio, length)
- # DEBUG
- LOG.info("Response:")
- LOG.debug(util.hexprint(data))
- return data
+def _do_ident(radio):
- """Put the radio in PROGRAM mode & identify it"""
- radio.pipe.baudrate = BAUDRATE
- radio.pipe.parity = "N"
- radio.pipe.timeout = STIMEOUT
- # Flush input buffer
- _clean_buffer(radio)
- # Ident radio
- magic = "\xFE\xFE\xEE\xEF\xE0\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 36)
- if not ack.startswith("\xFE\xFE\xEF\xEE\xE1\x55\x56\x38\x38"
- ) or not ack.endswith("\xFD"):
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond as expected (A)")
- return True
+def _exit_program_mode(radio):
- # This may be the last part of a read
- magic = "\xFE\xFE\xEE\xEF\xE5\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 7)
- if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond as expected (B)")
+def _download(radio):
- """Get the memory map"""
- # Put radio in program mode and identify it
- _do_ident(radio)
- # Enter read mode
- magic = "\xFE\xFE\xEE\xEF\xE2\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 7)
- if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond to enter read mode")
- # UI progress
- status = chirp_common.Status()
- status.cur = 0
- status.max = MEM_SIZE / BLOCK_SIZE
- status.msg = "Cloning from radio..."
- radio.status_fn(status)
- data = ""
- for addr in range(0, MEM_SIZE, BLOCK_SIZE):
- frame = _make_read_frame(addr, BLOCK_SIZE)
- # DEBUG
- LOG.debug("Frame=" + util.hexprint(frame))
- # Sending the read request
- _rawsend(radio, frame)
- # Now we read data
- d = _recv(radio, addr, BLOCK_SIZE + 13)
- LOG.debug("Response Data= " + util.hexprint(d))
- if not d.startswith("\xFE\xFE\xEF\xEE\xE4"):
- LOG.warning("Incorrect start")
- if not d.endswith("\xFD"):
- LOG.warning("Incorrect end")
- # could validate the block data
- # Aggregate the data
- data += d[11:-2]
- # UI Update
- status.cur = addr / BLOCK_SIZE
- status.msg = "Cloning from radio..."
- radio.status_fn(status)
- _exit_program_mode(radio)
- return data
+def _upload(radio):
- """Upload procedure"""
- # Put radio in program mode and identify it
- _do_ident(radio)
- magic = "\xFE\xFE\xEE\xEF\xE3\x55\x56\x38\x38\xFD"
- _rawsend(radio, magic)
- ack = _rawrecv(radio, 7)
- if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
- _exit_program_mode(radio)
- if ack:
- LOG.debug(repr(ack))
- raise errors.RadioError("Radio did not respond to enter write mode")
- # UI progress
- status = chirp_common.Status()
- status.cur = 0
- status.max = MEM_SIZE / BLOCK_SIZE
- status.msg = "Cloning to radio..."
- radio.status_fn(status)
- # The fun starts here
- for addr in range(0, MEM_SIZE, BLOCK_SIZE):
- # Official programmer skips writing these memory locations
- if addr >= 0x1680 and addr < 0x1940:
- continue
- # Sending the data
- data = radio.get_mmap()[addr:addr + BLOCK_SIZE]
- frame = _make_write_frame(addr, BLOCK_SIZE, data)
- LOG.warning("Frame:%s:" % util.hexprint(frame))
- _rawsend(radio, frame)
- ack = _rawrecv(radio, 7)
- LOG.debug("Response Data= " + util.hexprint(ack))
- if not ack.startswith("\xFE\xFE\xEF\xEE\xE6\x00\xFD"):
- LOG.warning("Unexpected response")
- _exit_program_mode(radio)
- msg = "Bad ack writing block 0x%04x" % addr
- raise errors.RadioError(msg)
- # UI Update
- status.cur = addr / BLOCK_SIZE
- status.msg = "Cloning to radio..."
- radio.status_fn(status)
- _exit_program_mode(radio)
+def _do_map(chn, sclr, mary):
- """Set or Clear the chn (1-128) bit in mary[] word array map"""
- # chn is 1-based channel, sclr:1 = set, 0= = clear, 2= return state
- # mary[] is u8 array, but the map is by nibbles
- ndx = int(math.floor((chn - 1) / 8))
- bv = (chn - 1) % 8
- msk = 1 << bv
- mapbit = sclr
- if sclr == 1: # Set the bit
- mary[ndx] = mary[ndx] | msk
- elif sclr == 0: # clear
- mary[ndx] = mary[ndx] & (~ msk) # ~ is complement
- else: # return current bit state
- mapbit = 0
- if (mary[ndx] & msk) > 0:
- mapbit = 1
- return mapbit
+@directory.register +class THUV88Radio(chirp_common.CloneModeRadio):
- """TYT UV88 Radio"""
- VENDOR = "TYT"
- MODEL = "TH-UV88"
- MODES = ['WFM', 'FM', 'NFM']
- TONES = chirp_common.TONES
- DTCS_CODES = chirp_common.DTCS_CODES
- NAME_LENGTH = 10
- DTMF_CHARS = list("0123456789ABCD*#")
- # 136-174, 400-480
- VALID_BANDS = [(136000000, 174000000), (400000000, 480000000)]
- # Valid chars on the LCD
- VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
- "`!"#$%&'()*+,-./:;<=>?@[]^_"
- @classmethod
- def get_prompts(cls):
- rp = chirp_common.RadioPrompts()
- rp.info = \
- ('TYT UV-88\n')
- rp.pre_download = _(dedent("""\
- This is an early stage beta driver
- """))
- rp.pre_upload = _(dedent("""\
- This is an early stage beta driver - upload at your own risk
- """))
- return rp
- def get_features(self):
- rf = chirp_common.RadioFeatures()
- rf.has_settings = True
- rf.has_bank = False
- rf.has_comment = False
- rf.has_tuning_step = False # Not as chan feature
- rf.valid_tuning_steps = STEPS
- rf.can_odd_split = False
- rf.has_name = True
- rf.has_offset = True
- rf.has_mode = True
- rf.has_dtcs = True
- rf.has_rx_dtcs = True
- rf.has_dtcs_polarity = True
- rf.has_ctone = True
- rf.has_cross = True
- rf.has_sub_devices = False
- rf.valid_name_length = self.NAME_LENGTH
- rf.valid_modes = self.MODES
- rf.valid_characters = self.VALID_CHARS
- rf.valid_duplexes = ["-", "+", "off", ""]
- rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
- rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS",
- "Tone->DTCS", "DTCS->Tone", "->Tone",
- "DTCS->DTCS"]
- rf.valid_skips = []
- rf.valid_power_levels = POWER_LEVELS
- rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES # this is just to
- # get it working, not sure this is right
- rf.valid_bands = self.VALID_BANDS
- rf.memory_bounds = (1, 199)
- rf.valid_skips = ["", "S"]
- return rf
- def sync_in(self):
- """Download from radio"""
- try:
- data = _download(self)
- except errors.RadioError:
- # Pass through any real errors we raise
- raise
- except Exception:
- # If anything unexpected happens, make sure we raise
- # a RadioError and log the problem
- LOG.exception('Unexpected error during download')
- raise errors.RadioError('Unexpected error communicating '
- 'with the radio')
- self._mmap = memmap.MemoryMap(data)
- self.process_mmap()
- def sync_out(self):
- """Upload to radio"""
- try:
- _upload(self)
- except Exception:
- # If anything unexpected happens, make sure we raise
- # a RadioError and log the problem
- LOG.exception('Unexpected error during upload')
- raise errors.RadioError('Unexpected error communicating '
- 'with the radio')
- def process_mmap(self):
- """Process the mem map into the mem object"""
- self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
- def get_raw_memory(self, number):
- return repr(self._memobj.memory[number - 1])
- def set_memory(self, memory):
- """A value in a UI column for chan 'number' has been modified."""
- # update all raw channel memory values (_mem) from UI (mem)
- _mem = self._memobj.chan_mem[memory.number - 1]
- _name = self._memobj.chan_name[memory.number - 1]
- if memory.empty:
- _do_map(memory.number, 0, self._memobj.chan_avail.bitmap)
- return
- _do_map(memory.number, 1, self._memobj.chan_avail.bitmap)
- if memory.skip == "":
- _do_map(memory.number, 1, self._memobj.chan_skip.bitmap)
- else:
- _do_map(memory.number, 0, self._memobj.chan_skip.bitmap)
- return self._set_memory(memory, _mem, _name)
- def get_memory(self, number):
- # radio first channel is 1, mem map is base 0
- _mem = self._memobj.chan_mem[number - 1]
- _name = self._memobj.chan_name[number - 1]
- mem = chirp_common.Memory()
- mem.number = number
- # Determine if channel is empty
- if _do_map(number, 2, self._memobj.chan_avail.bitmap) == 0:
- mem.empty = True
- return mem
- if _do_map(mem.number, 2, self._memobj.chan_skip.bitmap) > 0:
- mem.skip = ""
- else:
- mem.skip = "S"
- return self._get_memory(mem, _mem, _name)
- def _get_memory(self, mem, _mem, _name):
- """Convert raw channel memory data into UI columns"""
- mem.extra = RadioSettingGroup("extra", "Extra")
- mem.empty = False
- # This function process both 'normal' and Freq up/down' entries
- mem.freq = int(_mem.rxfreq) * 10
- if _mem.txfreq == 0xFFFFFFFF:
- # TX freq not set
- mem.duplex = "off"
- mem.offset = 0
- elif 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.name = ""
- for i in range(6): # 0 - 6
- mem.name += chr(_mem.name[i])
- for i in range(10):
- mem.name += chr(_name.extra_name[i])
- mem.name = mem.name.rstrip() # remove trailing spaces
- # ########## TONE ##########
- if _mem.txtone > 2600:
- # All off
- txmode = ""
- elif _mem.txtone > 511:
- txmode = "Tone"
- mem.rtone = int(_mem.txtone) / 10.0
- else:
- # DTSC
- txmode = "DTCS"
- mem.dtcs = int(format(int(_mem.txtone), 'o'))
- if _mem.rxtone > 2600:
- rxmode = ""
- elif _mem.rxtone > 511:
- rxmode = "Tone"
- mem.ctone = int(_mem.rxtone) / 10.0
- else:
- rxmode = "DTCS"
- mem.rx_dtcs = int(format(int(_mem.rxtone), 'o'))
- mem.dtcs_polarity = ("N", "R")[_mem.encodeDSCI] + (
- "N", "R")[_mem.decodeDSCI]
- mem.tmode = ""
- if txmode == "Tone" and not rxmode:
- mem.tmode = "Tone"
- elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
- mem.tmode = "TSQL"
- elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
- mem.tmode = "DTCS"
- elif rxmode or txmode:
- mem.tmode = "Cross"
- mem.cross_mode = "%s->%s" % (txmode, rxmode)
- # ########## TONE ##########
- mem.mode = self.MODES[_mem.wide]
- mem.power = POWER_LEVELS[int(_mem.power)]
- b_lock = RadioSetting("b_lock", "B_Lock",
- RadioSettingValueList(B_LOCK_LIST,
- B_LOCK_LIST[_mem.b_lock]))
- mem.extra.append(b_lock)
- b_lock = RadioSetting("step", "Step",
- RadioSettingValueList(LIST_STEPS,
- LIST_STEPS[_mem.step]))
- mem.extra.append(b_lock)
- scramble_value = _mem.scramble
- if scramble_value >= 8: # Looks like OFF is 0x0f ** CONFIRM
- scramble_value = 0
- scramble = RadioSetting("scramble", "Scramble",
- RadioSettingValueList(SCRAMBLE_LIST,
- SCRAMBLE_LIST[
- scramble_value]))
- mem.extra.append(scramble)
- optsig = RadioSetting("signal", "Optional signaling",
- RadioSettingValueList(
- OPTSIG_LIST,
- OPTSIG_LIST[_mem.signal]))
- mem.extra.append(optsig)
- rs = RadioSetting("pttid", "PTT ID",
- RadioSettingValueList(PTTID_LIST,
- PTTID_LIST[_mem.pttid]))
- mem.extra.append(rs)
- return mem
- def _set_memory(self, mem, _mem, _name):
- # """Convert UI column data (mem) into MEM_FORMAT memory (_mem)."""
- _mem.rxfreq = mem.freq / 10
- if mem.duplex == "off":
- _mem.txfreq = 0xFFFFFFFF
- elif mem.duplex == "+":
- _mem.txfreq = (mem.freq + mem.offset) / 10
- elif mem.duplex == "-":
- _mem.txfreq = (mem.freq - mem.offset) / 10
- else:
- _mem.txfreq = _mem.rxfreq
- out_name = mem.name.ljust(16)
- for i in range(6): # 0 - 6
- _mem.name[i] = ord(out_name[i])
- for i in range(10):
- _name.extra_name[i] = ord(out_name[i+6])
- if mem.name != "":
- _mem.displayName = 1 # Name only displayed if this is set on
- else:
- _mem.displayName = 0
- rxmode = ""
- txmode = ""
- if mem.tmode == "Tone":
- txmode = "Tone"
- elif mem.tmode == "TSQL":
- rxmode = "Tone"
- txmode = "TSQL"
- elif mem.tmode == "DTCS":
- rxmode = "DTCSSQL"
- txmode = "DTCS"
- elif mem.tmode == "Cross":
- txmode, rxmode = mem.cross_mode.split("->", 1)
- if mem.dtcs_polarity[1] == "N":
- _mem.decodeDSCI = 0
- else:
- _mem.decodeDSCI = 1
- if rxmode == "":
- _mem.rxtone = 0xFFF
- elif rxmode == "Tone":
- _mem.rxtone = int(float(mem.ctone) * 10)
- elif rxmode == "DTCSSQL":
- _mem.rxtone = int(str(mem.dtcs), 8)
- elif rxmode == "DTCS":
- _mem.rxtone = int(str(mem.rx_dtcs), 8)
- if mem.dtcs_polarity[0] == "N":
- _mem.encodeDSCI = 0
- else:
- _mem.encodeDSCI = 1
- if txmode == "":
- _mem.txtone = 0xFFF
- elif txmode == "Tone":
- _mem.txtone = int(float(mem.rtone) * 10)
- elif txmode == "TSQL":
- _mem.txtone = int(float(mem.ctone) * 10)
- elif txmode == "DTCS":
- _mem.txtone = int(str(mem.dtcs), 8)
- _mem.wide = self.MODES.index(mem.mode)
- _mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
- for element in mem.extra:
- setattr(_mem, element.get_name(), element.value)
- return
- def get_settings(self):
- """Translate the MEM_FORMAT structs into setstuf in the UI"""
- _settings = self._memobj.basicsettings
- _workmode = self._memobj.workmodesettings
- basic = RadioSettingGroup("basic", "Basic Settings")
- group = RadioSettings(basic)
- # Menu 02 - TX Channel Select
- options = ["Last Channel", "Main Channel"]
- rx = RadioSettingValueList(options, options[_settings.txChSelect])
- rset = RadioSetting("basicsettings.txChSelect",
- "Priority Transmit", rx)
- basic.append(rset)
- # Menu 03 - VOX Level
- rx = RadioSettingValueInteger(1, 7, _settings.voxLevel - 1)
- rset = RadioSetting("basicsettings.voxLevel", "Vox Level", rx)
- basic.append(rset)
- # Menu 05 - Squelch Level
- options = ["OFF"] + ["%s" % x for x in range(1, 10)]
- rx = RadioSettingValueList(options, options[_settings.sqlLevel])
- rset = RadioSetting("basicsettings.sqlLevel", "Squelch Level", rx)
- basic.append(rset)
- # Menu 06 - Dual Wait
- rx = RadioSettingValueBoolean(_settings.dualWait)
- rset = RadioSetting("basicsettings.dualWait", "Dual Wait/Standby", rx)
- basic.append(rset)
- # Menu 07 - LED Mode
- options = ["Off", "On", "Auto"]
- rx = RadioSettingValueList(options, options[_settings.ledMode])
- rset = RadioSetting("basicsettings.ledMode", "LED Display Mode", rx)
- basic.append(rset)
- # Menu 08 - Light
- options = ["%s" % x for x in range(1, 8)]
- rx = RadioSettingValueList(options, options[_settings.light])
- rset = RadioSetting("basicsettings.light",
- "Background Light Color", rx)
- basic.append(rset)
- # Menu 09 - Beep
- rx = RadioSettingValueBoolean(_settings.beep)
- rset = RadioSetting("basicsettings.beep", "Keypad Beep", rx)
- basic.append(rset)
- # Menu 11 - TOT
- options = ["Off"] + ["%s seconds" % x for x in range(30, 300, 30)]
- rx = RadioSettingValueList(options, options[_settings.tot])
- rset = RadioSetting("basicsettings.tot",
- "Transmission Time-out Timer", rx)
- basic.append(rset)
- # Menu 13 - VOX Switch
- rx = RadioSettingValueBoolean(_settings.voxSw)
- rset = RadioSetting("basicsettings.voxSw", "Vox Switch", rx)
- basic.append(rset)
- # Menu 14 - Roger
- rx = RadioSettingValueBoolean(_settings.roger)
- rset = RadioSetting("basicsettings.roger", "Roger Beep", rx)
- basic.append(rset)
- # Menu 16 - Save Mode
- options = ["Off", "1:1", "1:2", "1:4"]
- rx = RadioSettingValueList(options, options[_settings.saveMode])
- rset = RadioSetting("basicsettings.saveMode", "Battery Save Mode", rx)
- basic.append(rset)
- # Menu 33 - Display Mode
- options = ['Frequency', 'Channel', 'Name']
- rx = RadioSettingValueList(options, options[_settings.disMode])
- rset = RadioSetting("basicsettings.disMode", "LED Display Mode", rx)
- basic.append(rset)
- advanced = RadioSettingGroup("advanced", "Advanced Settings")
- group.append(advanced)
- # software only
- options = ['0.5S', '1.0S', '1.5S', '2.0S', '2.5S', '3.0S', '3.5S',
- '4.0S', '4.5S', '5.0S']
- rx = RadioSettingValueList(options, options[_settings.voxDelay])
- rset = RadioSetting("basicsettings.voxDelay", "VOX Delay", rx)
- advanced.append(rset)
- # software only
- name = ""
- for i in range(16): # 0 - 16
- name += chr(self._memobj.openradioname.name1[i])
- name = name.rstrip() # remove trailing spaces
- rx = RadioSettingValueString(0, 16, name)
- rset = RadioSetting("openradioname.name1", "Intro Line 1", rx)
- advanced.append(rset)
- # software only
- name = ""
- for i in range(16): # 0 - 16
- name += chr(self._memobj.openradioname.name2[i])
- name = name.rstrip() # remove trailing spaces
- rx = RadioSettingValueString(0, 16, name)
- rset = RadioSetting("openradioname.name2", "Intro Line 2", rx)
- advanced.append(rset)
- workmode = RadioSettingGroup("workmode", "Work Mode Settings")
- group.append(workmode)
- # Toggle with [#] key
- options = ["Frequency", "Channel"]
- rx = RadioSettingValueList(options, options[_workmode.vfomrmode])
- rset = RadioSetting("workmodesettings.vfomrmode", "VFO/MR Mode", rx)
- workmode.append(rset)
- # Toggle with [A/B] key
- options = ["A", "B"]
- rx = RadioSettingValueList(options, options[_workmode.ab])
- rset = RadioSetting("workmodesettings.ab", "A/B Select", rx)
- workmode.append(rset)
- return group # END get_settings()
- def set_settings(self, settings):
- return
- _settings = self._memobj.settings
- _mem = self._memobj
- for element in settings:
- if not isinstance(element, RadioSetting):
- self.set_settings(element)
- continue
- else:
- try:
- name = element.get_name()
- if "." in name:
- bits = name.split(".")
- obj = self._memobj
- for bit in bits[:-1]:
- if "/" in bit:
- bit, index = bit.split("/", 1)
- index = int(index)
- obj = getattr(obj, bit)[index]
- else:
- obj = getattr(obj, bit)
- setting = bits[-1]
- else:
- obj = _settings
- setting = element.get_name()
- if element.has_apply_callback():
- LOG.debug("Using apply callback")
- element.run_apply_callback()
- elif setting == "voxLevel":
- setattr(obj, setting, int(element.value) + 1)
- elif element.value.get_mutable():
- LOG.debug("Setting %s = %s" % (setting, element.value))
- setattr(obj, setting, element.value)
- except Exception, e:
- LOG.debug(element.get_name())
- raise
diff -r d5e496f563fd -r 3fd7ed9a0de0 tools/cpep8.manifest --- a/tools/cpep8.manifest Fri Nov 13 08:07:04 2020 -0500 +++ b/tools/cpep8.manifest Mon Nov 16 15:18:58 2020 -0500 @@ -77,6 +77,7 @@ ./chirp/drivers/th_uv3r.py ./chirp/drivers/th_uv3r25.py ./chirp/drivers/th_uv8000.py +./chirp/drivers/th_uv88.py ./chirp/drivers/th_uvf8d.py ./chirp/drivers/thd72.py ./chirp/drivers/thuv1f.py _______________________________________________ chirp_devel mailing list chirp_devel@intrepid.danplanet.com http://intrepid.danplanet.com/mailman/listinfo/chirp_devel Developer docs: http://chirp.danplanet.com/projects/chirp/wiki/Developers
participants (2)
-
Jim Unroe
-
Jim Unroe