diff --git a/chirp/bitwise.py b/chirp/bitwise.py
index 98720a145..b5654b697 100644
--- a/chirp/bitwise.py
+++ b/chirp/bitwise.py
@@ -322,6 +322,19 @@ def __set_value_char(self, value):
if len(value) != len(self.__items):
raise ValueError("String expects exactly %i characters, not %i" % (
len(self.__items), len(value)))
+ if not isinstance(value, bytes) and hasattr(value, '__str__'):
+ # Special case to allow ASCII strings straight through because
+ # almost anything will support them. If the string is not fully
+ # supported by ASCII, then the driver should have provided a
+ # pre-encoded bytestring.
+ warnings.warn(
+ 'String compatibility used - should pass bytes instead',
+ category=DeprecationWarning, stacklevel=4)
+ try:
+ value = str(value).encode('ascii')
+ except UnicodeEncodeError:
+ raise ValueError('Non-ASCII string set on char type: use '
+ 'pre-encoded bytestring instead')
for i in range(0, len(self.__items)):
self.__items[i].set_value(value[i])
diff --git a/chirp/drivers/boblov_x3plus.py b/chirp/drivers/boblov_x3plus.py
index 5482a4bc7..62c310590 100644
--- a/chirp/drivers/boblov_x3plus.py
+++ b/chirp/drivers/boblov_x3plus.py
@@ -51,6 +51,7 @@ class BoblovX3Plus(chirp_common.CloneModeRadio,
MODEL = 'X3Plus'
BAUD_RATE = 9600
CHANNELS = 16
+ NEEDS_COMPAT_SERIAL = False
MEM_FORMAT = """
#seekto 0x0010;
@@ -86,12 +87,12 @@ class BoblovX3Plus(chirp_common.CloneModeRadio,
"""
# Radio command data
- CMD_ACK = '\x06'
- CMD_IDENTIFY = '\x02'
- CMD_PROGRAM_ENTER = '.VKOGRAM'
- CMD_PROGRAM_EXIT = '\x62' # 'b'
- CMD_READ = 'R'
- CMD_WRITE = 'W'
+ CMD_ACK = b'\x06'
+ CMD_IDENTIFY = b'\x02'
+ CMD_PROGRAM_ENTER = b'.VKOGRAM'
+ CMD_PROGRAM_EXIT = b'\x62' # 'b'
+ CMD_READ = b'R'
+ CMD_WRITE = b'W'
BLOCK_SIZE = 0x08
@@ -177,7 +178,7 @@ def sync_in(self):
self._enter_programming_mode()
- data = ''
+ data = b''
for addr in range(0, self._memsize, self.BLOCK_SIZE):
status.cur = addr + self.BLOCK_SIZE
self.status_fn(status)
@@ -260,14 +261,13 @@ def get_memory(self, number):
mem.freq = int(rmem.rxfreq) * 10
# A blank (0 MHz) or 0xFFFFFFFF frequency is considered empty
- if mem.freq == 0 or (
- rmem.rxfreq.get_raw(asbytes=False) == '\xFF\xFF\xFF\xFF'):
+ if mem.freq == 0 or rmem.rxfreq.get_raw() == b'\xFF\xFF\xFF\xFF':
LOG.debug('empty channel %d', number)
mem.freq = 0
mem.empty = True
return mem
- if rmem.txfreq.get_raw(asbytes=False) == '\xFF\xFF\xFF\xFF':
+ if rmem.txfreq.get_raw() == b'\xFF\xFF\xFF\xFF':
mem.duplex = 'off'
mem.offset = 0
elif int(rmem.rxfreq) == int(rmem.txfreq):
@@ -303,7 +303,7 @@ def set_memory(self, memory):
rmem = self._memobj.memory[memory.number - 1]
if memory.empty:
- rmem.set_raw('\xFF' * (rmem.size() // 8))
+ rmem.set_raw(b'\xFF' * (rmem.size() // 8))
return
rmem.rxfreq = memory.freq / 10
@@ -311,7 +311,7 @@ def set_memory(self, memory):
set_txtone = True
if memory.duplex == 'off':
for i in range(0, 4):
- rmem.txfreq[i].set_raw('\xFF')
+ rmem.txfreq[i].set_raw(b'\xFF')
# If receive only then txtone value should be none
self._encode_tone(rmem.txtone, mode='', value=None, pol=None)
set_txtone = False
@@ -534,7 +534,7 @@ def _enter_programming_mode(self):
self._write(self.CMD_IDENTIFY)
ident = self._read(8)
- if not ident.startswith('SMP558'):
+ if not ident.startswith(b'SMP558'):
LOG.debug(util.hexprint(ident))
raise errors.RadioError('Radio returned unknown ID string')
diff --git a/chirp/drivers/leixen.py b/chirp/drivers/leixen.py
index 777b2be01..6cf15e71d 100644
--- a/chirp/drivers/leixen.py
+++ b/chirp/drivers/leixen.py
@@ -472,9 +472,9 @@ def _get_tone(self, mem, _mem):
(rx_tmode, rx_tone, rx_pol))
def _is_txinh(self, _mem):
- raw_tx = ""
+ raw_tx = b""
for i in range(0, 4):
- raw_tx += _mem.tx_freq[i].get_raw(asbytes=False)
+ raw_tx += _mem.tx_freq[i].get_raw()
return raw_tx == b"\xFF\xFF\xFF\xFF"
def _get_memobjs(self, number):
@@ -488,7 +488,7 @@ def get_memory(self, number):
mem = chirp_common.Memory()
mem.number = number
- if _mem.get_raw(asbytes=False)[:4] == "\xFF\xFF\xFF\xFF":
+ if _mem.get_raw()[0:4] == b"\xFF\xFF\xFF\xFF":
mem.empty = True
return mem
@@ -590,7 +590,7 @@ def set_memory(self, mem):
if mem.empty:
_mem.set_raw(b"\xFF" * 16)
return
- elif _mem.get_raw(asbytes=False) == (b"\xFF" * 16):
+ elif _mem.get_raw() == (b"\xFF" * 16):
_mem.set_raw(b"\xFF" * 8 + b"\xFF\x00\xFF\x00\xFF\xFE\xF0\xFC")
_mem.rx_freq = mem.freq / 10
diff --git a/chirp/drivers/puxing.py b/chirp/drivers/puxing.py
index 3645ad688..4a626f3ee 100644
--- a/chirp/drivers/puxing.py
+++ b/chirp/drivers/puxing.py
@@ -218,12 +218,12 @@ def get_memory(self, number):
def _is_empty():
for i in range(0, 4):
- if _mem.rx_freq[i].get_raw(asbytes=False) != "\xFF":
+ if _mem.rx_freq[i].get_raw() != b"\xFF":
return False
return True
def _is_no_tone(field):
- return field.get_raw(asbytes=False) in ["\x00\x00", "\xFF\xFF"]
+ return field.get_raw() in [b"\x00\x00", b"\xFF\xFF"]
def _get_dtcs(value):
# Upper nibble 0x80 -> DCS, 0xC0 -> Inv. DCS
@@ -238,12 +238,12 @@ def _do_dtcs(mem, txfield, rxfield):
if int(txfield) < 8000 or int(rxfield) < 8000:
raise Exception("Split tone not supported")
- if txfield[0].get_raw(asbytes=False) == "\xFF":
+ if txfield[0].get_raw() == b"\xFF":
tp, tx = "N", None
else:
tp, tx = _get_dtcs(int(txfield))
- if rxfield[0].get_raw(asbytes=False) == "\xFF":
+ if rxfield[0].get_raw() == b"\xFF":
rp, rx = "N", None
else:
rp, rx = _get_dtcs(int(rxfield))
@@ -303,7 +303,7 @@ def set_memory(self, mem):
_nam = self._memobj.names[mem.number - 1]
if mem.empty:
- wipe_memory(_mem, "\xFF")
+ wipe_memory(_mem, b"\xFF")
return
_mem.rx_freq = mem.freq / 10
@@ -316,10 +316,10 @@ def set_memory(self, mem):
_mem.skip = mem.skip != "S"
_mem.iswide = mem.mode != "NFM"
- _mem.rx_tone[0].set_raw("\xFF")
- _mem.rx_tone[1].set_raw("\xFF")
- _mem.tx_tone[0].set_raw("\xFF")
- _mem.tx_tone[1].set_raw("\xFF")
+ _mem.rx_tone[0].set_raw(b"\xFF")
+ _mem.rx_tone[1].set_raw(b"\xFF")
+ _mem.tx_tone[0].set_raw(b"\xFF")
+ _mem.tx_tone[1].set_raw(b"\xFF")
if mem.tmode == "DTCS":
_mem.tx_tone = int("%x" % int("%i" % (mem.dtcs), 16))
@@ -329,9 +329,9 @@ def set_memory(self, mem):
txm = mem.dtcs_polarity[0] == "N" and 0x80 or 0xC0
rxm = mem.dtcs_polarity[1] == "N" and 0x80 or 0xC0
_mem.tx_tone[1].set_raw(
- chr(ord(_mem.tx_tone[1].get_raw(asbytes=False)) | txm))
+ bytes([_mem.tx_tone[1].get_raw()[0] | txm]))
_mem.rx_tone[1].set_raw(
- chr(ord(_mem.rx_tone[1].get_raw(asbytes=False)) | rxm))
+ bytes([_mem.rx_tone[1].get_raw()[0] | rxm]))
elif mem.tmode:
_mem.tx_tone = int(mem.rtone * 10)
@@ -461,7 +461,7 @@ def get_memory(self, number):
mem = chirp_common.Memory()
mem.number = number
- if _mem.get_raw(asbytes=False)[0:4] == "\xff\xff\xff\xff":
+ if _mem.get_raw()[0:4] == b"\xff\xff\xff\xff":
mem.empty = True
return mem
@@ -500,7 +500,7 @@ def set_memory(self, mem):
_mem = self._memobj.memory[mem.number-1]
if mem.empty:
- _mem.set_raw("\xff" * 16)
+ _mem.set_raw(b"\xff" * 16)
return
_mem.freq = mem.freq / 10
diff --git a/chirp/drivers/tk8102.py b/chirp/drivers/tk8102.py
index 888f015ca..b15836838 100644
--- a/chirp/drivers/tk8102.py
+++ b/chirp/drivers/tk8102.py
@@ -297,7 +297,7 @@ def get_memory(self, number):
mem = chirp_common.Memory()
mem.number = number
- if _mem.get_raw(asbytes=False)[:4] == "\xFF\xFF\xFF\xFF":
+ if _mem.get_raw()[0:4] == b"\xFF\xFF\xFF\xFF":
mem.empty = True
return mem
@@ -382,7 +382,7 @@ def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
if mem.empty:
- _mem.set_raw("\xFF" * 16)
+ _mem.set_raw(b"\xFF" * 16)
return
_mem.unknown3[0] = 0x07
@@ -490,7 +490,8 @@ def set_settings(self, settings):
setting = element.get_name()
if "line" in setting:
- value = str(element.value).ljust(32, "\xFF")
+ value = bytes(str(element.value).encode('ascii'))
+ value = value.ljust(32, b"\xFF")
elif 'key' in setting:
value = int(element.value) + 4
else:
diff --git a/chirp/drivers/vx2.py b/chirp/drivers/vx2.py
index 41ee3f4a9..62f765054 100644
--- a/chirp/drivers/vx2.py
+++ b/chirp/drivers/vx2.py
@@ -265,7 +265,7 @@ def get_memory_mappings(self, memory):
def _wipe_memory(mem):
- mem.set_raw("\x00" * (mem.size() // 8))
+ mem.set_raw(b"\x00" * (mem.size() // 8))
@directory.register
diff --git a/chirp/drivers/vx3.py b/chirp/drivers/vx3.py
index 6cff08381..1951163db 100644
--- a/chirp/drivers/vx3.py
+++ b/chirp/drivers/vx3.py
@@ -342,7 +342,7 @@ def get_memory_mappings(self, memory):
def _wipe_memory(mem):
- mem.set_raw("\x00" * (mem.size() // 8))
+ mem.set_raw(b"\x00" * (mem.size() // 8))
# the following settings are set to match the defaults
# on the radio, some of these fields are unknown
mem.name = [0xFF for _i in range(0, 6)]
diff --git a/chirp/drivers/vx7.py b/chirp/drivers/vx7.py
index 01b854343..ebe827354 100644
--- a/chirp/drivers/vx7.py
+++ b/chirp/drivers/vx7.py
@@ -168,7 +168,7 @@ def get_memory_mappings(self, memory):
def _wipe_memory(mem):
- mem.set_raw("\x00" * (mem.size() // 8))
+ mem.set_raw(b"\x00" * (mem.size() // 8))
mem.unknown1 = 0x05
mem.ones = 0x03
@@ -286,7 +286,7 @@ def get_memory(self, number):
mem.power = levels[0]
for i in _mem.name:
- if i == "\xFF":
+ if i == 0xFF:
break
mem.name += CHARSET[i]
mem.name = mem.name.rstrip()
diff --git a/chirp/drivers/vx8.py b/chirp/drivers/vx8.py
index 91608472c..ee93fbffe 100644
--- a/chirp/drivers/vx8.py
+++ b/chirp/drivers/vx8.py
@@ -496,7 +496,7 @@ def get_memory_mappings(self, memory):
def _wipe_memory(mem):
- mem.set_raw("\x00" * (mem.size() // 8))
+ mem.set_raw(b"\x00" * (mem.size() // 8))
@directory.register
@@ -629,7 +629,7 @@ def _checksums(self):
@staticmethod
def _add_ff_pad(val, length):
- return val.ljust(length, "\xFF")[:length]
+ return val.ljust(length, b"\xFF")[:length]
@classmethod
def _strip_ff_pads(cls, messages):
@@ -720,7 +720,7 @@ def set_memory(self, mem):
_mem.power = 0
label = "".join([chr(CHARSET.index(x)) for x in mem.name.rstrip()])
- _mem.label = self._add_ff_pad(label, 16)
+ _mem.label = self._add_ff_pad(label.encode('ascii'), 16)
# We only speak English here in chirpville
_mem.charsetbits[0] = 0x00
_mem.charsetbits[1] = 0x00
@@ -1338,7 +1338,7 @@ def _apply_callsign(cls, callsign, obj, default_ssid=None):
ssid = int(ssid) % 16
except ValueError:
ssid = default_ssid
- setattr(obj, "callsign", cls._add_ff_pad(callsign, 6))
+ setattr(obj, "callsign", cls._add_ff_pad(callsign.encode('ascii'), 6))
if ssid is not None:
setattr(obj, "ssid", ssid)
@@ -1380,8 +1380,8 @@ def apply_digi_path(self, setting, obj):
def apply_ff_padded_string(cls, setting, obj):
# FF pad.
val = setting.value.get_value()
- max_len = getattr(obj, "padded_string").size() / 8
- val = str(val).rstrip()
+ max_len = getattr(obj, "padded_string").size() // 8
+ val = str(val).rstrip().encode('ascii')
setattr(obj, "padded_string", cls._add_ff_pad(val, max_len))
@classmethod
@@ -1440,12 +1440,9 @@ def set_settings(self, settings):
def apply_ff_padded_yaesu(cls, setting, obj):
# FF pad yaesus custom string format.
rawval = setting.value.get_value()
- max_len = getattr(obj, "padded_yaesu").size() / 8
- rawval = str(rawval).rstrip()
- val = [CHARSET.index(x) for x in rawval]
- for x in range(len(val), max_len):
- val.append(0xFF)
- obj.padded_yaesu = val
+ max_len = getattr(obj, "padded_yaesu").size() // 8
+ rawval = str(rawval).rstrip().translate(CHARSET).encode('ascii')
+ obj.padded_yaesu = list(rawval.ljust(max_len, b'\xFF')[0:max_len])
def apply_volume(cls, setting, vfo):
val = setting.value.get_value()
diff --git a/tests/Python3_Driver_Testing.md b/tests/Python3_Driver_Testing.md
index 6e79c32d5..d7562d08f 100644
--- a/tests/Python3_Driver_Testing.md
+++ b/tests/Python3_Driver_Testing.md
@@ -73,7 +73,7 @@
| Baojie_BJ-318 | [@KC9HI](https://github.com/KC9HI) | 2-Jan-2023 | Yes | 0.10% |
| Baojie_BJ-9900 | | | | 0.02% |
| Baojie_BJ-UV55 | | | Yes | 0.03% |
-| Boblov_X3Plus | | | | 0.02% |
+| Boblov_X3Plus | | | Yes | 0.02% |
| CRT_Micron_UV | [Implied by Retevis_RT95](#user-content-Retevis_RT95) | 13-Nov-2022 | Yes | 0.08% |
| CRT_Micron_UV_V2 | [Implied by Retevis_RT95](#user-content-Retevis_RT95) | 13-Nov-2022 | Yes | 0.10% |
| Cignus_XTR-5 | [Implied by Radioddity_GS-5B](#user-content-Radioddity_GS-5B) | 17-Mar-2023 | Yes | |
@@ -429,7 +429,7 @@
**Tested:** 87% (369/53) (93% of usage stats)
-**Byte clean:** 90% (382/40)
+**Byte clean:** 90% (383/39)
## Meaning of this testing