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
 

Reply via email to