Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package chirp for openSUSE:Factory checked 
in at 2026-01-17 21:42:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/chirp (Old)
 and      /work/SRC/openSUSE:Factory/.chirp.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "chirp"

Sat Jan 17 21:42:31 2026 rev:53 rq:1327774 version:20260116

Changes:
--------
--- /work/SRC/openSUSE:Factory/chirp/chirp.changes      2026-01-12 
10:32:18.012611284 +0100
+++ /work/SRC/openSUSE:Factory/.chirp.new.1928/chirp.changes    2026-01-17 
21:43:30.711794462 +0100
@@ -1,0 +2,9 @@
+Sat Jan 17 07:47:40 UTC 2026 - Andreas Stieger <[email protected]>
+
+- Update to version 20260116:
+  * Add support for Baofeng BF-F8HP PRO firmware v0.52
+  * BTech UV-25X2_G2 lengthen timeout when downloading
+  * ha1g - Add bank support
+  * anytone779: support sql_mode when setting
+
+-------------------------------------------------------------------

Old:
----
  chirp-20260109.obscpio

New:
----
  chirp-20260116.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ chirp.spec ++++++
--- /var/tmp/diff_new_pack.bWndUQ/_old  2026-01-17 21:43:33.115892941 +0100
+++ /var/tmp/diff_new_pack.bWndUQ/_new  2026-01-17 21:43:33.131893597 +0100
@@ -20,7 +20,7 @@
 
 %define pythons python3
 Name:           chirp
-Version:        20260109
+Version:        20260116
 Release:        0
 Summary:        Tool for programming amateur radio sets
 License:        GPL-3.0-only

++++++ _service ++++++
--- /var/tmp/diff_new_pack.bWndUQ/_old  2026-01-17 21:43:33.487908180 +0100
+++ /var/tmp/diff_new_pack.bWndUQ/_new  2026-01-17 21:43:33.539910311 +0100
@@ -4,8 +4,8 @@
     <param name="scm">git</param>
     <param name="changesgenerate">enable</param>
     <param name="filename">chirp</param>
-    <param name="versionformat">20260109</param>
-    <param name="revision">6af302c5a4e015b50c99f49847331656070e23c8</param>
+    <param name="versionformat">20260116</param>
+    <param name="revision">1581ba898d1d8baad5c212caaa9b71de9c32e1eb</param>
   </service>
   <service mode="manual" name="set_version"/>
   <service name="tar" mode="buildtime"/>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.bWndUQ/_old  2026-01-17 21:43:33.723917848 +0100
+++ /var/tmp/diff_new_pack.bWndUQ/_new  2026-01-17 21:43:33.763919487 +0100
@@ -1,7 +1,7 @@
 <servicedata>
   <service name="tar_scm">
     <param name="url">https://github.com/kk7ds/chirp.git</param>
-    <param 
name="changesrevision">6af302c5a4e015b50c99f49847331656070e23c8</param>
+    <param 
name="changesrevision">1581ba898d1d8baad5c212caaa9b71de9c32e1eb</param>
   </service>
 </servicedata>
 (No newline at EOF)

++++++ chirp-20260109.obscpio -> chirp-20260116.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260109/chirp/drivers/baofeng_common.py 
new/chirp-20260116/chirp/drivers/baofeng_common.py
--- old/chirp-20260109/chirp/drivers/baofeng_common.py  2026-01-09 
06:55:35.000000000 +0100
+++ new/chirp-20260116/chirp/drivers/baofeng_common.py  2026-01-16 
00:18:03.000000000 +0100
@@ -529,7 +529,7 @@
         raw_tx = b""
         for i in range(0, 4):
             raw_tx += _mem.txfreq[i].get_raw()
-        return raw_tx == b"\xFF\xFF\xFF\xFF"
+        return raw_tx in (b"\xFF\xFF\xFF\xFF", b"\x00\x00\x00\x00")
 
     def get_memory(self, number):
         _mem = self._memobj.memory[number]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260109/chirp/drivers/baofeng_uv17Pro.py 
new/chirp-20260116/chirp/drivers/baofeng_uv17Pro.py
--- old/chirp-20260109/chirp/drivers/baofeng_uv17Pro.py 2026-01-09 
06:55:35.000000000 +0100
+++ new/chirp-20260116/chirp/drivers/baofeng_uv17Pro.py 2026-01-16 
00:18:03.000000000 +0100
@@ -254,7 +254,9 @@
     _has_pilot_tone = False
     _has_send_id_delay = False
     _has_skey1_short = False
+    _has_skey1_long = False
     _has_skey2_short = False
+    _has_skey2_long = False
     _has_skey_disable = False
     _has_zone_linking = False
     _scode_offset = 0
@@ -289,6 +291,7 @@
     _vhf2_range = (200000000, 260000000)
     _uhf_range = (400000000, 520000000)
     _uhf2_range = (350000000, 390000000)
+    AIRBANDS = [_airband]
 
     STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0]
     LIST_STEPS = ["2.5", "5.0", "6.25", "10.0", "12.5", "20.0", "25.0", "50.0"]
@@ -795,61 +798,67 @@
                             0x0A: 4,
                             0x0C: 5,
                             0x34: 6,
-                            0x08: 4,
-                            0x03: 5}
+                            0x05: 7,
+                            0x03: 8,
+                            0x35: 9}
             return key_to_index.get(int(value), 0)
 
-        def apply_Key1short(setting, obj):
-            val = str(setting.value)
-            key_to_index = {'FM': 0x07,
-                            'Scan': 0x1C,
-                            'Search': 0x1D,
-                            'Vox': 0x2D,
-                            'TX Power': 0x0A,
-                            'NOAA': 0x0C,
-                            'Zone Select': 0x34,
-                            'Flashlight': 0x08,
-                            'SOS': 0x03}
-            obj.key1short = key_to_index.get(val, 0x07)
-
-        def getKey2shortIndex(value):
-            key_to_index = {0x07: 0,
-                            0x1C: 1,
-                            0x1D: 2,
-                            0x2D: 3,
-                            0x0A: 4,
-                            0x0C: 5,
-                            0x34: 6}
-            return key_to_index.get(int(value), 0)
-
-        def apply_Key2short(setting, obj):
-            val = str(setting.value)
-            key_to_index = {'FM': 0x07,
-                            'Scan': 0x1C,
-                            'Search': 0x1D,
-                            'Vox': 0x2D,
-                            'TX Power': 0x0A,
-                            'NOAA': 0x0C,
-                            'Zone Select': 0x34}
-            obj.key2short = key_to_index.get(val, 0x07)
-
-        if self._has_skey1_short:
-            rs = RadioSetting("settings.key1short", "Skey1 Short",
-                              RadioSettingValueList(
-                                self.LIST_SKEY2_SHORT,
-                                current_index=getKey1shortIndex(
-                                        _mem.settings.key1short)))
-            rs.set_apply_callback(apply_Key1short, _mem.settings)
-            basic.append(rs)
-
-        if self._has_skey2_short:
-            rs = RadioSetting("settings.key2short", "Skey2 Short",
-                              RadioSettingValueList(
-                                self.LIST_SKEY2_SHORT,
-                                current_index=getKey2shortIndex(
-                                        _mem.settings.key2short)))
-            rs.set_apply_callback(apply_Key2short, _mem.settings)
-            basic.append(rs)
+        KEY_NAME_TO_CODE = {
+            'FM': 0x07,
+            'Scan': 0x1C,
+            'Search': 0x1D,
+            'Vox': 0x2D,
+            'TX Power': 0x0A,
+            'NOAA': 0x0C,
+            'Zone Select': 0x34,
+            'Monitor': 0x05,
+            'Alarm': 0x03,
+            'Scan Edit': 0x35,
+        }
+
+        KEY_CODE_TO_INDEX = {
+            code: idx for idx, code in enumerate(KEY_NAME_TO_CODE.values())
+        }
+
+        def make_apply_key(attr_name):
+            def apply(setting, obj):
+                val = str(setting.value)
+                setattr(obj, attr_name,
+                        KEY_NAME_TO_CODE.get(val, 0x07))
+            return apply
+
+        apply_key1short = make_apply_key("key1short")
+        apply_key1long = make_apply_key("key1long")
+        apply_key2short = make_apply_key("key2short")
+        apply_key2long = make_apply_key("key2long")
+
+        def get_key_index(value):
+            return KEY_CODE_TO_INDEX.get(int(value), 0)
+
+        KEY_SETTINGS = [
+            ("_has_skey1_short", "settings.key1short", "Skey1 Short",
+             "key1short", apply_key1short),
+            ("_has_skey1_long", "settings.key1long", "Skey1 Long",
+             "key1long", apply_key1long),
+            ("_has_skey2_short", "settings.key2short", "Skey2 Short",
+             "key2short", apply_key2short),
+            ("_has_skey2_long", "settings.key2long", "Skey2 Long",
+             "key2long", apply_key2long),
+        ]
+
+        for flag, path, label, attr, apply_cb in KEY_SETTINGS:
+            if getattr(self, flag):
+                rs = RadioSetting(
+                    path,
+                    label,
+                    RadioSettingValueList(
+                        self.LIST_SKEY2_SHORT,
+                        current_index=get_key_index(
+                            getattr(_mem.settings, attr))
+                    )
+                )
+                rs.set_apply_callback(apply_cb, _mem.settings)
+                basic.append(rs)
 
         if self._has_skey_disable:
             rs = RadioSetting("settings.skdisable", "Side Key Disable",
@@ -1263,14 +1272,16 @@
     def validate_memory(self, mem):
         msgs = []
         if 'AM' in self.MODES:
-            if chirp_common.in_range(mem.freq,
-                                     [self._airband]) and mem.mode != 'AM':
-                msgs.append(chirp_common.ValidationWarning(
-                    _('Frequency in this range requires AM mode')))
-            if not chirp_common.in_range(mem.freq,
-                                         [self._airband]) and mem.mode == 'AM':
-                msgs.append(chirp_common.ValidationWarning(
-                    _('Frequency in this range must not be AM mode')))
+            in_range = chirp_common.in_range
+            airbands = self.AIRBANDS
+
+            for band in airbands:
+                if in_range(mem.freq, [band]) and mem.mode != 'AM':
+                    msgs.append(chirp_common.ValidationWarning(
+                        _('Frequency in this range requires AM mode')))
+                if not in_range(mem.freq, [band]) and mem.mode == 'AM':
+                    msgs.append(chirp_common.ValidationWarning(
+                        _('Frequency in this range must not be AM mode')))
 
         return msgs + super().validate_memory(mem)
 
@@ -1353,7 +1364,7 @@
             mem.power = levels[0]
 
         mem.mode = _mem.wide and self.MODES[0] or self.MODES[1]
-        if chirp_common.in_range(mem.freq, [self._airband]):
+        if chirp_common.in_range(mem.freq, self.AIRBANDS):
             mem.mode = "AM"
 
         mem.extra = RadioSettingGroup("Extra", "extra")
@@ -1630,7 +1641,7 @@
 
     # ==========
     # Notice to developers:
-    # The BF-F8HP-PRO support in this driver is currently based upon v0.44
+    # The BF-F8HP-PRO support in this driver is currently based upon v0.52
     # firmware.
     # ==========
 
@@ -1646,10 +1657,14 @@
                   "100.0"]
 
     _airband = (108000000, 136999999)
-    _vhf_range = (137000000, 174000000)
+    _vhf_range = (137000000, 173999999)
+    _airband2 = (174000000, 218999999)
+    _vhf2_range = (219000000, 224999999)
+    _airband3 = (225000000, 399999999)
+    AIRBANDS = [_airband, _airband2, _airband3]
 
-    VALID_BANDS = [_airband, _vhf_range, UV17Pro._vhf2_range,
-                   UV17Pro._uhf_range, UV17Pro._uhf2_range]
+    VALID_BANDS = [_airband, _vhf_range, _airband2, _vhf2_range, _airband3,
+                   UV17Pro._uhf_range]
     POWER_LEVELS = [chirp_common.PowerLevel("High", watts=10.00),
                     chirp_common.PowerLevel("Low",  watts=1.00),
                     chirp_common.PowerLevel('Mid', watts=3.00)]
@@ -1662,10 +1677,11 @@
                             "20 sec", "30 sec", "60 sec"]
     LIST_ID_DELAY = ["%s ms" % x for x in range(100, 3100, 100)]
     LIST_SKEY2_SHORT = ["FM", "Scan", "Search", "Vox", "TX Power", "NOAA",
-                        "Zone Select"]
+                        "Zone Select", "Monitor", "Alarm", "Scan Edit"]
     MODES = UV17Pro.MODES + ['AM']
     SQUELCH_LIST = ["Off"] + list("12345678")
     LIST_SKEY_DISABLE = ["Off", "SK Only", "PTT Only", "SK + PTT"]
+    LIST_MODE = ["Name", "Frequency", "Channel Number", "Name + Frequency"]
 
     _has_support_for_banknames = True
     _vfoscan = True
@@ -1674,7 +1690,9 @@
     _has_pilot_tone = True
     _has_send_id_delay = True
     _has_skey1_short = True
+    _has_skey1_long = True
     _has_skey2_short = True
+    _has_skey2_long = True
     _has_skey_disable = True
     _has_voice = False
     _has_when_to_send_aniid = False
@@ -1749,9 +1767,10 @@
       u8 gpsw;
       u8 gpsmode;
       u8 key1short;
-      u8 unknown7;
+      u8 key1long;
       u8 key2short;
-      u8 unknown8[2];
+      u8 key2long;
+      u8 unknown8;
       u8 rstmenu;
       u8 singlewatch;
       u8 hangup;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260109/chirp/drivers/btech.py 
new/chirp-20260116/chirp/drivers/btech.py
--- old/chirp-20260109/chirp/drivers/btech.py   2026-01-09 06:55:35.000000000 
+0100
+++ new/chirp-20260116/chirp/drivers/btech.py   2026-01-16 00:18:03.000000000 
+0100
@@ -1,4 +1,4 @@
-# Copyright 2016-2023:
+# Copyright 2016-2026:
 # * Pavel Milanes CO7WT, <[email protected]>
 # * Jim Unroe KC9HI, <[email protected]>
 #
@@ -542,6 +542,9 @@
     # put radio in program mode and identify it
     _do_ident(radio, status)
 
+    # lengthen timeout here as some radios are slow causing CHIRP to time out
+    radio.pipe.timeout = 0.75
+
     # reset the progress bar in the UI Bug #11851 BLOCKSIZE fix for KT-8900D
     status.max = MEM_SIZE // radio._block_size
     status.msg = "Cloning from radio..."
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260109/chirp/drivers/radioddity_gm30.py 
new/chirp-20260116/chirp/drivers/radioddity_gm30.py
--- old/chirp-20260109/chirp/drivers/radioddity_gm30.py 2026-01-09 
06:55:35.000000000 +0100
+++ new/chirp-20260116/chirp/drivers/radioddity_gm30.py 2026-01-16 
00:18:03.000000000 +0100
@@ -60,15 +60,16 @@
     u8      tail_revert;
     u8      tail_delay;
     u8      tbst;
+    u8      unk_sett[6];
     u8      unk_bits_5:6,
-            a_ch_disp:1,
-            b_ch_disp:1;
-    u8      unk_sett[30];
+            b_ch_disp:1,
+            a_ch_disp:1;
+    u8      unk_sett_2[24];
     u8      passw_w_ena;
     u8      passw_r_ena;
     u8      passw_w_val[8];
     u8      passw_r_val[8];
-    u8      unk_sett_2[5];
+    u8      unk_sett_3[5];
 } settings;
 struct{
     u8     unusedsettings[32];
@@ -949,3 +950,147 @@
                 except Exception:
                     LOG.debug(element.get_name())
                     raise
+
+
[email protected]
+class RadioddityMU5(RadioddityGM30):
+    """Radioddity MU-5 (MURS)
+
+    Identical to Radioddity GM-30 except for the following:
+
+    MURS channels 1-20 are fixed to the 5 MURS frequencies in a repeating
+    pattern. Frequency in EEPROM is ignored for 1-20. Channels 21-250 are
+    user-programmable RX-only channels. Changing power level is not allowed
+    on 1-20. It is possible to set power level on 21-250, but it is not used
+    (radio forces RX-only).
+    """
+    VENDOR = "Radioddity"
+    MODEL = "MU-5"
+
+    # MU-5 does not actually support variable power levels
+    # MURS channels are fixed to "Low" power which is presumably 2W
+    POWER_LEVELS = [chirp_common.PowerLevel("Low",  watts=2.00),
+                    chirp_common.PowerLevel("High", watts=2.00)]
+
+    MURS_FREQS = [151820000, 151880000, 151940000, 154570000, 154600000]
+
+    def _get_murs_freq(self, channel):
+        return self.MURS_FREQS[(channel - 1) % 5]
+
+    def _is_murs_narrowband(self, channel):
+        return ((channel - 1) % 5) < 3
+
+    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()
+
+        # 1-20 always exist and may have 0xFFFFFFFF for freq
+        # other channels with 0xFFFFFFFF for freq are empty
+        if _mem.rx_freq.get_raw() == b'\xff\xff\xff\xff':
+            if not (1 <= number <= 20):
+                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:
+            mem.duplex = '+'
+            mem.offset = (int(_mem.tx_freq) - int(_mem.rx_freq)) * 10
+        elif int(_mem.tx_freq) - int(_mem.rx_freq) < 0:
+            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"
+
+        # MURS channels 1-20: fixed freq, simplex, fixed power
+        if 1 <= mem.number <= 20:
+            mem.freq = self._get_murs_freq(mem.number)
+            mem.duplex = ""
+            mem.offset = 0
+            mem.immutable = ['freq', 'duplex', 'offset', 'power', 'empty']
+            # Narrowband channels have fixed mode
+            if self._is_murs_narrowband(mem.number):
+                mem.mode = "NFM"
+                mem.immutable = mem.immutable + ['mode']
+        # Channels 21-250: RX-only user channels
+        elif mem.number > 20:
+            mem.duplex = "off"
+            mem.immutable = ['duplex', 'offset']
+
+        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 1 <= mem.number <= 20:
+            expected_freq = self._get_murs_freq(mem.number)
+            if mem.freq != expected_freq:
+                return [chirp_common.ValidationError(
+                    f'MURS Channel {mem.number} must be '
+                    f'{expected_freq / 1000000:.3f} MHz')]
+            if self._is_murs_narrowband(mem.number) and mem.mode != "NFM":
+                return [chirp_common.ValidationError(
+                    f'MURS Channel {mem.number} must be narrowband (NFM)')]
+        return chirp_common.CloneModeRadio.validate_memory(self, mem)
+
+    def set_memory(self, mem):
+        # For MURS channels 1-20, write 0xFFFFFFFF for rx/tx freq
+        # The radio firmware hardcodes the actual MURS frequency
+        # This is what the official CPS does
+        if 1 <= mem.number <= 20:
+            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.fill_raw(b'\xff')
+            _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)
+
+            for setting in mem.extra:
+                setattr(_mem, setting.get_name(), setting.value)
+        else:
+            super().set_memory(mem)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260109/chirp/drivers/retevis_ha1g.py 
new/chirp-20260116/chirp/drivers/retevis_ha1g.py
--- old/chirp-20260109/chirp/drivers/retevis_ha1g.py    2026-01-09 
06:55:35.000000000 +0100
+++ new/chirp-20260116/chirp/drivers/retevis_ha1g.py    2026-01-16 
00:18:03.000000000 +0100
@@ -140,15 +140,15 @@
 #seekto 0xc0;
 struct  {
     ul16 zonenum;
-    ul16 zoneindex[64];
+    ul16 zoneindex[16];
 } zonedata;
 
-#seekto 0x142;
+#seekto 0xe2;
 struct {
     char name[14];
     ul16 chnum;
-    ul16 chindex[16];
-} zones[64];
+    ul16 chindex[64];
+} zones[16];
 
 #seekto 0x0D42;
 struct  {
@@ -387,6 +387,106 @@
     {"name": "25kHz", "id": 25000}]
 
 
+class HA1GBank(chirp_common.NamedBank):
+
+    def get_name(self):
+        _bank = self._model._radio._memobj.zones[self.index]
+        name = "".join(filter(_bank.name, NAMECHARSET, 14))
+        return name.rstrip()
+
+    def set_name(self, name):
+        _bank = self._model._radio._memobj.zones[self.index]
+        _bank.name = str(name).ljust(14)[:14]
+
+
+class HA1GBankModel(chirp_common.BankModel):
+
+    def get_num_mappings(self):
+        return len(self.get_mappings())
+
+    def get_mappings(self):
+        banks = self._radio._memobj.zones
+        bank_mappings = []
+        for index, _bank in enumerate(banks):
+            bank = HA1GBank(self, "%i" % index, "b%i" % (index + 1))
+            bank.index = index
+            bank_mappings.append(bank)
+
+        return bank_mappings
+
+    def _get_channel_numbers_in_bank(self, bank):
+        _bank_used = self._radio._memobj.zonedata.zoneindex[bank.index]
+        if _bank_used == 0xFFFF:
+            return set()
+
+        _members = self._radio._memobj.zones[bank.index]
+        return set([int(ch) - 1 for ch in _members.chindex if ch != 0xFFFF])
+
+    def _update_bank_with_channel_numbers(self, bank, channels_in_bank):
+        _members = self._radio._memobj.zones[bank.index]
+        if len(channels_in_bank) > len(_members.chindex):
+            raise Exception("Too many entries in bank %d" % bank.index)
+
+        empty = 0
+        for index, channel_number in enumerate(sorted(channels_in_bank)):
+            _members.chindex[index] = channel_number + 1
+            empty = index + 1
+        for index in range(empty, len(_members.chindex)):
+            _members.chindex[index] = 0xFFFF
+
+        _members.chnum = len(channels_in_bank)
+
+    def _update_banknum(self):
+        # Simple approach to keep the number of banks synced to the actual
+        # bank data. Only drop trailing banks that have no channels.
+        _zonedata = self._radio._memobj.zonedata
+        _banknum = 0
+        for index, _bank in enumerate(reversed(_zonedata.zoneindex)):
+            if not _bank == 0xFFFF:
+                _banknum = len(_zonedata.zoneindex) - index
+                break
+
+        _zonedata.zonenum = _banknum
+
+    def add_memory_to_mapping(self, memory, bank):
+        channels_in_bank = self._get_channel_numbers_in_bank(bank)
+        channels_in_bank.add(memory.number)
+        self._update_bank_with_channel_numbers(bank, channels_in_bank)
+
+        # enable bank
+        self._radio._memobj.zonedata.zoneindex[bank.index] = bank.index
+        self._update_banknum()
+
+    def remove_memory_from_mapping(self, memory, bank):
+        channels_in_bank = self._get_channel_numbers_in_bank(bank)
+        try:
+            channels_in_bank.remove(memory.number)
+        except KeyError:
+            raise Exception("Memory %i is not in bank %s. Cannot remove" %
+                            (memory.number, bank))
+        self._update_bank_with_channel_numbers(bank, channels_in_bank)
+
+        if not channels_in_bank:
+            # disable bank
+            self._radio._memobj.zonedata.zoneindex[bank.index] = 0xFFFF
+            self._update_banknum()
+
+    def get_mapping_memories(self, bank):
+        memories = []
+        for channel in self._get_channel_numbers_in_bank(bank):
+            memories.append(self._radio.get_memory(channel))
+
+        return memories
+
+    def get_memory_mappings(self, memory):
+        banks = []
+        for bank in self.get_mappings():
+            if memory.number in self._get_channel_numbers_in_bank(bank):
+                banks.append(bank)
+
+        return banks
+
+
 def do_download(self):
     error_map = {
         HandshakeStatuses.RadioWrong: "Radio model mismatch",
@@ -480,7 +580,6 @@
     EXCLUDED_REGIONS = {MemoryRegions.radioHead,
                         MemoryRegions.radioInfo,
                         MemoryRegions.radioVer,
-                        MemoryRegions.zoneData,
                         MemoryRegions.scanData,
                         MemoryRegions.alarmData}
     for item in MemoryRegions:
@@ -1331,7 +1430,8 @@
         rf.has_rx_dtcs = True
         rf.has_dtcs = True
         rf.has_cross = True
-        rf.has_bank = False
+        rf.has_bank = self.supports_banks()
+        rf.has_bank_names = True
         rf.valid_bands = [self._airband, self._vhf, self._uhf]
         rf.has_tuning_step = False
         rf.has_nostep_tuning = True
@@ -1375,6 +1475,9 @@
 
         return msgs + super().validate_memory(mem)
 
+    def get_bank_model(self):
+        return HA1GBankModel(self)
+
     def process_mmap(self):
         self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
         self._dtmf_list = self.get_dtmf_item_list()
@@ -1545,7 +1648,11 @@
 
     def supports_airband(self):
         return compare_version(self.metadata.get(
-            'ha1g_firmware', '0.0.0.0'), '1.1.12.5') > 0
+            'ha1g_firmware', '0.0.0.0'), '1.1.12.5') >= 0
+
+    def supports_banks(self):
+        return compare_version(self.metadata.get(
+            'ha1g_firmware', '0.0.0.0'), '1.1.13.1') >= 0
 
 
 @directory.register
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260109/chirp/drivers/retevis_ra25.py 
new/chirp-20260116/chirp/drivers/retevis_ra25.py
--- old/chirp-20260109/chirp/drivers/retevis_ra25.py    2026-01-09 
06:55:35.000000000 +0100
+++ new/chirp-20260116/chirp/drivers/retevis_ra25.py    2026-01-16 
00:18:03.000000000 +0100
@@ -58,7 +58,8 @@
     char    name[8];
     u8      busychannellockout;
     u8      tone_id;
-    u8      unk_mem4[3];
+    u8      sql_mode;
+    u8      unk_mem4[2];
 } vfo[2];
 
 struct{
@@ -92,7 +93,8 @@
     char    name[8];
     u8      busychannellockout;
     u8      tone_id;
-    u8      unk_mem4[3];
+    u8      sql_mode;
+    u8      unk_mem4[2];
 } memory[504];
 
 struct{
@@ -645,6 +647,17 @@
         _mem.channel_width = 0 if mem.mode == "FM" else 1
         self.set_tones__mem(_mem, mem)
         self.set_duplex__mem(_mem, mem)
+
+        # Set sql_mode based on tmode - CT/DCS (1) if receive tone is set,
+        # otherwise SQ (0).
+        if mem.tmode in ("TSQL", "DTCS"):
+            _mem.sql_mode = 1
+        elif mem.tmode == "Cross":
+            txmode, rxmode = mem.cross_mode.split("->", 1)
+            _mem.sql_mode = 1 if rxmode else 0
+        else:
+            _mem.sql_mode = 0
+
         _mem.tx_offset = mem.offset/10
         _mem.step = self.VALID_TUNING_STEPS.index(mem.tuning_step)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260109/chirp/wxui/__init__.py 
new/chirp-20260116/chirp/wxui/__init__.py
--- old/chirp-20260109/chirp/wxui/__init__.py   2026-01-09 06:55:35.000000000 
+0100
+++ new/chirp-20260116/chirp/wxui/__init__.py   2026-01-16 00:18:03.000000000 
+0100
@@ -11,6 +11,7 @@
 
 LOG = logging.getLogger(__name__)
 CONF = None
+logging.captureWarnings(True)
 
 
 def developer_mode(enabled=None):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260109/chirp/wxui/serialtrace.py 
new/chirp-20260116/chirp/wxui/serialtrace.py
--- old/chirp-20260109/chirp/wxui/serialtrace.py        2026-01-09 
06:55:35.000000000 +0100
+++ new/chirp-20260116/chirp/wxui/serialtrace.py        2026-01-16 
00:18:03.000000000 +0100
@@ -14,11 +14,13 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import datetime
+import functools
 import logging
 import os
 import serial
 import tempfile
 import time
+import warnings
 
 from chirp import util
 from chirp.wxui import config
@@ -57,6 +59,26 @@
             LOG.error('Failed to remove old trace file %s: %s', fn, e)
 
 
+def warn_timeout(f):
+    @functools.wraps(f)
+    def wrapper(self, *a, **k):
+        try:
+            size = a[0]
+        except IndexError:
+            size = k.get('size', 1)
+        cps = self.baudrate / (self.stopbits +
+                               self.bytesize +
+                               (self.parity and 1 or 0))
+        required_time = size / cps
+        if self.timeout is not None and required_time > self.timeout:
+            warnings.warn(
+                ('Read of %i bytes requires %.3f seconds at %i baud, '
+                 'but timeout is %.3fs') % (
+                    size, required_time, self.baudrate, self.timeout))
+        return f(self, *a, **k)
+    return wrapper
+
+
 class SerialTrace(serial.Serial):
     def __init__(self, *a, **k):
         self.__tracef = None
@@ -90,6 +112,7 @@
                 LOG.error('Failed to write to serial trace file: %s' % e)
                 self.__tracef = None
 
+    @warn_timeout
     def read(self, size=1):
         data = super().read(size)
         if self.__tracef:
Binary files old/chirp-20260109/tests/images/Radioddity_MU-5.img and 
new/chirp-20260116/tests/images/Radioddity_MU-5.img differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260109/tests/unit/test_retevis_ra25.py 
new/chirp-20260116/tests/unit/test_retevis_ra25.py
--- old/chirp-20260109/tests/unit/test_retevis_ra25.py  1970-01-01 
01:00:00.000000000 +0100
+++ new/chirp-20260116/tests/unit/test_retevis_ra25.py  2026-01-16 
00:18:03.000000000 +0100
@@ -0,0 +1,132 @@
+import os
+import unittest
+
+from chirp import memmap
+from chirp.drivers import retevis_ra25
+
+
+class TestRA25SqlMode(unittest.TestCase):
+    """Test sql_mode (Signal/Squelch Mode) feature for RA25/779UV"""
+
+    def setUp(self):
+        test_image = os.path.join(os.path.dirname(__file__),
+                                  '..', 'images', 'Retevis_RA25.img')
+        self.radio = retevis_ra25.RA25UVRadio(None)
+        with open(test_image, 'rb') as f:
+            self.radio._mmap = memmap.MemoryMapBytes(f.read())
+        self.radio.process_mmap()
+
+    def test_sql_mode_not_in_memory_extra(self):
+        """sql_mode setting should NOT be present in mem.extra"""
+        mem = self.radio.get_memory(1)
+        setting_names = [s.get_name() for s in mem.extra]
+        self.assertNotIn('sql_mode', setting_names)
+
+    def test_sql_mode_raw_values(self):
+        """sql_mode raw values should be 0 (SQ) or 1 (CT/DCS)"""
+        _mem = self.radio._memobj.memory[0]
+
+        # Test setting to 0 (SQ)
+        _mem.sql_mode = 0
+        self.assertEqual(int(_mem.sql_mode), 0)
+
+        # Test setting to 1 (CT/DCS)
+        _mem.sql_mode = 1
+        self.assertEqual(int(_mem.sql_mode), 1)
+
+    def test_sql_mode_adjacent_fields_intact(self):
+        """Modifying sql_mode should not corrupt adjacent fields"""
+        _mem = self.radio._memobj.memory[0]
+
+        # Store original values
+        orig_tone_id = int(_mem.tone_id)
+        orig_bcl = int(_mem.busychannellockout)
+
+        # Modify sql_mode
+        _mem.sql_mode = 1
+        _mem.sql_mode = 0
+
+        # Verify adjacent fields unchanged
+        self.assertEqual(int(_mem.tone_id), orig_tone_id)
+        self.assertEqual(int(_mem.busychannellockout), orig_bcl)
+
+    def test_sql_mode_in_vfo(self):
+        """VFO struct should also have sql_mode field"""
+        for i in [0, 1]:
+            _vfo = self.radio._memobj.vfo[i]
+            # Should be accessible without error
+            _ = int(_vfo.sql_mode)
+
+    def test_sql_mode_set_from_tmode_empty(self):
+        """sql_mode should be 0 (SQ) when tmode is empty"""
+        mem = self.radio.get_memory(1)
+        mem.tmode = ""
+        self.radio.set_memory(mem)
+
+        _mem = self.radio._memobj.memory[0]
+        self.assertEqual(int(_mem.sql_mode), 0)
+
+    def test_sql_mode_set_from_tmode_tone(self):
+        """sql_mode should be 0 (SQ) when tmode is Tone (TX only)"""
+        mem = self.radio.get_memory(1)
+        mem.tmode = "Tone"
+        mem.rtone = 88.5
+        self.radio.set_memory(mem)
+
+        _mem = self.radio._memobj.memory[0]
+        self.assertEqual(int(_mem.sql_mode), 0)
+
+    def test_sql_mode_set_from_tmode_tsql(self):
+        """sql_mode should be 1 (CT/DCS) when tmode is TSQL"""
+        mem = self.radio.get_memory(1)
+        mem.tmode = "TSQL"
+        mem.ctone = 88.5
+        self.radio.set_memory(mem)
+
+        _mem = self.radio._memobj.memory[0]
+        self.assertEqual(int(_mem.sql_mode), 1)
+
+    def test_sql_mode_set_from_tmode_dtcs(self):
+        """sql_mode should be 1 (CT/DCS) when tmode is DTCS"""
+        mem = self.radio.get_memory(1)
+        mem.tmode = "DTCS"
+        mem.dtcs = 23
+        self.radio.set_memory(mem)
+
+        _mem = self.radio._memobj.memory[0]
+        self.assertEqual(int(_mem.sql_mode), 1)
+
+    def test_sql_mode_set_from_cross_with_rx_tone(self):
+        """sql_mode should be 1 (CT/DCS) when Cross mode has RX tone"""
+        mem = self.radio.get_memory(1)
+        mem.tmode = "Cross"
+        mem.cross_mode = "Tone->Tone"
+        mem.rtone = 88.5
+        mem.ctone = 100.0
+        self.radio.set_memory(mem)
+
+        _mem = self.radio._memobj.memory[0]
+        self.assertEqual(int(_mem.sql_mode), 1)
+
+    def test_sql_mode_set_from_cross_with_rx_dtcs(self):
+        """sql_mode should be 1 (CT/DCS) when Cross mode has RX DTCS"""
+        mem = self.radio.get_memory(1)
+        mem.tmode = "Cross"
+        mem.cross_mode = "Tone->DTCS"
+        mem.rtone = 88.5
+        mem.rx_dtcs = 23
+        self.radio.set_memory(mem)
+
+        _mem = self.radio._memobj.memory[0]
+        self.assertEqual(int(_mem.sql_mode), 1)
+
+    def test_sql_mode_set_from_cross_without_rx_tone(self):
+        """sql_mode should be 0 (SQ) when Cross mode has no RX tone"""
+        mem = self.radio.get_memory(1)
+        mem.tmode = "Cross"
+        mem.cross_mode = "DTCS->"
+        mem.dtcs = 23
+        self.radio.set_memory(mem)
+
+        _mem = self.radio._memobj.memory[0]
+        self.assertEqual(int(_mem.sql_mode), 0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20260109/tests/unit/test_serialtrace.py 
new/chirp-20260116/tests/unit/test_serialtrace.py
--- old/chirp-20260109/tests/unit/test_serialtrace.py   2026-01-09 
06:55:35.000000000 +0100
+++ new/chirp-20260116/tests/unit/test_serialtrace.py   2026-01-16 
00:18:03.000000000 +0100
@@ -1,5 +1,6 @@
 import unittest
 from unittest import mock
+import warnings
 
 from chirp.wxui import serialtrace
 
@@ -77,3 +78,31 @@
 
         # Before we are closed, the trace file should have been abandoned
         self.assertIsNone(trace._SerialTrace__tracef)
+
+    @mock.patch('tempfile.NamedTemporaryFile')
+    @mock.patch('serial.Serial.open')
+    @mock.patch('serial.Serial.write')
+    @mock.patch('serial.Serial.read')
+    def test_timeout_warning(self, mock_read, mock_write, mock_open, mock_tf):
+        mock_tf.return_value.writelines.side_effect = [None]
+        trace = serialtrace.SerialTrace(timeout=0.01, baudrate=9600)
+        trace.open()
+        with self.assertWarns(Warning) as cm:
+            # 2000 bytes takes about 2s, so 0.01s timeout should warn
+            trace.read(2000)
+        self.assertIn('requires', str(cm.warning))
+
+    @mock.patch('tempfile.NamedTemporaryFile')
+    @mock.patch('serial.Serial.open')
+    @mock.patch('serial.Serial.write')
+    @mock.patch('serial.Serial.read')
+    def test_timeout_no_warning(self, mock_read, mock_write, mock_open,
+                                mock_tf):
+        mock_tf.return_value.writelines.side_effect = [None]
+        trace = serialtrace.SerialTrace(timeout=2, baudrate=9600)
+        trace.open()
+        warnings.resetwarnings()
+        with warnings.catch_warnings(record=True) as w:
+            # 1000 bytes takes about 1s, so 2s timeout should yield no warning
+            trace.read(1000)
+            self.assertTrue(len(w) == 0)

++++++ chirp.obsinfo ++++++
--- /var/tmp/diff_new_pack.bWndUQ/_old  2026-01-17 21:43:36.832045167 +0100
+++ /var/tmp/diff_new_pack.bWndUQ/_new  2026-01-17 21:43:36.856046151 +0100
@@ -1,5 +1,5 @@
 name: chirp
-version: 20260109
-mtime: 1767938135
-commit: 6af302c5a4e015b50c99f49847331656070e23c8
+version: 20260116
+mtime: 1768519083
+commit: 1581ba898d1d8baad5c212caaa9b71de9c32e1eb
 

Reply via email to