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