Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package chirp for openSUSE:Factory checked in at 2025-06-26 11:39:45 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/chirp (Old) and /work/SRC/openSUSE:Factory/.chirp.new.7067 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "chirp" Thu Jun 26 11:39:45 2025 rev:29 rq:1288585 version:20250620 Changes: -------- --- /work/SRC/openSUSE:Factory/chirp/chirp.changes 2025-06-10 09:06:39.118825298 +0200 +++ /work/SRC/openSUSE:Factory/.chirp.new.7067/chirp.changes 2025-06-26 11:40:57.453846981 +0200 @@ -1,0 +2,9 @@ +Wed Jun 25 16:39:51 UTC 2025 - Andreas Stieger <andreas.stie...@gmx.de> + +- Update to version 20250620: + * tk272g: Allow full-range entry on both models + * Add Radioddity GM-30 + * h777: Convert to using ignore bits on BCD tones + * Add ignore_bits to bcdDataType + +------------------------------------------------------------------- Old: ---- chirp-20250606.obscpio New: ---- chirp-20250620.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ chirp.spec ++++++ --- /var/tmp/diff_new_pack.ep7kIa/_old 2025-06-26 11:40:58.581893311 +0200 +++ /var/tmp/diff_new_pack.ep7kIa/_new 2025-06-26 11:40:58.581893311 +0200 @@ -19,7 +19,7 @@ %define pythons python3 Name: chirp -Version: 20250606 +Version: 20250620 Release: 0 Summary: Tool for programming amateur radio sets License: GPL-3.0-only ++++++ _service ++++++ --- /var/tmp/diff_new_pack.ep7kIa/_old 2025-06-26 11:40:58.617894790 +0200 +++ /var/tmp/diff_new_pack.ep7kIa/_new 2025-06-26 11:40:58.625895119 +0200 @@ -4,8 +4,8 @@ <param name="scm">git</param> <param name="changesgenerate">enable</param> <param name="filename">chirp</param> - <param name="versionformat">20250606</param> - <param name="revision">c61c1e464d7f7cbc74beb118e27b10d40a2aa9e0</param> + <param name="versionformat">20250620</param> + <param name="revision">e669e01f3e23c4f03e5e9499dbafae0095339047</param> </service> <service mode="manual" name="set_version"/> <service name="tar" mode="buildtime"/> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.ep7kIa/_old 2025-06-26 11:40:58.645895940 +0200 +++ /var/tmp/diff_new_pack.ep7kIa/_new 2025-06-26 11:40:58.649896104 +0200 @@ -1,7 +1,7 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/kk7ds/chirp.git</param> - <param name="changesrevision">c61c1e464d7f7cbc74beb118e27b10d40a2aa9e0</param> + <param name="changesrevision">e669e01f3e23c4f03e5e9499dbafae0095339047</param> </service> </servicedata> (No newline at EOF) ++++++ chirp-20250606.obscpio -> chirp-20250620.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20250606/chirp/bitwise.py new/chirp-20250620/chirp/bitwise.py --- old/chirp-20250606/chirp/bitwise.py 2025-06-04 01:48:48.000000000 +0200 +++ new/chirp-20250620/chirp/bitwise.py 2025-06-20 06:15:44.000000000 +0200 @@ -701,6 +701,10 @@ class bcdDataElement(DataElement): + def __init__(self, *a, **k): + super().__init__(*a, **k) + self._ignoremask = 0x00 + def __int__(self): tens, ones = self.get_value() return (tens * 10) + ones @@ -714,6 +718,9 @@ def get_bits(self, mask): return ord(self._data[self._offset]) & int(mask) + def ignore_bits(self, mask): + self._ignoremask = mask + def set_raw(self, data): if isinstance(data, int): self._data[self._offset] = data & 0xFF @@ -728,11 +735,14 @@ type(data)) def set_value(self, value): - self._data[self._offset] = int("%02i" % value, 16) + preserve = self._data[self._offset][0] & self._ignoremask + self._data[self._offset] = ( + int("%02i" % value, 16) & ~self._ignoremask) | preserve def _get_value(self, data): - a = (ord(data) & 0xF0) >> 4 - b = ord(data) & 0x0F + data = data[0] & ~self._ignoremask + a = (data & 0xF0) >> 4 + b = data & 0x0F return (a, b) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20250606/chirp/drivers/h777.py new/chirp-20250620/chirp/drivers/h777.py --- old/chirp-20250606/chirp/drivers/h777.py 2025-06-04 01:48:48.000000000 +0200 +++ new/chirp-20250620/chirp/drivers/h777.py 2025-06-20 06:15:44.000000000 +0200 @@ -98,6 +98,8 @@ "120 seconds", "150 seconds", "180 seconds", "210 seconds", "240 seconds", "270 seconds", "300 seconds"] +DTCS_FLAG = 0x80 +DTCS_REV_FLAG = 0x40 def _h777_enter_programming_mode(radio): @@ -371,27 +373,31 @@ def get_raw_memory(self, number): return repr(self._memobj.memory[number - 1]) - def _decode_tone(self, val): - val = int(val) - if val == 16665: + def _decode_tone(self, memval): + memval[1].ignore_bits(DTCS_FLAG | DTCS_REV_FLAG) + is_dtcs = memval[1].get_bits(DTCS_FLAG) + is_rev = memval[1].get_bits(DTCS_REV_FLAG) + if memval.get_raw() == b"\xFF\xFF": return '', None, None - elif val >= 12000: - return 'DTCS', val - 12000, 'R' - elif val >= 8000: - return 'DTCS', val - 8000, 'N' + elif is_dtcs: + return 'DTCS', int(memval), 'R' if is_rev else 'N' else: - return 'Tone', val / 10.0, None + return 'Tone', int(memval) / 10.0, None def _encode_tone(self, memval, mode, value, pol): + memval[1].ignore_bits(DTCS_FLAG | DTCS_REV_FLAG) if mode == '': - memval[0].set_raw(0xFF) - memval[1].set_raw(0xFF) + memval.fill_raw(b'\xFF') elif mode == 'Tone': + memval[1].clr_bits(DTCS_FLAG | DTCS_REV_FLAG) memval.set_value(int(value * 10)) elif mode == 'DTCS': - flag = 0x80 if pol == 'N' else 0xC0 + memval[1].set_bits(DTCS_FLAG) + if pol == 'R': + memval[1].set_bits(DTCS_REV_FLAG) + else: + memval[1].clr_bits(DTCS_REV_FLAG) memval.set_value(value) - memval[1].set_bits(flag) else: raise Exception("Internal error: invalid mode `%s'" % mode) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20250606/chirp/drivers/radioddity_gm30.py new/chirp-20250620/chirp/drivers/radioddity_gm30.py --- old/chirp-20250606/chirp/drivers/radioddity_gm30.py 1970-01-01 01:00:00.000000000 +0100 +++ new/chirp-20250620/chirp/drivers/radioddity_gm30.py 2025-06-20 06:15:44.000000000 +0200 @@ -0,0 +1,951 @@ +# Copyright 2025 Mike Iacovacci <asce...@linuxmail.org> +# +# 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 struct +import logging + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, RadioSettings + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +struct{ + u8 unknown02[256]; +} unknown02[16]; +struct{ + u8 bootscrmode; + u8 bsmodepad[15]; + u8 bootscreen1[10]; + u8 bs1pad[6]; + u8 bootscreen2[10]; + u8 bs2pad[6]; + u8 unused[16]; + u8 timeout; + u8 squelch; + u8 vox_level; + u8 batt_save:4, + unk_bits:2, + work_mode:1, + voice_alert:1; + u8 backlight; + u8 beep_tone:1, + auto_key_lock:1, + unk_bit_2:1, + ctcss_revert:1, + scan_type:2, + side_tone:2; + u8 unk_bit_3:1, + standby:1, + roger:1, + alarm_mode:2, + alarm_sound:1, + fm_radio:1, + unk_bit_4:1; + u8 tail_revert; + u8 tail_delay; + u8 tbst; + u8 unk_bits_5:6, + a_ch_disp:1, + b_ch_disp:1; + u8 unk_sett[30]; + u8 passw_w_ena; + u8 passw_r_ena; + u8 passw_w_val[8]; + u8 passw_r_val[8]; + u8 unk_sett_2[5]; +} settings; +struct{ + u8 unusedsettings[32]; +} unusedsettings[124]; +struct{ + u8 entry[5]; +} dtmf_list[15]; +struct{ + u8 dtmf_l_pad[5]; + u8 radio_id[5]; + u8 unk_dtmf[11]; + u8 unk_dtmf_bits:6, + press_send:1, + release_send:1; + u8 delay_time; + u8 digit_dur; + u8 inter_dur; + u8 unk_dtmf_2[28]; +} dtmf; +struct{ + u8 unkdtmf[128]; +} unuseddtmf[31]; +struct{ + u8 unk_mem[16]; +} tuning; +struct { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + u8 mem_changed; + lbcd rx_tone[2]; + lbcd tx_tone[2]; + u8 unk_mem2:1, + busy_lock:1, + ptt_id:2, + unk_mem3:1, + mode:1, + power:1, + unk_mem4:1; + u8 signal:4, + unk_mem5:2, + freq_hop:1, + scan:1; + u8 step; +} vfomemory[2]; +struct { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + u8 mem_changed; + lbcd rx_tone[2]; + lbcd tx_tone[2]; + u8 unk_mem2:1, + busy_lock:1, + ptt_id:2, + unk_mem3:1, + mode:1, + power:1, + unk_mem4:1; + u8 signal:4, + unk_mem5:2, + freq_hop:1, + scan:1; + u8 step; +} memory[250]; +struct{ + u8 unused_memch[16]; +} unused_mem[3]; +struct{ + u8 unknown17[256]; +} unknown17[16]; +struct{ + u8 unknown18[256]; +} unknown18[16]; +struct{ + u8 unknown19[256]; +} unknown19[16]; +struct{ + char name[6]; + u8 pad[5]; +} memnames[250]; +struct{ + u8 unknames[64]; +} unknownnames [21]; +struct{ + u8 unknown25[256]; +} unknown25[16]; +struct{ + u8 unknown26[256]; +} unknown26[16]; +""" +# radio defines these types +# query each type to retrieve +# the mem location it is stored +TYPE_MAP = [ + (0x02, "unknown02"), + (0x04, "settings"), + (0x06, "dtmf"), + (0x16, "memories"), + (0x17, "unknown17"), + (0x18, "unknown18"), + (0x19, "unknown19"), + (0x24, "chan_names"), + (0x25, "unknown25"), + (0x26, "unknown26") +] + +# we only write these data types +# back to the radio. The 3rd tuple +# being the offset in _memobj +WRITE_MAP = [ + (0x04, "settings", 0x1000), + (0x06, "dtmf", 0x2000), + (0x16, "memories", 0x3000), + (0x24, "chan_names", 0x7000) +] + + +def do_ack_ack(serial): + serial.write(b'\x06') + ack = serial.read(1) + if ack != b'\x06': + err = f"Error expected 06 ack got {ack}" + LOG.debug(err) + raise errors.RadioError(err) + + +def raw_send(serial, data, exlen): + serial.write(data) + return serial.read(exlen) + + +def do_read_cmd(serial, cmd, exlen): + echo_ack = len(cmd) + 1 # ack 0x57 + echo of cmd + resp = raw_send(serial, b'\x52' + cmd, exlen + echo_ack) + if resp[0] != 0x57: + raise errors.RadioError(f"Read CMD resp failed got {resp}") + if len(resp[echo_ack:]) != exlen: + raise errors.RadioError(f"Read CMD resp expect len={exlen} got {resp}") + do_ack_ack(serial) + return resp[echo_ack:] + + +def enter_prog(serial): + resp = raw_send(serial, b'PSEARCH', 8) + if len(resp) != 8: + err = ("Enter programming failed" + + f" Resp : {resp}") + raise errors.RadioError(err) + if resp[0] != 0x06: + raise errors.RadioError("Enter programming: " + + f"Radio Bad Ack: {resp}") + return resp[1:] + + +def exit_prog(serial): + try: + serial.write(b'\x06') + serial.write(b'\x06') + serial.write(b'\x00') + serial.close() + LOG.debug("Exited programming") + except Exception as e: + raise errors.RadioError(f"Error exiting programming {e}") + + +def check_ident(data): + if data == b'P13GMRS': + LOG.info(f"Radio is: {data}") + else: + err = (f"Ident returned unknown Radio: {data}") + LOG.debug(err) + raise errors.RadioError(err) + + +def do_sysinfo(serial): + resp = raw_send(serial, b'PASSSTA', 3) + if resp != b'\x50\x00\x00': + raise errors.RadioError(f"Expected 0x500000 got {resp}") + resp = raw_send(serial, b'SYSINFO', 1) + if resp != b'\x06': + raise errors.RadioError(f"ACK expected got {resp}") + LOG.debug(f"SYSINFO: {resp}") + + +def do_readconfig(serial): + # cmds start with 56, and expect 06 ack after recv ack + for addr, _len in [(0x00000a0d, 13), (0x00100a0d, 13), + (0x00200a0d, 13), (0x0000000a, 11)]: + cmd = struct.pack('>BL', 0x56, addr) + resp = raw_send(serial, cmd, _len) + if len(resp) != _len: + raise errors.RadioError(f"Expected (_len) Bytes got {resp}") + do_ack_ack(serial) + return True + + +def do_prog2(serial): + cmd = struct.pack('>LB', 0xffffffff, 0x0c) + serial.write(cmd) # no resp expected + resp = raw_send(serial, b'P13GMRS', 1) + if resp != b'\x06': + raise errors.RadioError(f"Error expected 06 ack got {resp}") + resp = raw_send(serial, b'\x02', 8) + if len(resp) != 8: + raise errors.RadioError(f"Error expected len 8 got {resp}") + do_ack_ack(serial) + + +def do_read_tlmap(serial): + """ The radio has defined types of config data. + Before reading or writing, we ask the radio where + The data type is stored in memory. The radio + moves memory locations possibly for durability + Build ephemeral map of type to location """ + tl_map = {m[0]: 0 for m in TYPE_MAP} + for i in range(1, 16): + addr = (i << 4 | 0x0f) + # ask the radio where each data type is in mem + cmd = struct.pack('>4B', 0xff, addr, 0x00, 0x01) + resp = do_read_cmd(serial, cmd, 1) + # mem location is for this prog session only + r_int = struct.unpack('>B', resp)[0] + if r_int in tl_map: + tl_map[r_int] = (i << 4) + return tl_map + + +def do_read_ranges(serial, loc, radio, status): + # read each data/config type from the (loc)ation + # obtained by tlmap query. each loc has 4 (pre)fix + # block numbers. Each block is 64 bytes + data = b'' + for i in range(16): + for pre in range(0x0, 0xc1, 0x40): + addr = struct.pack('>BBBB', pre, loc + i, 0x00, 0x40) + data += do_read_cmd(serial, addr, 0x40) + status.cur += 0x40 + radio.status_fn(status) + return data + + +def do_upload_block(serial, loc, offset, radio, status): + # write each config type to loc retrieved from tlmap + # 4 defined (pre)fixes/blocks per (loc)ation + # pre = 0x00 , 0x40 , 0x80, 0xc0 + # full command example [57][80][4d]0040 + # 57 = write , 80 prefix/block , 4d location to write + _mem = radio._memobj.get_raw() + for i in range(16): + for pre in range(0x0, 0xc1, 0x40): + x = offset + (i * 0x100) + pre + block = _mem[x:x + 0x40] + addr = struct.pack('>BBBBB', 0x57, pre, loc + i, 0x00, 0x40) + data = addr + block + ack = raw_send(serial, data, 1) + if ack != b'\x06': + err = f"Bad ack on write expect 0x06 got {ack}" + LOG.debug(err) + raise errors.RadioError(err) + status.cur += len(block) + radio.status_fn(status) + + +def do_download(radio): + data = b'' + try: + status = chirp_common.Status() + serial = radio.pipe + serial.flush() + ident = enter_prog(serial) + check_ident(ident) + do_sysinfo(serial) + do_readconfig(serial) + do_prog2(serial) + tl_map = do_read_tlmap(serial) + status.max = len(tl_map) * 0x1000 + status.msg = "Downloading..." + for t, loc in tl_map.items(): + if loc == 0: + raise errors.RadioError(f"TL Map failed {t} {loc}") + data += do_read_ranges(serial, loc, radio, status) + except Exception as e: + raise errors.RadioError(f"Error during download {e}") + finally: + exit_prog(serial) + return memmap.MemoryMapBytes(data) + + +def do_upload(radio): + try: + status = chirp_common.Status() + status.max = len(WRITE_MAP) * 0x1000 + serial = radio.pipe + serial.flush() + ident = enter_prog(serial) + check_ident(ident) + do_sysinfo(serial) + do_readconfig(serial) + do_prog2(serial) + tl_map = do_read_tlmap(serial) + for wt, label, offset in WRITE_MAP: + status.msg = f"Uploading: {label}..." + loc = tl_map[wt] + do_upload_block(serial, loc, offset, radio, status) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError(f"Error during upload {e}") + finally: + exit_prog(serial) + + +@directory.register +class RadioddityGM30(chirp_common.CloneModeRadio): + """Radioddity GM-30""" + VENDOR = "Radioddity" + MODEL = "GM-30" + BAUD_RATE = 57600 + POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.50), + chirp_common.PowerLevel("High", watts=3.00)] + VALID_MODES = ["NFM", "FM"] + _range = [(136000000, 174000000), (400000000, 470000000)] + GMRS_RPTR = [462550000, 462575000, 462600000, 462625000, 462650000, + 462675000, 462700000, 462725000] + + VALID_TONES = chirp_common.TONES + VALID_DCS = [i for i in range( + 0, 778) if '9' not in str(i) and '8' not in str(i)] + DCS_N = 0x80 + DCS_R = 0x40 + VALID_CHARSET = chirp_common.CHARSET_ASCII + VALID_DTMF = [str(i) for i in range(0, 10)] + \ + ["A", "B", "C", "D", "*", "#"] + ASCII_NUM = [str(i) for i in range(10)] + [' '] + + VALID_STEPS = [2.5, 5.0, 6.25, 10, 12.5, 20, 25, 50] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.valid_tuning_steps = self.VALID_STEPS + rf.has_name = True + rf.valid_characters = self.VALID_CHARSET + rf.valid_name_length = 6 + 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.can_odd_split = False + rf.can_delete = True + rf.valid_modes = self.VALID_MODES + rf.valid_duplexes = ["", "-", "+", "off"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = [ + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "Tone->Tone", + "->DTCS", + "DTCS->", + "DTCS->DTCS"] + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_skips = ["", "S"] + rf.valid_bands = self._range + rf.memory_bounds = (1, 250) + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_in(self): + try: + data = do_download(self) + except errors.RadioError: + raise + except Exception as e: + err = f'Error during download {e}' + LOG.error(err) + raise errors.RadioError(err) + self._mmap = data + self.process_mmap() + + def sync_out(self): + try: + do_upload(self) + except Exception as e: + err = f'Error during upload {e}' + LOG.error(err) + raise errors.RadioError(err) + + def val_or_def(self, memidx, _list): + try: + return _list[memidx] + except IndexError: + return _list[0] + + def idx_or_def(self, memitem, _list): + try: + return _list.index(memitem) + except ValueError: + return 0 + + def get_str_name(self, _name): + return ''.join(filter + (lambda x: x in self.VALID_CHARSET, + str(_name))) + + def get_memory(self, number): + mem = chirp_common.Memory() + mem.number = number + _mem = self._memobj.memory[number-1] + _name = self._memobj.memnames[number-1]["name"] + str_name = self.get_str_name(_name) + mem.name = str_name.rstrip() + + if _mem.rx_freq.get_raw() == b'\xff\xff\xff\xff': + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + + if _mem.tx_freq.get_raw() == b'\xff\xff\xff\xff': + mem.duplex = 'off' + elif int(_mem.tx_freq) - int(_mem.rx_freq) > 0: + # '+' duplex + mem.duplex = '+' + mem.offset = (int(_mem.tx_freq) - int(_mem.rx_freq)) * 10 + elif int(_mem.tx_freq) - int(_mem.rx_freq) < 0: + # '-' duplex + mem.duplex = '-' + mem.offset = (int(_mem.rx_freq) - int(_mem.tx_freq)) * 10 + + mem.mode = self.val_or_def(_mem.mode, self.VALID_MODES) + mem.power = self.val_or_def(_mem.power, self.POWER_LEVELS) + mem.skip = "" if _mem.scan else "S" + + if 1 <= mem.number <= 7 or 15 <= mem.number <= 22: + mem.duplex = "" + mem.immutable = ['duplex', 'offset', 'empty'] + elif 8 <= mem.number <= 14: + mem.duplex = "off" + mem.immutable += ['duplex', 'offset', 'empty', 'mode', 'power'] + elif 23 <= mem.number <= 54: + mem.offset = 5 * 1000000 + mem.duplex = '+' + mem.immutable = ['duplex', 'offset', 'empty'] + else: + mem.offset = 0 + mem.duplex = "off" + mem.immutable += ['offset', 'duplex'] + if 0 < mem.number <= 30: + mem.immutable = mem.immutable + ['freq'] + + txtone = self.get_tone(_mem.tx_tone) + rxtone = self.get_tone(_mem.rx_tone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSettingValueBoolean(_mem.busy_lock) + rset = RadioSetting("busy_lock", "Busy Lock", rs) + mem.extra.append(rset) + + rs = RadioSettingValueBoolean(_mem.freq_hop) + rset = RadioSetting("freq_hop", "Freq. Hop", rs) + mem.extra.append(rset) + + _current = _mem.signal if _mem.signal else 1 + rs = RadioSettingValueInteger(1, 15, current=_current) + rset = RadioSetting("signal", "DTMF ID", rs) + mem.extra.append(rset) + + options = ['Off', 'BOT', 'EOT', 'BOTH'] + rs = RadioSettingValueList(options, current_index=_mem.ptt_id) + rset = RadioSetting("ptt_id", "PTT ID", rs) + mem.extra.append(rset) + + return mem + + def validate_memory(self, mem): + if 31 <= mem.number <= 54: + if mem.freq not in self.GMRS_RPTR: + return [chirp_common.ValidationError( + 'Only GMRS repeater freq. permitted' + ' on channels 31 - 54')] + return super().validate_memory(mem) + + def set_memory(self, mem): + number = mem.number + _mem = self._memobj.memory[number-1] + _name = self._memobj.memnames[number-1] + newname = [str(c) for c in mem.name] + _name.name = "".join(newname).ljust(6, '\x00') + + if mem.empty: + _mem.set_raw(b'\xff' * 13 + b'\x06\x11\x00') + return + + _mem.rx_freq = mem.freq // 10 + + if mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) // 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) // 10 + elif mem.duplex == "": + _mem.tx_freq = _mem.rx_freq + elif mem.duplex == "off": + _mem.tx_freq.fill_raw(b'\xff') + + _mem.mode = self.idx_or_def(mem.mode, self.VALID_MODES) + _mem.power = self.idx_or_def(mem.power, self.POWER_LEVELS) + _mem.scan = False if mem.skip == "S" else True + + ((txmode, txval, txpol), + (rxmode, rxval, rxpol)) = chirp_common.split_tone_encode(mem) + self.set_tone(_mem.tx_tone, txmode, txval, txpol) + self.set_tone(_mem.rx_tone, rxmode, rxval, rxpol) + + # extra settings + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_tone(self, _memval): + _memval[1].ignore_bits(self.DCS_N | self.DCS_R) + dcsn = _memval[1].get_bits(self.DCS_N) + dcsr = _memval[1].get_bits(self.DCS_R) + rb = _memval[1].get_raw() + if rb == b'\xff' or rb == b'\x00': + return "", 0, None + elif dcsn: + pol = "R" if dcsr else "N" + return "DTCS", int(_memval), pol + else: + return "Tone", int(_memval) / 10, None + + def set_tone(self, _memval, mode, val, pol): + # sets tones in _mem from ui edit + _memval[1].ignore_bits(self.DCS_N | self.DCS_R) + if mode == "": + _memval.set_raw(b'\xff\xff') + if mode == "Tone": + _memval[1].clr_bits(self.DCS_N | self.DCS_R) + _memval.set_value(val * 10) + if mode == "DTCS": + _memval[1].set_bits(self.DCS_N) + if pol == 'R': + _memval[1].set_bits(self.DCS_R) + else: + _memval[1].clr_bits(self.DCS_R) + _memval.set_value(val) + + def get_settings(self): + _settings = self._memobj.settings + _dtmf = self._memobj.dtmf + _dtmf_list = self._memobj.dtmf_list + + gsettings = RadioSettingGroup("gsettings", "General Settings") + group = RadioSettings(gsettings) + + _options = ["Logo", "Message", "Voltage"] + rs = RadioSettingValueList(_options, + current_index=_settings.bootscrmode) + rset = RadioSetting("settings.bootscrmode", "Boot Screen Mode", rs) + gsettings.append(rset) + + _current = "".join(chr(i) for i in _settings.bootscreen1 + if chr(i) in self.VALID_CHARSET) + rs = RadioSettingValueString(minlength=0, maxlength=10, + current=_current, + charset=self.VALID_CHARSET, + mem_pad_char=' ') + rset = RadioSetting("settings.bootscreen1", + "Boot Screen 1", rs) + gsettings.append(rset) + + _current = "".join(chr(i) for i in _settings.bootscreen2 + if chr(i) in self.VALID_CHARSET) + rs = RadioSettingValueString(minlength=0, maxlength=10, + current=_current, + charset=self.VALID_CHARSET, + mem_pad_char=' ') + rset = RadioSetting("settings.bootscreen2", + "Boot Screen 2", rs) + gsettings.append(rset) + + _options = [str(i) for i in range(0, 601, 15)] + rs = RadioSettingValueList(_options, + current_index=_settings.timeout) + rset = RadioSetting("settings.timeout", "Timeout (s)", rs) + gsettings.append(rset) + + rs = RadioSettingValueInteger(minval=0, maxval=9, + current=_settings.squelch, step=1) + rset = RadioSetting("settings.squelch", "Squelch Level", rs) + gsettings.append(rset) + + rs = RadioSettingValueInteger(minval=0, maxval=9, + current=_settings.vox_level, step=1) + rset = RadioSetting("settings.vox_level", "Vox Level", rs) + gsettings.append(rset) + + rs = RadioSettingValueBoolean( + current=_settings.voice_alert, mem_vals=(0, 1)) + rset = RadioSetting("settings.voice_alert", "Voice Alert", rs) + gsettings.append(rset) + + _options = ["Freq. Mode", "Ch. Mode"] + rs = RadioSettingValueList(_options, + current_index=_settings.work_mode) + rset = RadioSetting("settings.work_mode", "Display Mode", rs) + gsettings.append(rset) + + _options = ["None", "1:1", "1:2", "1:3", "1:4"] + rs = RadioSettingValueList(_options, + current_index=_settings.batt_save) + rset = RadioSetting("settings.batt_save", "Battery Save Mode", rs) + gsettings.append(rset) + + _options = ["Bright", "1", "2", "3", "4", "5", + "6", "7", "8", "9", "10"] + rs = RadioSettingValueList(_options, + current_index=_settings.backlight) + rset = RadioSetting("settings.backlight", "Backlight", rs) + gsettings.append(rset) + + rs = RadioSettingValueBoolean( + current=_settings.auto_key_lock, mem_vals=(0, 1)) + rset = RadioSetting("settings.auto_key_lock", "Auto Key Lock", rs) + gsettings.append(rset) + + _options = ["Off", "DT-ST", "ANI-ST", "DT+ANI"] + rs = RadioSettingValueList(_options, + current_index=_settings.side_tone) + rset = RadioSetting("settings.side_tone", "DTMF Side Tone", rs) + gsettings.append(rset) + + _options = ["Time", "Carrier", "Search"] + rs = RadioSettingValueList(_options, + current_index=_settings.scan_type) + rset = RadioSetting("settings.scan_type", "Scan Type", rs) + gsettings.append(rset) + + rs = RadioSettingValueBoolean( + current=_settings.ctcss_revert, mem_vals=(0, 1)) + rset = RadioSetting("settings.ctcss_revert", "CTCSS Tail Revert", rs) + gsettings.append(rset) + + rs = RadioSettingValueBoolean( + current=_settings.beep_tone, mem_vals=(0, 1)) + rset = RadioSetting("settings.beep_tone", "Beep Tone", rs) + gsettings.append(rset) + + _options = ["On Site", "Send Sound", "Send Code"] + rs = RadioSettingValueList(_options, + current_index=_settings.alarm_mode) + rset = RadioSetting("settings.alarm_mode", "Alarm Mode", rs) + gsettings.append(rset) + + rs = RadioSettingValueBoolean( + current=_settings.fm_radio, mem_vals=(0, 1)) + rset = RadioSetting("settings.fm_radio", "FM Radio", rs) + gsettings.append(rset) + + rs = RadioSettingValueBoolean( + current=_settings.roger, mem_vals=(0, 1)) + rset = RadioSetting("settings.roger", "Roger Beep", rs) + gsettings.append(rset) + + rs = RadioSettingValueBoolean( + current=_settings.standby, mem_vals=(0, 1)) + rset = RadioSetting("settings.standby", "Dual Standby", rs) + gsettings.append(rset) + + _options = [str(i) for i in range(0, 1001, 100)] + rs = RadioSettingValueList(_options, + current_index=_settings.tail_revert) + rset = RadioSetting("settings.tail_revert", + "Repeater Tail Revert (ms)", rs) + gsettings.append(rset) + + # same options as tail_rvt + rs = RadioSettingValueList(_options, + current_index=_settings.tail_delay) + rset = RadioSetting("settings.tail_delay", + "Repeater Tail Delay (ms)", rs) + gsettings.append(rset) + + _options = ["1000", "1450", "1750", "2100"] + rs = RadioSettingValueList(_options, + current_index=_settings.tbst) + rset = RadioSetting("settings.tbst", "Tone Burst", rs) + gsettings.append(rset) + + _options = ["Name + Number", "Freq. + Number"] + rs = RadioSettingValueList(_options, + current_index=_settings.a_ch_disp) + rset = RadioSetting("settings.a_ch_disp", "A Channel Display Type", rs) + gsettings.append(rset) + + # same options as a_chan_disp + rs = RadioSettingValueList(_options, + current_index=_settings.b_ch_disp) + rset = RadioSetting("settings.b_ch_disp", "B Channel Display Type", rs) + gsettings.append(rset) + + # DTMF Menu + dtmf = RadioSettingGroup("dtmf", "DTMF") + group.append(dtmf) + + def _dtmf_decode(setting, pad_len): + _map = list(range(10)) + ['A', 'B', 'C', 'D', '*', '#'] + s = "" + for i in setting: + _i = int(i) + if _i < len(_map): + s += str(_map[_i]) + return s.ljust(pad_len) + + _current = _dtmf_decode(_dtmf.radio_id, 5) + rs = RadioSettingValueString( + minlength=5, maxlength=5, current=_current) + rset = RadioSetting("dtmf.radio_id", "Radio ID", rs) + dtmf.append(rset) + + rs = RadioSettingValueBoolean( + current=_dtmf.press_send, mem_vals=(0, 1)) + rset = RadioSetting("dtmf.press_send", "PTT Press Send", rs) + dtmf.append(rset) + + rs = RadioSettingValueBoolean( + current=_dtmf.release_send, mem_vals=(0, 1)) + rset = RadioSetting("dtmf.release_send", "PTT Release Send", rs) + dtmf.append(rset) + + _options = [str(i) for i in range(100, 1010, 50)] + rs = RadioSettingValueList(_options, + current_index=_dtmf.delay_time) + rset = RadioSetting("dtmf.delay_time", "Delay Time (ms)", rs) + dtmf.append(rset) + + _options = [str(i) for i in range(80, 2010, 10)] + rs = RadioSettingValueList(_options, + current_index=_dtmf.digit_dur) + rset = RadioSetting("dtmf.digit_dur", "Digit Duration (ms)", rs) + dtmf.append(rset) + + # uses same options as digit_dur + rs = RadioSettingValueList(_options, + current_index=_dtmf.inter_dur) + rset = RadioSetting( + "dtmf.inter_dur", "Digit Interval Duration (ms)", rs) + dtmf.append(rset) + + # DTMF Entries List + dtmflist = RadioSettingGroup("dtmflist", "DTMF List") + group.append(dtmflist) + for i in range(0, 15): # Entries # 1-15 + rs = RadioSettingValueString( + minlength=0, maxlength=5, + current=_dtmf_decode(_dtmf_list[i].entry, 5), + autopad=False, + charset=self.VALID_DTMF + [' ']) + rset = RadioSetting(f"dtmf_list[{i}].entry", f"Entry {i+1}", rs) + dtmflist.append(rset) + + # Password Menu + pro = RadioSettingGroup("protect", "Protect") + group.append(pro) + + def _ascii_num_filter(setting): + s = "" + for i in setting: + if chr(i) in self.ASCII_NUM: + s += str(chr(i)) + return s + + rs = RadioSettingValueBoolean( + current=_settings.passw_w_ena, mem_vals=(0, 1)) + rs.set_mutable(False) + rset = RadioSetting("settings.passw_w_ena", "Write Protect", rs) + pro.append(rset) + + _current = _ascii_num_filter(_settings.passw_w_val) + rs = RadioSettingValueString( + minlength=0, maxlength=8, current=_current, + charset=self.ASCII_NUM) + rs.set_mutable(False) + rset = RadioSetting("settings.passw_w_val", "Write Password", rs) + pro.append(rset) + + rs = RadioSettingValueBoolean( + current=_settings.passw_r_ena, mem_vals=(0, 1)) + rs.set_mutable(False) + rset = RadioSetting("settings.passw_r_ena", "Read Protect", rs) + pro.append(rset) + + _current = _ascii_num_filter(_settings.passw_r_val) + rs = RadioSettingValueString( + minlength=0, maxlength=8, current=_current, + charset=self.ASCII_NUM) + rs.set_mutable(False) + rset = RadioSetting("settings.passw_r_val", "Read Password", rs) + pro.append(rset) + + return group + + def _ff_pad__mem(self, obj, setting, element, charset, allow_space=False): + """ set_settings helper for 0xff padded elements + optional remove space chars + """ + _charset = [c for c in list(charset)] + if not allow_space: + _charset = [c for c in list(charset) if c != ' '] + _val = [0xff] * len(obj[setting]) + _i = 0 # offset for invalid chars + for i in range(len(obj[setting])): + if element.value[i] in _charset: + _val[i - _i] = ord(element.value[i]) + else: + _i += 1 + setattr(obj, setting, _val) + + def _dtmf_set__mem(self, obj, setting, element): + _dtmf_map = [str(i) for i in range(10)] + _dtmf_map += ['A', 'B', 'C', 'D', '*', '#'] + _val = [0xff] * len(obj[setting]) + _os = 0 # offset _mem setting idx for pad chars + for i in range(len(element.value)): + if element.value[i] in _dtmf_map: + _val[i - _os] = _dtmf_map.index(element.value[i]) + else: + _os += 1 + setattr(obj, setting, _val) + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + toks = element.get_name().split(".") + obj = self._memobj + for tok in toks[:-1]: + if '[' in tok: + t, i = tok.split("[") + i = int(i[:-1]) + obj = getattr(obj, t)[i] + else: + obj = getattr(obj, tok) + setting = toks[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("applying callback") + element.run_apply_callback() + + if element.value.get_mutable(): + if setting in ['passw_w_val', 'passw_r_val']: + self._ff_pad__mem( + obj, setting, element, self.ASCII_NUM) + elif setting in ['bootscreen1', 'bootscreen2']: + self._ff_pad__mem( + obj, setting, element, self.VALID_CHARSET, + allow_space=True) + elif setting == 'entry': + self._dtmf_set__mem( + obj, setting, element) + else: + setattr(obj, setting, element.value) + except Exception: + LOG.debug(element.get_name()) + raise diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20250606/chirp/drivers/tk760g.py new/chirp-20250620/chirp/drivers/tk760g.py --- old/chirp-20250606/chirp/drivers/tk760g.py 2025-06-04 01:48:48.000000000 +0200 +++ new/chirp-20250620/chirp/drivers/tk760g.py 2025-06-20 06:15:44.000000000 +0200 @@ -1649,7 +1649,9 @@ MODEL = "TK-272G" TYPE = b"P2720" VARIANTS = { - b"P2720\x05\xfb": (32, 136, 150, "K1"), + # NOTE: This is technically 136-150 MHz, but the radio supports + # the full range for RX at least + b"P2720\x05\xfb": (32, 136, 174, "K1"), b"P2720\x04\xfb": (32, 150, 174, "K") } Binary files old/chirp-20250606/tests/images/Radioddity_GM-30.img and new/chirp-20250620/tests/images/Radioddity_GM-30.img differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20250606/tests/unit/test_bitwise.py new/chirp-20250620/tests/unit/test_bitwise.py --- old/chirp-20250606/tests/unit/test_bitwise.py 2025-06-04 01:48:48.000000000 +0200 +++ new/chirp-20250620/tests/unit/test_bitwise.py 2025-06-20 06:15:44.000000000 +0200 @@ -248,6 +248,22 @@ def test_lbcd_array(self): self._test_def("lbcd foo[2];", "foo", b"\x12\x34", 3412) + def test_bbcd_array_leading_bits(self): + data = memmap.MemoryMapBytes(bytes(b"\xc7\x54")) + obj = bitwise.parse("bbcd foo[2];", data) + # This is what we interpret a leading 0xC000 as + self.assertEqual(int(obj.foo), 754 + 12000) + + # Ignore the top nibble and we expect just the bottom three + obj.foo[0].ignore_bits(0xF0) + self.assertEqual(int(obj.foo), 754) + + # If wet try to set something in the ignored nibble it should be + # ignored but preserved + obj.foo = 1754 + self.assertEqual(int(obj.foo), 754) + self.assertEqual(data[0][0], 0xC7) + class TestBitwiseCharTypes(BaseTest): def test_char(self): ++++++ chirp.obsinfo ++++++ --- /var/tmp/diff_new_pack.ep7kIa/_old 2025-06-26 11:40:59.293922555 +0200 +++ /var/tmp/diff_new_pack.ep7kIa/_new 2025-06-26 11:40:59.297922720 +0200 @@ -1,5 +1,5 @@ name: chirp -version: 20250606 -mtime: 1748994528 -commit: c61c1e464d7f7cbc74beb118e27b10d40a2aa9e0 +version: 20250620 +mtime: 1750392944 +commit: e669e01f3e23c4f03e5e9499dbafae0095339047