diff --git a/chirp/bitwise.py b/chirp/bitwise.py
index 4cdbfad2f..4e1188c05 100644
--- a/chirp/bitwise.py
+++ b/chirp/bitwise.py
@@ -64,8 +64,6 @@
import re
import warnings
-from builtins import bytes
-
from chirp import bitwise_grammar
LOG = logging.getLogger(__name__)
diff --git a/chirp/chirp_common.py b/chirp/chirp_common.py
index e8261cfef..f20718ce2 100644
--- a/chirp/chirp_common.py
+++ b/chirp/chirp_common.py
@@ -13,8 +13,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from builtins import bytes
-
import base64
import json
import inspect
@@ -1389,9 +1387,11 @@ def load(self, filename):
pass
-class DetectableInterface:
- DETECTED_MODELS = None
+def class_detected_models_attribute(cls):
+ return 'DETECTED_MODELS_%s' % cls.__name__
+
+class DetectableInterface:
@classmethod
def detect_from_serial(cls, pipe):
"""Communicate with the radio via serial to determine proper class
@@ -1400,14 +1400,28 @@ def detect_from_serial(cls, pipe):
RadioError if not. If NotImplemented is raised, we assume that no
detection is possible or necessary.
"""
- assert cls.DETECTED_MODELS is None, (
+ detected = getattr(cls, class_detected_models_attribute(cls), None)
+ assert detected is None, (
'Class has detected models but no detect_from_serial() '
'implementation')
raise NotImplementedError()
@classmethod
- def detected_models(cls):
- return list(cls.DETECTED_MODELS or [])
+ def detected_models(cls, include_self=True):
+ detected = getattr(cls, class_detected_models_attribute(cls), [])
+ # Only include this class if it is registered
+ if include_self and hasattr(cls, '_DETECTED_BY'):
+ extra = [cls]
+ else:
+ extra = []
+ return extra + list(detected)
+
+ @classmethod
+ def detect_model(cls, detected_cls):
+ detected_attr = class_detected_models_attribute(cls)
+ if getattr(cls, detected_attr, None) is None:
+ setattr(cls, detected_attr, [])
+ getattr(cls, detected_attr).append(detected_cls)
class CloneModeRadio(FileBackedRadio, ExternalMemoryProperties,
diff --git a/chirp/directory.py b/chirp/directory.py
index b917bb648..934d8375f 100644
--- a/chirp/directory.py
+++ b/chirp/directory.py
@@ -63,8 +63,8 @@ def register(cls):
DRV_TO_RADIO[ident] = cls
RADIO_TO_DRV[cls] = ident
- if not hasattr(cls, '_DETECTED_MODEL'):
- cls._DETECTED_MODEL = False
+ if not hasattr(cls, '_DETECTED_BY'):
+ cls._DETECTED_BY = None
return cls
@@ -81,10 +81,8 @@ def detected_by(manager_class):
def wrapper(cls):
assert issubclass(cls, chirp_common.CloneModeRadio)
- cls._DETECTED_MODEL = True
- if manager_class.DETECTED_MODELS is None:
- manager_class.DETECTED_MODELS = []
- manager_class.DETECTED_MODELS.append(cls)
+ cls._DETECTED_BY = manager_class
+ manager_class.detect_model(cls)
return cls
return wrapper
diff --git a/chirp/drivers/anytone778uv.py b/chirp/drivers/anytone778uv.py
index d06f645a5..b3faa3f86 100644
--- a/chirp/drivers/anytone778uv.py
+++ b/chirp/drivers/anytone778uv.py
@@ -51,18 +51,6 @@
LOG = logging.getLogger(__name__)
-# Gross hack to handle missing future module on un-updatable
-# platforms like MacOS. Just avoid registering these radio
-# classes for now.
-try:
- from builtins import bytes
- has_future = True
-except ImportError:
- has_future = False
- LOG.debug('python-future package is not '
- 'available; %s requires it' % __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.
@@ -380,15 +368,13 @@ def cstring_to_py_string(cstring):
# Check the radio version reported to see if it's one we support,
-# returns bool version supported, and the band index
+# returns bool version supported, the band index, and has_vox
def check_ver(ver_response, allowed_types):
''' Check the returned radio version is one we approve of '''
LOG.debug('ver_response = ')
LOG.debug(util.hexprint(ver_response))
- global HAS_VOX
-
resp = bitwise.parse(VER_FORMAT, ver_response)
verok = False
@@ -399,8 +385,6 @@ def check_ver(ver_response, allowed_types):
(model, version))
LOG.debug('allowed_types = %s' % allowed_types)
- HAS_VOX = "P" == model[-1:]
-
if model in allowed_types:
LOG.debug('model in allowed_types')
@@ -416,8 +400,7 @@ def check_ver(ver_response, allowed_types):
# Put the radio in programming mode, sending the initial command and checking
# the response. raise RadioError if there is no response (500ms timeout), and
# if the returned version isn't matched by check_ver
-def enter_program_mode(radio):
- serial = radio.pipe
+def enter_program_mode(serial):
# place the radio in program mode, and confirm
program_response = send_serial_command(serial, b'PROGRAM')
@@ -426,29 +409,26 @@ def enter_program_mode(radio):
LOG.debug('entered program mode')
# read the radio ID string, make sure it matches one we know about
- ver_response = send_serial_command(serial, b'\x02')
+ return send_serial_command(serial, b'\x02')
+
- verok, bandlimit = check_ver(ver_response, radio.ALLOWED_RADIO_TYPES)
+def get_bandlimit_from_ver(radio, ver_response):
+ verok, bandlimit, = check_ver(ver_response,
+ radio.ALLOWED_RADIO_TYPES)
if not verok:
- exit_program_mode(radio)
- ver = "V2" if radio.VENDOR == "CRT" else "VOX"
- if HAS_VOX and ("V2" and "VOX") not in radio.MODEL:
- raise errors.RadioError(
- 'Radio identified as model with VOX\n'
- 'Try %s-%s %s' %
- (radio.VENDOR, radio.MODEL, ver))
- else:
- raise errors.RadioError(
- 'Radio version not in allowed list for %s-%s:\n'
- '%s' %
- (radio.VENDOR, radio.MODEL, util.hexprint(ver_response)))
+ LOG.debug('Radio version response not allowed for %s-%s: %s',
+ radio.VENDOR, radio.MODEL, ver_response)
+ raise errors.RadioError('Radio model/version mismatch')
return bandlimit
# Exit programming mode
-def exit_program_mode(radio):
- send_serial_command(radio.pipe, b'END')
+def exit_program_mode(serial):
+ try:
+ send_serial_command(serial, b'END')
+ except Exception as e:
+ LOG.error('Failed to exit programming mode: %s', e)
# Parse a packet from the radio returning the header (R/W, address, data, and
@@ -468,12 +448,13 @@ def parse_read_response(resp):
def do_download(radio):
'''Download memories from the radio'''
+ # NOTE: The radio is already in programming mode because of
+ # detect_from_serial()
+
# Get the serial port connection
serial = radio.pipe
try:
- enter_program_mode(radio)
-
memory_data = bytes()
# status info for the UI
@@ -494,6 +475,8 @@ def do_download(radio):
# LOG.debug('read response:\n%s' % util.hexprint(read_response))
address, data, valid = parse_read_response(read_response)
+ if not valid:
+ raise errors.RadioError('Invalid response received from radio')
memory_data += data
# update UI
@@ -501,11 +484,12 @@ def do_download(radio):
// MEMORY_RW_BLOCK_SIZE
radio.status_fn(status)
- exit_program_mode(radio)
except errors.RadioError as e:
raise e
except Exception as e:
raise errors.RadioError('Failed to download from radio: %s' % e)
+ finally:
+ exit_program_mode(radio.pipe)
return memmap.MemoryMapBytes(memory_data)
@@ -522,7 +506,8 @@ def make_write_data_cmd(addr, data, datalen):
# Upload a memory map to the radio
def do_upload(radio):
try:
- bandlimit = enter_program_mode(radio)
+ ver_response = enter_program_mode(radio.pipe)
+ bandlimit = get_bandlimit_from_ver(radio, ver_response)
if bandlimit != radio._memobj.radio_settings.bandlimit:
LOG.warning('radio and image bandlimits differ'
@@ -573,17 +558,18 @@ def do_upload(radio):
LOG.debug(' * write cmd:\n%s' % util.hexprint(write_command))
LOG.debug(' * write response:\n%s' %
util.hexprint(write_response))
- exit_program_mode(radio)
+ exit_program_mode(radio.pipe)
raise errors.RadioError('Radio NACK\'d write command')
# update UI
status.cur = idx
radio.status_fn(status)
- exit_program_mode(radio)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError('Failed to download from radio: %s' % e)
+ finally:
+ exit_program_mode(radio.pipe)
# Get the value of @bitfield @number of bits in from 0
@@ -647,6 +633,17 @@ class AnyTone778UVBase(chirp_common.CloneModeRadio,
NAME_LENGTH = 5
HAS_VOX = False
+ @classmethod
+ def detect_from_serial(cls, pipe):
+ ver_response = enter_program_mode(pipe)
+ for radio_cls in cls.detected_models():
+ ver_ok, _ = check_ver(ver_response, radio_cls.ALLOWED_RADIO_TYPES)
+ if ver_ok:
+ return radio_cls
+ LOG.warning('No match for ver_response: %s',
+ util.hexprint(ver_response))
+ raise errors.RadioError('Incorrect radio model')
+
@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
@@ -1730,46 +1727,49 @@ def set_settings(self, settings):
# Original non-VOX models
-if has_future:
- @directory.register
- class AnyTone778UV(AnyTone778UVBase):
- VENDOR = "AnyTone"
- MODEL = "778UV"
- # Allowed radio types is a dict keyed by model of a list of version
- # strings
- ALLOWED_RADIO_TYPES = {'AT778UV': ['V100', 'V200']}
-
- @directory.register
- class RetevisRT95(AnyTone778UVBase):
- VENDOR = "Retevis"
- MODEL = "RT95"
- # Allowed radio types is a dict keyed by model of a list of version
- # strings
- ALLOWED_RADIO_TYPES = {'RT95': ['V100']}
-
- @directory.register
- class CRTMicronUV(AnyTone778UVBase):
- VENDOR = "CRT"
- MODEL = "Micron UV"
- # Allowed radio types is a dict keyed by model of a list of version
- # strings
- ALLOWED_RADIO_TYPES = {'MICRON': ['V100']}
-
- @directory.register
- class MidlandDBR2500(AnyTone778UVBase):
- VENDOR = "Midland"
- MODEL = "DBR2500"
- # Allowed radio types is a dict keyed by model of a list of version
- # strings
- ALLOWED_RADIO_TYPES = {'DBR2500': ['V100']}
-
- @directory.register
- class YedroYCM04vus(AnyTone778UVBase):
- VENDOR = "Yedro"
- MODEL = "YC-M04VUS"
- # Allowed radio types is a dict keyed by model of a list of version
- # strings
- ALLOWED_RADIO_TYPES = {'YCM04UV': ['V100']}
+@directory.register
+class AnyTone778UV(AnyTone778UVBase):
+ VENDOR = "AnyTone"
+ MODEL = "778UV"
+ # Allowed radio types is a dict keyed by model of a list of version
+ # strings
+ ALLOWED_RADIO_TYPES = {'AT778UV': ['V100', 'V200']}
+
+
+@directory.register
+class RetevisRT95(AnyTone778UVBase):
+ VENDOR = "Retevis"
+ MODEL = "RT95"
+ # Allowed radio types is a dict keyed by model of a list of version
+ # strings
+ ALLOWED_RADIO_TYPES = {'RT95': ['V100']}
+
+
+@directory.register
+class CRTMicronUV(AnyTone778UVBase):
+ VENDOR = "CRT"
+ MODEL = "Micron UV"
+ # Allowed radio types is a dict keyed by model of a list of version
+ # strings
+ ALLOWED_RADIO_TYPES = {'MICRON': ['V100']}
+
+
+@directory.register
+class MidlandDBR2500(AnyTone778UVBase):
+ VENDOR = "Midland"
+ MODEL = "DBR2500"
+ # Allowed radio types is a dict keyed by model of a list of version
+ # strings
+ ALLOWED_RADIO_TYPES = {'DBR2500': ['V100']}
+
+
+@directory.register
+class YedroYCM04vus(AnyTone778UVBase):
+ VENDOR = "Yedro"
+ MODEL = "YC-M04VUS"
+ # Allowed radio types is a dict keyed by model of a list of version
+ # strings
+ ALLOWED_RADIO_TYPES = {'YCM04UV': ['V100']}
class AnyTone778UVvoxBase(AnyTone778UVBase):
@@ -1779,27 +1779,31 @@ class AnyTone778UVvoxBase(AnyTone778UVBase):
# New VOX models
-if has_future:
- @directory.register
- class AnyTone778UVvox(AnyTone778UVvoxBase):
- VENDOR = "AnyTone"
- MODEL = "778UV VOX"
- # Allowed radio types is a dict keyed by model of a list of version
- # strings
- ALLOWED_RADIO_TYPES = {'778UV-P': ['V100']}
-
- @directory.register
- class RetevisRT95vox(AnyTone778UVvoxBase):
- VENDOR = "Retevis"
- MODEL = "RT95 VOX"
- # Allowed radio types is a dict keyed by model of a list of version
- # strings
- ALLOWED_RADIO_TYPES = {'RT95-P': ['V100']}
-
- @directory.register
- class CRTMicronUVvox(AnyTone778UVvoxBase):
- VENDOR = "CRT"
- MODEL = "Micron UV V2"
- # Allowed radio types is a dict keyed by model of a list of version
- # strings
- ALLOWED_RADIO_TYPES = {'MICRONP': ['V100']}
+@directory.register
+@directory.detected_by(AnyTone778UV)
+class AnyTone778UVvox(AnyTone778UVvoxBase):
+ VENDOR = "AnyTone"
+ MODEL = "778UV VOX"
+ # Allowed radio types is a dict keyed by model of a list of version
+ # strings
+ ALLOWED_RADIO_TYPES = {'778UV-P': ['V100']}
+
+
+@directory.register
+@directory.detected_by(RetevisRT95)
+class RetevisRT95vox(AnyTone778UVvoxBase):
+ VENDOR = "Retevis"
+ MODEL = "RT95 VOX"
+ # Allowed radio types is a dict keyed by model of a list of version
+ # strings
+ ALLOWED_RADIO_TYPES = {'RT95-P': ['V100']}
+
+
+@directory.register
+@directory.detected_by(CRTMicronUV)
+class CRTMicronUVvox(AnyTone778UVvoxBase):
+ VENDOR = "CRT"
+ MODEL = "Micron UV V2"
+ # Allowed radio types is a dict keyed by model of a list of version
+ # strings
+ ALLOWED_RADIO_TYPES = {'MICRONP': ['V100']}
diff --git a/chirp/drivers/btech.py b/chirp/drivers/btech.py
index 50d11b06a..d2bfa5606 100644
--- a/chirp/drivers/btech.py
+++ b/chirp/drivers/btech.py
@@ -15,8 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from builtins import bytes
-
import struct
import logging
diff --git a/chirp/drivers/ft817.py b/chirp/drivers/ft817.py
index 36ca8589b..7a020016b 100644
--- a/chirp/drivers/ft817.py
+++ b/chirp/drivers/ft817.py
@@ -16,7 +16,6 @@
"""FT817 - FT817ND - FT817ND/US management module"""
-from builtins import bytes
from chirp.drivers import yaesu_clone
from chirp import chirp_common, util, memmap, errors, directory, bitwise
from chirp.settings import RadioSetting, RadioSettingGroup, \
diff --git a/chirp/drivers/ga510.py b/chirp/drivers/ga510.py
index 58d9b050e..c84d24cdc 100644
--- a/chirp/drivers/ga510.py
+++ b/chirp/drivers/ga510.py
@@ -29,14 +29,6 @@
LOG = logging.getLogger(__name__)
-try:
- from builtins import bytes
- has_future = True
-except ImportError:
- has_future = False
- LOG.debug('python-future package is not available; '
- '%s requires it' % __name__)
-
# GA510 and SHX8800 also have DTCS code 645
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
diff --git a/chirp/drivers/icf.py b/chirp/drivers/icf.py
index 5f43d1e84..c948276bb 100644
--- a/chirp/drivers/icf.py
+++ b/chirp/drivers/icf.py
@@ -13,8 +13,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from builtins import bytes
-
import binascii
import hashlib
import os
diff --git a/chirp/drivers/id51.py b/chirp/drivers/id51.py
index 402b1dceb..a5da570f5 100644
--- a/chirp/drivers/id51.py
+++ b/chirp/drivers/id51.py
@@ -12,7 +12,6 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from builtins import bytes
import logging
diff --git a/chirp/drivers/id51plus.py b/chirp/drivers/id51plus.py
index 4aa751c8d..9e2310246 100644
--- a/chirp/drivers/id51plus.py
+++ b/chirp/drivers/id51plus.py
@@ -12,7 +12,6 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from builtins import bytes
import logging
diff --git a/chirp/drivers/leixen.py b/chirp/drivers/leixen.py
index a4628bd8f..ece9d2bd9 100644
--- a/chirp/drivers/leixen.py
+++ b/chirp/drivers/leixen.py
@@ -394,7 +394,7 @@ def detect_from_serial(cls, pipe):
_addr, _data = recv(radio)
ident = _data[8:14]
LOG.debug('Got ident from radio:\n%s' % util.hexprint(ident))
- for rclass in [cls] + cls.detected_models():
+ for rclass in cls.detected_models():
if ident == rclass._model_ident:
return rclass
# Reset the radio if we didn't find a match
diff --git a/chirp/drivers/tdh8.py b/chirp/drivers/tdh8.py
index 1cc4c090d..d69e35c04 100644
--- a/chirp/drivers/tdh8.py
+++ b/chirp/drivers/tdh8.py
@@ -852,9 +852,8 @@ class TDH8(chirp_common.CloneModeRadio):
@classmethod
def detect_from_serial(cls, pipe):
ident = _do_ident(pipe, cls._idents[0])
- for rclass in [cls] + cls.detected_models():
- if (rclass.ident_mode == ident and
- rclass.MODEL.startswith(cls.MODEL)):
+ for rclass in cls.detected_models():
+ if rclass.ident_mode == ident:
return rclass
LOG.error('No model match found for %r', ident)
raise errors.RadioError('Unsupported model')
diff --git a/chirp/drivers/th350.py b/chirp/drivers/th350.py
index a728d2f1c..22c370333 100644
--- a/chirp/drivers/th350.py
+++ b/chirp/drivers/th350.py
@@ -14,8 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division
-
import struct
import logging
from math import floor
diff --git a/chirp/drivers/tk8102.py b/chirp/drivers/tk8102.py
index 888f015ca..53d5c0a9f 100644
--- a/chirp/drivers/tk8102.py
+++ b/chirp/drivers/tk8102.py
@@ -13,7 +13,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from builtins import bytes
import logging
import struct
diff --git a/chirp/drivers/tk8180.py b/chirp/drivers/tk8180.py
index a3f7965dd..88b1f8a71 100644
--- a/chirp/drivers/tk8180.py
+++ b/chirp/drivers/tk8180.py
@@ -27,18 +27,6 @@
LOG = logging.getLogger(__name__)
-# Gross hack to handle missing future module on un-updatable
-# platforms like MacOS. Just avoid registering these radio
-# classes for now.
-try:
- from builtins import bytes
- has_future = True
-except ImportError:
- has_future = False
- LOG.debug('python-future package is not '
- 'available; %s requires it' % __name__)
-
-
HEADER_FORMAT = """
#seekto 0x0100;
struct {
@@ -1225,47 +1213,52 @@ def get_sub_devices(self):
return []
-if has_future:
- @directory.register
- class KenwoodTK7180Radio(KenwoodTKx180Radio):
- MODEL = 'TK-7180'
- VALID_BANDS = [(136000000, 174000000)]
- _model = b'M7180\x04'
-
- @directory.register
- class KenwoodTK8180Radio(KenwoodTKx180Radio):
- MODEL = 'TK-8180'
- VALID_BANDS = [(400000000, 520000000)]
- _model = b'M8180\x06'
-
- @directory.register
- class KenwoodTK2180Radio(KenwoodTKx180Radio):
- MODEL = 'TK-2180'
- VALID_BANDS = [(136000000, 174000000)]
- _model = b'P2180\x04'
-
- # K1,K3 are technically 450-470 (K3 == keypad)
- @directory.register
- class KenwoodTK3180K1Radio(KenwoodTKx180Radio):
- MODEL = 'TK-3180K'
- VALID_BANDS = [(400000000, 520000000)]
- _model = b'P3180\x06'
-
- # K2,K4 are technically 400-470 (K4 == keypad)
- @directory.register
- class KenwoodTK3180K2Radio(KenwoodTKx180Radio):
- MODEL = 'TK-3180K2'
- VALID_BANDS = [(400000000, 520000000)]
- _model = b'P3180\x07'
-
- @directory.register
- class KenwoodTK8180E(KenwoodTKx180Radio):
- MODEL = 'TK-8180E'
- VALID_BANDS = [(400000000, 520000000)]
- _model = b'M8189\''
-
- @directory.register
- class KenwoodTK7180ERadio(KenwoodTKx180Radio):
- MODEL = 'TK-7180E'
- VALID_BANDS = [(136000000, 174000000)]
- _model = b'M7189$'
+@directory.register
+class KenwoodTK7180Radio(KenwoodTKx180Radio):
+ MODEL = 'TK-7180'
+ VALID_BANDS = [(136000000, 174000000)]
+ _model = b'M7180\x04'
+
+
+@directory.register
+class KenwoodTK8180Radio(KenwoodTKx180Radio):
+ MODEL = 'TK-8180'
+ VALID_BANDS = [(400000000, 520000000)]
+ _model = b'M8180\x06'
+
+
+@directory.register
+class KenwoodTK2180Radio(KenwoodTKx180Radio):
+ MODEL = 'TK-2180'
+ VALID_BANDS = [(136000000, 174000000)]
+ _model = b'P2180\x04'
+
+
+# K1,K3 are technically 450-470 (K3 == keypad)
+@directory.register
+class KenwoodTK3180K1Radio(KenwoodTKx180Radio):
+ MODEL = 'TK-3180K'
+ VALID_BANDS = [(400000000, 520000000)]
+ _model = b'P3180\x06'
+
+
+# K2,K4 are technically 400-470 (K4 == keypad)
+@directory.register
+class KenwoodTK3180K2Radio(KenwoodTKx180Radio):
+ MODEL = 'TK-3180K2'
+ VALID_BANDS = [(400000000, 520000000)]
+ _model = b'P3180\x07'
+
+
+@directory.register
+class KenwoodTK8180E(KenwoodTKx180Radio):
+ MODEL = 'TK-8180E'
+ VALID_BANDS = [(400000000, 520000000)]
+ _model = b'M8189\''
+
+
+@directory.register
+class KenwoodTK7180ERadio(KenwoodTKx180Radio):
+ MODEL = 'TK-7180E'
+ VALID_BANDS = [(136000000, 174000000)]
+ _model = b'M7189$'
diff --git a/chirp/drivers/uv5r.py b/chirp/drivers/uv5r.py
index 993ccb4b4..94325c71b 100644
--- a/chirp/drivers/uv5r.py
+++ b/chirp/drivers/uv5r.py
@@ -13,8 +13,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from builtins import bytes
-
import struct
import time
import logging
diff --git a/chirp/drivers/uvk5.py b/chirp/drivers/uvk5.py
index b320b4b7e..29ab87da7 100644
--- a/chirp/drivers/uvk5.py
+++ b/chirp/drivers/uvk5.py
@@ -429,11 +429,13 @@ def _send_command(serport, data: bytes):
def _receive_reply(serport):
header = serport.read(4)
- if len(header) != 4:
+ if not header:
+ raise errors.RadioError("No response from radio")
+ elif len(header) != 4:
LOG.warning("Header short read: [%s] len=%i",
util.hexprint(header), len(header))
raise errors.RadioError("Header short read")
- if header[0] != 0xAB or header[1] != 0xCD or header[3] != 0x00:
+ elif header[0] != 0xAB or header[1] != 0xCD or header[3] != 0x00:
LOG.warning("Bad response header: %s len=%i",
util.hexprint(header), len(header))
raise errors.RadioError("Bad response header")
@@ -2071,7 +2073,7 @@ def k5_approve_firmware(cls, firmware):
@classmethod
def detect_from_serial(cls, pipe):
firmware = _sayhello(pipe)
- for rclass in [UVK5Radio] + cls.detected_models():
+ for rclass in cls.detected_models():
if rclass.k5_approve_firmware(firmware):
return rclass
raise errors.RadioError('Firmware %r not supported' % firmware)
diff --git a/chirp/drivers/yaesu_clone.py b/chirp/drivers/yaesu_clone.py
index a5e764392..39e417634 100644
--- a/chirp/drivers/yaesu_clone.py
+++ b/chirp/drivers/yaesu_clone.py
@@ -13,7 +13,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from builtins import bytes
import time
import logging
diff --git a/chirp/memmap.py b/chirp/memmap.py
index b392ac673..bc36071c7 100644
--- a/chirp/memmap.py
+++ b/chirp/memmap.py
@@ -13,8 +13,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from builtins import bytes
-
from chirp import util
diff --git a/chirp/wxui/clone.py b/chirp/wxui/clone.py
index e6f26b854..9591fa385 100644
--- a/chirp/wxui/clone.py
+++ b/chirp/wxui/clone.py
@@ -47,6 +47,7 @@ def get_fakes():
'Fake F7E': fake.FakeKenwoodSerial(),
'Fake UV17': fake.FakeUV17Serial(),
'Fake UV17Pro': fake.FakeUV17ProSerial(),
+ 'Fake AT778': developer.FakeAT778(),
}
@@ -242,6 +243,10 @@ def port_sort_key(port):
return key
+def model_value(rclass):
+ return ('%s %s' % (rclass.MODEL, rclass.VARIANT)).strip()
+
+
# Make this global so it sticks for a session
CUSTOM_PORTS = []
@@ -291,6 +296,11 @@ def _add_grid(label, control):
vbox.Add(grid, proportion=1,
flag=wx.TOP | wx.BOTTOM | wx.EXPAND,
border=20)
+ self.model_msg = wx.StaticText(
+ self,
+ label='',
+ style=(wx.ALIGN_CENTER_HORIZONTAL | wx.ST_NO_AUTORESIZE |
+ wx.ELLIPSIZE_END))
self.status_msg = wx.StaticText(
self, label='',
style=(wx.ALIGN_CENTER_HORIZONTAL | wx.ST_NO_AUTORESIZE |
@@ -300,6 +310,9 @@ def _add_grid(label, control):
flag=wx.EXPAND | wx.BOTTOM)
vbox.Add(self.gauge, flag=wx.EXPAND | wx.RIGHT | wx.LEFT, border=10,
proportion=0)
+ vbox.Add(self.model_msg,
+ border=5, proportion=0,
+ flag=wx.EXPAND | wx.BOTTOM)
vbox.Add(wx.StaticLine(self), flag=wx.EXPAND | wx.ALL, border=5)
vbox.Add(bs, flag=wx.ALL, border=10)
self.SetSizer(vbox)
@@ -310,7 +323,7 @@ def _add_grid(label, control):
if (not issubclass(rclass, chirp_common.CloneModeRadio) and
not issubclass(rclass, chirp_common.LiveRadio)):
continue
- if (getattr(rclass, '_DETECTED_MODEL', False) and
+ if (getattr(rclass, '_DETECTED_BY', None) and
not allow_detected_models):
continue
self._vendors[rclass.VENDOR].append(rclass)
@@ -445,9 +458,7 @@ def disable_running(self):
self.FindWindowById(wx.ID_OK).Disable()
def _persist_choices(self):
- CONF.set('last_vendor', self._vendor.GetStringSelection(), 'state')
- CONF.set('last_model', self._model.GetStringSelection(), 'state')
- CONF.set('last_port', self.get_selected_port(), 'state')
+ raise NotImplementedError()
def _selected_port(self, event):
if self._port.GetStringSelection() == CUSTOM:
@@ -464,7 +475,7 @@ def _selected_port(self, event):
self._persist_choices()
def _select_vendor(self, vendor):
- models = [('%s %s' % (x.MODEL, x.VARIANT)).strip()
+ models = [model_value(x)
for x in self._vendors[vendor]]
self._model.Set(models)
self._model.SetSelection(0)
@@ -525,6 +536,7 @@ def _selected_model(self, event):
super(ChirpDownloadDialog, self)._selected_model(event)
rclass = self.get_selected_rclass()
prompts = rclass.get_prompts()
+ self.model_msg.SetLabel('')
if prompts.experimental:
d = ChirpRadioPromptDialog(
self,
@@ -595,6 +607,9 @@ def _action(self, event):
self.fail(_('Internal driver error'))
return
+ self.model_msg.SetLabel('%s %s %s' % (
+ rclass.VENDOR, rclass.MODEL, rclass.VARIANT))
+
try:
self._radio = rclass(serial)
except Exception as e:
@@ -617,6 +632,12 @@ def _action(self, event):
self._clone_thread = CloneThread(self._radio, self, 'sync_in')
self._clone_thread.start()
+ def _persist_choices(self):
+ # On download, persist the selections from the actual UI boxes
+ CONF.set('last_vendor', self._vendor.GetStringSelection(), 'state')
+ CONF.set('last_model', self._model.GetStringSelection(), 'state')
+ CONF.set('last_port', self.get_selected_port(), 'state')
+
class ChirpUploadDialog(ChirpCloneDialog):
def __init__(self, radio, *a, **k):
@@ -624,9 +645,7 @@ def __init__(self, radio, *a, **k):
**k)
self._radio = radio
- self.select_vendor_model(
- self._radio.VENDOR,
- ('%s %s' % (self._radio.MODEL, self._radio.VARIANT)).strip())
+ self.select_vendor_model(self._radio.VENDOR, model_value(self._radio))
self.disable_model_select()
if isinstance(self._radio, chirp_common.LiveRadio):
@@ -691,3 +710,13 @@ def _action(self, event):
self._clone_thread = CloneThread(self._radio, self, 'sync_out')
self._clone_thread.start()
+
+ def _persist_choices(self):
+ # On upload, we may have a detected-only subclass, which won't be
+ # selectable normally. If so, use the detected_by instead of the
+ # actual driver
+ parent = getattr(self._radio, '_DETECTED_BY', None)
+ model = model_value(parent or self._radio)
+ CONF.set('last_vendor', self._vendor.GetStringSelection(), 'state')
+ CONF.set('last_model', model, 'state')
+ CONF.set('last_port', self.get_selected_port(), 'state')
diff --git a/chirp/wxui/developer.py b/chirp/wxui/developer.py
index 68d0a5746..04b7a2534 100644
--- a/chirp/wxui/developer.py
+++ b/chirp/wxui/developer.py
@@ -448,12 +448,47 @@ def _page_changed(self, event):
class FakeSerial(serial.SerialBase):
+ def __init__(self, *a, **k):
+ super().__init__(*a, **k)
+ self._fake_buf = bytearray()
+
+ @property
+ def in_waiting(self):
+ return len(self._fake_buf)
+
def write(self, buf):
LOG.debug('Fake serial write:\n%s' % util.hexprint(buf))
- def read(self, count):
- LOG.debug('Fake serial read %i' % count)
- return b''
+ def read(self, count=None):
+ if count is None:
+ count = len(self._fake_buf)
+ data = self._fake_buf[:count]
+ self._fake_buf = self._fake_buf[count:]
+ LOG.debug('Fake serial read %i: %s', count, util.hexprint(data))
+ return data
+
+ def flush(self):
+ LOG.debug('Fake serial flushed')
+
+
+class FakeAT778(FakeSerial):
+ def __init__(self, *a, **k):
+ super().__init__(*a, **k)
+ from chirp.drivers import anytone778uv
+ self._emulated = anytone778uv.RetevisRT95vox
+
+ def write(self, buf):
+ if buf == b'PROGRAM':
+ self._fake_buf.extend(buf + b'QX\x06')
+ elif buf == b'\x02':
+ model = list(self._emulated.ALLOWED_RADIO_TYPES.keys())[0]
+ version = self._emulated.ALLOWED_RADIO_TYPES[model][0]
+ self._fake_buf.extend(buf + b'\x49%7.7s\x00%6.6s\x06' % (
+ model.encode().ljust(7, b'\x00'),
+ version.encode().ljust(6, b'\x00')))
+ else:
+ raise Exception('Full clone not implemented')
+ super().write(buf)
class FakeEchoSerial(FakeSerial):
diff --git a/test-requirements.txt b/test-requirements.txt
index b0f855f23..07deee049 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -5,7 +5,6 @@ pytest-html
six
pep8
pyserial
-future
requests
pyyaml
pywin32; platform_system=="Windows"
diff --git a/tests/test_clone.py b/tests/test_clone.py
index ef13805f6..1723942be 100644
--- a/tests/test_clone.py
+++ b/tests/test_clone.py
@@ -1,4 +1,5 @@
import logging
+import random
import time
from unittest import mock
@@ -9,8 +10,18 @@
LOG = logging.getLogger(__name__)
+class SerialException(Exception):
+ pass
+
+
class SerialNone:
- def read(self, size):
+ def flush(self):
+ pass
+
+ def inWaiting(self):
+ return len(self.read(256))
+
+ def read(self, size=None):
return b""
def write(self, data):
@@ -31,15 +42,17 @@ def __str__(self):
class SerialError(SerialNone):
- def read(self, size):
- raise Exception("Foo")
+ def read(self, size=None):
+ raise SerialException("Foo")
def write(self, data):
- raise Exception("Bar")
+ raise SerialException("Bar")
class SerialGarbage(SerialNone):
- def read(self, size):
+ def read(self, size=None):
+ if size is None:
+ size = random.randint(0, 128)
buf = []
for i in range(0, size):
buf.append(i % 256)
@@ -47,8 +60,10 @@ def read(self, size):
class SerialShortGarbage(SerialNone):
- def read(self, size):
- return b'\x00' * (size - 1)
+ def read(self, size=None):
+ if size is None:
+ size = random.randint(0, 128)
+ return b'\x01' * (size - 1)
class TestCaseClone(base.DriverTest):
@@ -76,6 +91,18 @@ def _test_with_serial(self, serial):
# behavior on init.
LOG.info('Initializing radio with fake serial; Radio should not fail')
orig_mmap = self.parent._mmap
+
+ try:
+ cls = self.RADIO_CLASS.detect_from_serial(serial)
+ if cls and cls != self.RADIO_CLASS:
+ self.fail('Radio detection did not return self')
+ except NotImplementedError:
+ pass
+ except errors.RadioError:
+ pass
+ except SerialException:
+ pass
+
self.radio = self.RADIO_CLASS(serial)
self.radio._mmap = orig_mmap
self.radio.status_fn = lambda s: True
diff --git a/tests/unit/test_directory.py b/tests/unit/test_directory.py
index e60cb35d4..e9f169799 100644
--- a/tests/unit/test_directory.py
+++ b/tests/unit/test_directory.py
@@ -4,6 +4,7 @@
import os
import shutil
import tempfile
+from unittest import mock
import yaml
@@ -143,3 +144,50 @@ def test_uniqueness(self):
# before we add it to ensure there are no duplicates
self.assertNotIn(model['model'], directory_models[vendor])
directory_models[vendor].add(model['model'])
+
+
+class TestDetectedBy(base.BaseTest):
+ @mock.patch('chirp.directory.DRV_TO_RADIO', new={})
+ @mock.patch('chirp.directory.RADIO_TO_DRV', new={})
+ def test_detected_isolation(self):
+ @directory.register
+ class BaseRadio(chirp_common.CloneModeRadio):
+ VENDOR = 'CHIRP'
+ MODEL = 'Base'
+
+ @directory.register
+ class SubRadio1(BaseRadio):
+ MODEL = 'Sub1'
+
+ @directory.register
+ @directory.detected_by(SubRadio1)
+ class SubRadio2(SubRadio1):
+ MODEL = 'Sub2'
+
+ # BaseRadio should not think it detects the subs
+ self.assertEqual([BaseRadio], BaseRadio.detected_models())
+
+ # Sub1 detects both itself and Sub2
+ self.assertEqual([SubRadio1, SubRadio2], SubRadio1.detected_models())
+
+ # If include_self=False, Sub1 should not include itself
+ self.assertEqual([SubRadio2],
+ SubRadio1.detected_models(include_self=False))
+
+ # Sub2 does not also detect Sub1
+ self.assertEqual([SubRadio2], SubRadio2.detected_models())
+
+ @mock.patch('chirp.directory.DRV_TO_RADIO', new={})
+ @mock.patch('chirp.directory.RADIO_TO_DRV', new={})
+ def test_detected_include_self(self):
+ class BaseRadio(chirp_common.CloneModeRadio):
+ VENDOR = 'CHIRP'
+ MODEL = 'Base'
+
+ @directory.register
+ @directory.detected_by(BaseRadio)
+ class SubRadio(BaseRadio):
+ MODEL = 'Sub'
+
+ # BaseRadio should not include itself since it is not registered
+ self.assertEqual([SubRadio], BaseRadio.detected_models())
diff --git a/tox.ini b/tox.ini
index 6bcb5e1d4..cd40c7eb9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -16,7 +16,6 @@ passenv =
PIP_INDEX_URL
PIP_TRUSTED_HOST
allowlist_externals = bash
-deps = future
[testenv:style]
sitepackages = False