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-08-09 19:59:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/chirp (Old)
 and      /work/SRC/openSUSE:Factory/.chirp.new.1085 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "chirp"

Sat Aug  9 19:59:35 2025 rev:33 rq:1298456 version:20250808

Changes:
--------
--- /work/SRC/openSUSE:Factory/chirp/chirp.changes      2025-08-03 
13:37:59.800909433 +0200
+++ /work/SRC/openSUSE:Factory/.chirp.new.1085/chirp.changes    2025-08-09 
20:06:00.205739463 +0200
@@ -1,0 +2,8 @@
+Fri Aug  8 20:59:20 UTC 2025 - Andreas Stieger <andreas.stie...@gmx.de>
+
+- Update to version 20250808:
+  * Add Support for Retevis H777 V4.0
+  * uvk5: Fix band limits
+  * Remove legacy radio detection module
+
+-------------------------------------------------------------------

Old:
----
  chirp-20250801.obscpio

New:
----
  chirp-20250808.obscpio

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

Other differences:
------------------
++++++ chirp.spec ++++++
--- /var/tmp/diff_new_pack.dX0eLa/_old  2025-08-09 20:06:01.309785348 +0200
+++ /var/tmp/diff_new_pack.dX0eLa/_new  2025-08-09 20:06:01.313785514 +0200
@@ -19,7 +19,7 @@
 
 %define pythons python3
 Name:           chirp
-Version:        20250801
+Version:        20250808
 Release:        0
 Summary:        Tool for programming amateur radio sets
 License:        GPL-3.0-only

++++++ _service ++++++
--- /var/tmp/diff_new_pack.dX0eLa/_old  2025-08-09 20:06:01.373788008 +0200
+++ /var/tmp/diff_new_pack.dX0eLa/_new  2025-08-09 20:06:01.381788340 +0200
@@ -4,8 +4,8 @@
     <param name="scm">git</param>
     <param name="changesgenerate">enable</param>
     <param name="filename">chirp</param>
-    <param name="versionformat">20250801</param>
-    <param name="revision">477a28489d173e582f859e6f7242bbbeb840fd60</param>
+    <param name="versionformat">20250808</param>
+    <param name="revision">600a65c103b6c44c1d0d28f5a98d3025aebacd36</param>
   </service>
   <service mode="manual" name="set_version"/>
   <service name="tar" mode="buildtime"/>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.dX0eLa/_old  2025-08-09 20:06:01.421790003 +0200
+++ /var/tmp/diff_new_pack.dX0eLa/_new  2025-08-09 20:06:01.425790169 +0200
@@ -1,7 +1,7 @@
 <servicedata>
   <service name="tar_scm">
     <param name="url">https://github.com/kk7ds/chirp.git</param>
-    <param 
name="changesrevision">477a28489d173e582f859e6f7242bbbeb840fd60</param>
+    <param 
name="changesrevision">600a65c103b6c44c1d0d28f5a98d3025aebacd36</param>
   </service>
 </servicedata>
 (No newline at EOF)

++++++ chirp-20250801.obscpio -> chirp-20250808.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20250801/chirp/detect.py 
new/chirp-20250808/chirp/detect.py
--- old/chirp-20250801/chirp/detect.py  2025-08-01 00:30:55.000000000 +0200
+++ new/chirp-20250808/chirp/detect.py  1970-01-01 01:00:00.000000000 +0100
@@ -1,127 +0,0 @@
-# Copyright 2010 Dan Smith <dsm...@danplanet.com>
-#
-# 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 3 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 serial
-import logging
-
-from chirp import chirp_common, errors, directory
-from chirp.drivers import ic9x_ll, icf, kenwood_live, icomciv
-
-LOG = logging.getLogger(__name__)
-
-
-class DetectorRadio(chirp_common.Radio):
-    """Minimal radio for model detection"""
-
-    def get_payload(self, data, raw, checksum):
-        return data
-
-
-def _icom_model_data_to_rclass(md):
-    for _rtype, rclass in list(directory.DRV_TO_RADIO.items()):
-        if rclass.VENDOR != "Icom":
-            continue
-        if not hasattr(rclass, 'get_model') or not rclass.get_model():
-            continue
-        if rclass.get_model()[:4] == md[:4]:
-            return rclass
-
-    raise errors.RadioError("Unknown radio type %02x%02x%02x%02x" %
-                            (ord(md[0]), ord(md[1]), ord(md[2]), ord(md[3])))
-
-
-def _detect_icom_radio(ser):
-    # ICOM VHF/UHF Clone-type radios @ 9600 baud
-
-    try:
-        ser.baudrate = 9600
-        md = icf.get_model_data(DetectorRadio(ser))
-        return _icom_model_data_to_rclass(md)
-    except errors.RadioError as e:
-        LOG.error("_detect_icom_radio: %s", e)
-
-    # ICOM IC-91/92 Live-mode radios @ 4800/38400 baud
-
-    ser.baudrate = 4800
-    try:
-        ic9x_ll.send_magic(ser)
-        return _icom_model_data_to_rclass("ic9x")
-    except errors.RadioError:
-        pass
-
-    # ICOM CI/V Radios @ various bauds
-
-    for rate in [9600, 4800, 19200]:
-        try:
-            ser.baudrate = rate
-            return icomciv.probe_model(ser)
-        except errors.RadioError:
-            pass
-
-    ser.close()
-
-    raise errors.RadioError("Unable to get radio model")
-
-
-def detect_icom_radio(port):
-    """Detect which Icom model is connected to @port"""
-    if '://' in port:
-        ser = serial.serial_for_url(port, do_not_open=True)
-        ser.timeout = 0.5
-        ser.open()
-    else:
-        ser = serial.Serial(port=port, timeout=0.5)
-
-    try:
-        result = _detect_icom_radio(ser)
-    except Exception:
-        ser.close()
-        raise
-
-    ser.close()
-
-    LOG.info("Auto-detected %s %s on %s" %
-             (result.VENDOR, result.MODEL, port))
-
-    return result
-
-
-def detect_kenwoodlive_radio(port):
-    """Detect which Kenwood model is connected to @port"""
-    if '://' in port:
-        ser = serial.serial_for_url(port, do_not_open=True)
-        ser.timeout = 0.5
-        ser.open()
-    else:
-        ser = serial.Serial(port=port, baudrate=9600, timeout=0.5)
-
-    r_id = kenwood_live.get_id(ser)
-    ser.close()
-
-    models = {}
-    for rclass in list(directory.DRV_TO_RADIO.values()):
-        if rclass.VENDOR == "Kenwood":
-            models[rclass.MODEL] = rclass
-
-    if r_id in list(models.keys()):
-        return models[r_id]
-    else:
-        raise errors.RadioError("Unsupported model `%s'" % r_id)
-
-
-DETECT_FUNCTIONS = {
-    "Icom":    detect_icom_radio,
-    "Kenwood": detect_kenwoodlive_radio,
-}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20250801/chirp/drivers/h777.py 
new/chirp-20250808/chirp/drivers/h777.py
--- old/chirp-20250801/chirp/drivers/h777.py    2025-08-01 00:30:55.000000000 
+0200
+++ new/chirp-20250808/chirp/drivers/h777.py    2025-08-06 23:43:59.000000000 
+0200
@@ -102,22 +102,24 @@
 DTCS_REV_FLAG = 0x40
 
 
-def _h777_enter_programming_mode(radio):
-    serial = radio.pipe
+def _h777_enter_programming_mode(serial, radio_cls):
     # increase default timeout from .25 to .5 for all serial communications
     serial.timeout = 0.5
 
     try:
         serial.write(b"\x02")
         time.sleep(0.1)
-        serial.write(radio.PROGRAM_CMD)
+        serial.write(radio_cls.PROGRAM_CMD)
         ack = serial.read(1)
-    except:
+    except Exception as e:
+        LOG.warning('Failed to send program command: %s', e)
         raise errors.RadioError("Error communicating with radio")
 
     if not ack:
-        raise errors.RadioError("No response from radio")
+        raise errors.RadioError("No response from radio to program command")
     elif ack != CMD_ACK:
+        LOG.warning('Ack from program command was %r, expected %r',
+                    ack, CMD_ACK)
         raise errors.RadioError("Radio refused to enter programming mode")
 
     try:
@@ -127,37 +129,29 @@
         # version data and the last three bytes. We need to raise the
         # timeout so that the read doesn't finish early.
         ident = serial.read(8)
-    except:
+    except Exception as e:
+        LOG.warning('Failed to read ident: %s', e)
         raise errors.RadioError("Error communicating with radio")
 
-    # check if ident is OK
-    itis = False
-    for fp in radio.IDENT:
-        if fp in ident:
-            # got it!
-            itis = True
-
-            break
-
-    if itis is False:
-        LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident))
-        raise errors.RadioError("Radio identification failed.")
+    if ident:
+        LOG.info('Radio identified with:\n%s', util.hexprint(ident))
+        try:
+            serial.write(CMD_ACK)
+            ack = serial.read(1)
+            if ack != CMD_ACK:
+                raise errors.RadioError("Bad ACK after reading ident")
+        except:
+            raise errors.RadioError('No ACK after reading ident')
+        return ident
 
-    try:
-        serial.write(CMD_ACK)
-        ack = serial.read(1)
-    except:
-        raise errors.RadioError("Error communicating with radio")
-
-    if ack != CMD_ACK:
-        raise errors.RadioError("Radio refused to enter programming mode")
+    raise errors.RadioError('No identification received from radio')
 
 
-def _h777_exit_programming_mode(radio):
-    serial = radio.pipe
+def _h777_exit_programming_mode(serial):
     try:
         serial.write(b"E")
-    except:
+    except Exception as e:
+        LOG.warning('Failed to send exit command: %s', e)
         raise errors.RadioError("Radio refused to exit programming mode")
 
 
@@ -172,6 +166,8 @@
         serial.write(cmd)
         response = serial.read(4 + BLOCK_SIZE)
         if response[:4] != expectedresponse:
+            LOG.warning('Got %r expected %r' % (response[:4],
+                                                expectedresponse))
             raise Exception("Error reading block %04x." % (block_addr))
 
         block_data = response[4:]
@@ -208,9 +204,26 @@
                                 "to radio at %04x" % block_addr)
 
 
+def _h777_enter_single_programming_mode(radio):
+    ident = _h777_enter_programming_mode(radio.pipe, radio.__class__)
+    if not ident:
+        raise errors.RadioError('Radio did not identify')
+    if not any(rc_ident in ident for rc_ident in radio.IDENT):
+        LOG.warning('Expected %s for %s but got:\n%s',
+                    radio.IDENT, radio.__class__.__name__,
+                    util.hexprint(ident))
+        raise errors.RadioError('Incorrect model')
+
+
 def do_download(radio):
     LOG.debug("download")
-    _h777_enter_programming_mode(radio)
+
+    if len(radio.detected_models()) <= 1:
+        LOG.debug('Entering programming mode for %s', radio.__class__.__name__)
+        _h777_enter_single_programming_mode(radio)
+    else:
+        LOG.debug('Already in programming mode for %s',
+                  radio.__class__.__name__)
 
     data = b""
 
@@ -230,7 +243,7 @@
         LOG.debug("Address: %04x" % addr)
         LOG.debug(util.hexprint(block))
 
-    _h777_exit_programming_mode(radio)
+    _h777_exit_programming_mode(radio.pipe)
 
     return memmap.MemoryMapBytes(data)
 
@@ -239,7 +252,7 @@
     status = chirp_common.Status()
     status.msg = "Uploading to radio"
 
-    _h777_enter_programming_mode(radio)
+    _h777_enter_single_programming_mode(radio)
 
     status.cur = 0
     status.max = radio._memsize
@@ -250,7 +263,7 @@
             radio.status_fn(status)
             _h777_write_block(radio, addr, BLOCK_SIZE)
 
-    _h777_exit_programming_mode(radio)
+    _h777_exit_programming_mode(radio.pipe)
 
 
 class ArcshellAR5(chirp_common.Alias):
@@ -283,11 +296,6 @@
     MODEL = 'TW-325'
 
 
-class RetevisH777Alias(chirp_common.Alias):
-    VENDOR = 'Retevis'
-    MODEL = 'H777'
-
-
 @directory.register
 class H777Radio(chirp_common.CloneModeRadio):
     """HST H-777"""
@@ -305,7 +313,7 @@
     VALID_BANDS = (400000000, 490000000)
     MAX_VOXLEVEL = 5
     ALIASES = [ArcshellAR5, ArcshellAR6, GV8SAlias, GV9SAlias, A8SAlias,
-               TenwayTW325Alias, RetevisH777Alias]
+               TenwayTW325Alias]
     SIDEKEYFUNCTION_LIST = ["Off", "Monitor", "Transmit Power", "Alarm"]
     SCANMODE_LIST = ["Carrier", "Time"]
 
@@ -620,6 +628,37 @@
                     raise
 
 
+@directory.register
+class RetevisH777(H777Radio):
+    VENDOR = 'Retevis'
+    MODEL = 'H777'
+    ALIASES = []
+
+    @classmethod
+    def detect_from_serial(cls, pipe):
+        cls_to_ident = {rc: rc.IDENT for rc in cls.detected_models()}
+        for ident_rclass in reversed(cls.detected_models()):
+            ident = _h777_enter_programming_mode(pipe, ident_rclass)
+            for rclass, idents in cls_to_ident.items():
+                if any(rc_ident in ident for rc_ident in idents):
+                    LOG.debug('Detected %s', rclass.__name__)
+                    return rclass
+                LOG.debug('Ident was %r, idents were %r', ident, idents)
+
+            LOG.info('Did not identify radio as %s', rclass.__name__)
+            time.sleep(0.5)
+            _h777_exit_programming_mode(pipe)
+            time.sleep(1)
+
+        raise errors.RadioError('Failed to identify with radio.')
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        # This radio has always been post-metadata, so never do
+        # old-school detection
+        return False
+
+
 class H777TestCase(unittest.TestCase):
 
     def setUp(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20250801/chirp/drivers/retevis_h777v4.py 
new/chirp-20250808/chirp/drivers/retevis_h777v4.py
--- old/chirp-20250801/chirp/drivers/retevis_h777v4.py  1970-01-01 
01:00:00.000000000 +0100
+++ new/chirp-20250808/chirp/drivers/retevis_h777v4.py  2025-08-06 
23:43:59.000000000 +0200
@@ -0,0 +1,505 @@
+# Copyright 2025 Jim Unroe <rock.un...@gmail.com>
+#
+# 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 logging
+import struct
+
+from chirp import (
+    bitwise,
+    chirp_common,
+    directory,
+    errors,
+    memmap,
+    util,
+)
+from chirp.drivers import h777
+from chirp.settings import (
+    MemSetting,
+    RadioSettingGroup,
+    RadioSettings,
+    RadioSettingValueBoolean,
+    RadioSettingValueInvertedBoolean,
+    RadioSettingValueInteger,
+    RadioSettingValueList,
+)
+
+LOG = logging.getLogger(__name__)
+
+MEM_FORMAT = """
+struct {
+  ul32 rxfreq;        // 0-3
+  ul32 txfreq;        // 4-7
+  lbcd rx_tone[2];    // 8-9
+  lbcd tx_tone[2];    // A-B
+  u8 unknown0:2,      // C
+     scramb:1,        //     Scramble  0 = Off, 1 = On
+     scanadd:1,       //     Scan Add  0 = Scan, 1 = Skip
+     ishighpower:1,   //     Power Level  0 = Low, 1 = High
+     narrow:1,        //     Bandwidth  0 = Wide, 1 = Narrow
+     unknown1:1,      //
+     bcl:1;           //     Busy Channel Locklut  0 = On, 1 = Off
+} memory[16];
+
+struct {
+  u8 codesw:1,        // 00D0  Code Switch
+     scanm:1,         //       Scan Mode
+     voxs:1,          //       VOX Switch
+     roger:1,         //       Roger
+     voice:1,         //       Voice Annunciation
+     unknown_0:1,
+     save:1,          //       Battery Save
+     beep:1;          //       Beep Tone
+  u8 squelch;         // 00D1  Squelch Level
+  u8 tot;             // 00D2  Time-out Timer
+  u8 vox;             // 00D3  VOX Level
+  u8 voxd;            // 00D4  Vox Delay
+  u8 unknown_1;
+  u8 unknown_2;
+  u8 skey2;           // 00D7  Side Key 2 (long)
+  u8 unknown_3[5];    // 00D8 - 00DC
+  u8 password[6];     // 00DD - 00E2  Password
+} settings;
+"""
+
+
+CMD_ACK = b"\x06"
+
+DTCS = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
+
+OFF1TO9_LIST = ["Off"] + ["%s" % x for x in range(1, 10)]
+SCANM_LIST = ["Carrier", "Time"]
+SKEY2_LIST = ["Off", "VOX", "Power", "Scan"]
+TIMEOUTTIMER_LIST = ["Off"] + ["%s seconds" % x for x in range(30, 210, 30)]
+VOICE_LIST = ["Off", "English"]
+VOXD_LIST = ["0.5", "1.0", "1.5", "2.0", "2.5", "3.0"]
+
+
+def _read_block(radio, block_addr, block_size):
+    serial = radio.pipe
+
+    cmd = struct.pack(">cHb", b'R', block_addr, block_size)
+    expectedresponse = b"W" + cmd[1:]
+    LOG.debug("Reading block %04x..." % (block_addr))
+
+    try:
+        serial.write(cmd)
+        response = serial.read(4 + block_size)
+        if response[:4] != expectedresponse:
+            raise Exception("Error reading block %04x." % (block_addr))
+
+        block_data = response[4:]
+    except Exception:
+        raise errors.RadioError("Failed to read block at %04x" % block_addr)
+
+    return block_data
+
+
+def _write_block(radio, block_addr, block_size):
+    serial = radio.pipe
+
+    cmd = struct.pack(">cHb", b'W', block_addr, block_size)
+    data = radio.get_mmap()[block_addr:block_addr + block_size]
+
+    LOG.debug("Writing Data:")
+    LOG.debug(util.hexprint(cmd + data))
+
+    try:
+        serial.write(cmd + data)
+        if serial.read(1) != CMD_ACK:
+            raise Exception("No ACK")
+    except Exception:
+        raise errors.RadioError("Failed to send block "
+                                "to radio at %04x" % block_addr)
+
+
+def do_download(radio):
+    LOG.debug("download")
+
+    data = b""
+
+    status = chirp_common.Status()
+    status.msg = "Cloning from radio"
+
+    status.cur = 0
+    status.max = radio._memsize
+
+    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
+        status.cur = addr + radio.BLOCK_SIZE
+        radio.status_fn(status)
+
+        block = _read_block(radio, addr, radio.BLOCK_SIZE)
+        data += block
+
+        LOG.debug("Address: %04x" % addr)
+        LOG.debug(util.hexprint(block))
+
+    return memmap.MemoryMapBytes(data)
+
+
+def do_upload(radio):
+    status = chirp_common.Status()
+    status.msg = "Uploading to radio"
+
+    h777._h777_enter_single_programming_mode(radio)
+
+    status.cur = 0
+    status.max = radio._memsize
+
+    for start_addr, end_addr in radio._ranges:
+        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
+            status.cur = addr + radio.BLOCK_SIZE_UP
+            radio.status_fn(status)
+            _write_block(radio, addr, radio.BLOCK_SIZE_UP)
+
+
+class H777V4BaseRadio(chirp_common.CloneModeRadio):
+    """RETEVIS H777 V4 Base"""
+    VENDOR = "Retevis"
+    MODEL = "H777 V4 Base"
+    BAUD_RATE = 9600
+    BLOCK_SIZE = 0x0D
+    BLOCK_SIZE_UP = 0x0D
+
+    POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.50),
+                    chirp_common.PowerLevel("High", watts=2.00)
+                    ]
+
+    PROGRAM_CMD = b"C777HAM"
+    _ranges = [
+               (0x0000, 0x00EA),
+              ]
+    _memsize = 0x00EA
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_settings = True
+        rf.has_bank = False
+        rf.has_ctone = True
+        rf.has_cross = True
+        rf.has_rx_dtcs = True
+        rf.has_tuning_step = False
+        rf.can_odd_split = True
+        rf.has_name = False
+        rf.valid_skips = ["", "S"]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
+        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
+                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
+        rf.valid_power_levels = self.POWER_LEVELS
+        rf.valid_duplexes = ["", "-", "+", "split", "off"]
+        rf.valid_modes = ["FM", "NFM"]  # 25 kHz, 12.5 kHz.
+        rf.valid_dtcs_codes = DTCS
+        rf.memory_bounds = (1, 16)
+        rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 20., 25., 50.]
+        rf.valid_bands = [(400000000, 520000000)]
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def sync_in(self):
+        """Download from radio"""
+        try:
+            data = do_download(self)
+        except errors.RadioError:
+            # Pass through any real errors we raise
+            raise
+        except Exception:
+            # If anything unexpected happens, make sure we raise
+            # a RadioError and log the problem
+            LOG.exception('Unexpected error during download')
+            raise errors.RadioError('Unexpected error communicating '
+                                    'with the radio')
+        self._mmap = data
+        self.process_mmap()
+
+    def sync_out(self):
+        """Upload to radio"""
+        try:
+            do_upload(self)
+        except errors.RadioError:
+            raise
+        except Exception:
+            # If anything unexpected happens, make sure we raise
+            # a RadioError and log the problem
+            LOG.exception('Unexpected error during upload')
+            raise errors.RadioError('Unexpected error communicating '
+                                    'with the radio')
+
+    def _decode_tone(self, toneval, txrx, chnum):
+        pol = "N"
+        rawval = (toneval[1].get_bits(0xFF) << 8) | toneval[0].get_bits(0xFF)
+
+        if toneval[0].get_bits(0xFF) == 0xFF:
+            mode = ""
+            val = 0
+        elif toneval[1].get_bits(0xC0) == 0xC0:
+            val = int(f'{rawval & 0x1FF:o}')
+            if val in DTCS:
+                mode = "DTCS"
+                pol = "R"
+            else:
+                LOG.error('unknown value: CH# %i DTCS %fR %s' % (
+                    chnum, val, txrx))
+                mode = ""
+                val = 0
+        elif toneval[1].get_bits(0x80):
+            val = int(f'{rawval & 0x1FF:o}')
+            if val in DTCS:
+                mode = "DTCS"
+            else:
+                LOG.error('unknown value: CH# %i DTCS %fN %s' % (
+                    chnum, val, txrx))
+                mode = ""
+                val = 0
+        else:
+            val = int(toneval) / 10.0
+            if val in chirp_common.TONES:
+                mode = "Tone"
+            else:
+                LOG.error('unknown value: CH# %i CTCSS %fHZ %s' % (
+                    chnum, val, txrx))
+                mode = ""
+                val = 0
+
+        return mode, val, pol
+
+    def _encode_tone(self, _toneval, mode, val, pol):
+        toneval = 0
+        if mode == "Tone":
+            toneval = int("%i" % (val * 10), 16)
+        elif mode == "DTCS":
+            toneval = int('%i' % val, 8)
+            toneval |= 0x8000
+            if pol == "R":
+                toneval |= 0x4000
+        else:
+            toneval = 0xFFFF
+
+        _toneval[0].set_raw(toneval & 0xFF)
+        _toneval[1].set_raw((toneval >> 8) & 0xFF)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number - 1])
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number - 1]
+
+        mem = chirp_common.Memory()
+        mem.number = number
+
+        if _mem.get_raw()[:1] == b"\xFF":
+            mem.empty = True
+            return mem
+
+        mem.freq = int(_mem.rxfreq) * 10
+
+        if _mem.txfreq == 0xFFFFFFFF:
+            # TX freq not set
+            mem.duplex = "off"
+            mem.offset = 0
+        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 25000000:
+            mem.duplex = "split"
+            mem.offset = int(_mem.txfreq) * 10
+        elif int(_mem.rxfreq) == int(_mem.txfreq):
+            mem.duplex = ""
+            mem.offset = 0
+        else:
+            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) \
+                and "-" or "+"
+            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
+
+        txmode, txval, txpol = self._decode_tone(_mem.tx_tone, 'TX', number)
+        rxmode, rxval, rxpol = self._decode_tone(_mem.rx_tone, 'RX', number)
+
+        chirp_common.split_tone_decode(mem,
+                                       (txmode, txval, txpol),
+                                       (rxmode, rxval, rxpol))
+
+        if _mem.scanadd:
+            mem.skip = "S"
+
+        mem.power = self.POWER_LEVELS[_mem.ishighpower]
+
+        mem.mode = _mem.narrow and "NFM" or "FM"
+
+        mem.extra = RadioSettingGroup("Extra", "extra")
+
+        # BCL (Busy Channel Lockout)
+        rs = RadioSettingValueInvertedBoolean(not bool(_mem.bcl))
+        rset = MemSetting("bcl", "BCL", rs)
+        rset.set_doc("Busy Channel Lockout")
+        mem.extra.append(rset)
+
+        # Scramble
+        rs = RadioSettingValueInvertedBoolean(not bool(_mem.scramb))
+        rset = MemSetting("scramb", "Scramble", rs)
+        rset.set_doc("Frequency inversion Scramble")
+        mem.extra.append(rset)
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number - 1]
+
+        _mem.set_raw(b"\xff" * 13)
+        if mem.empty:
+            return
+
+        _mem.rxfreq = mem.freq / 10
+        if mem.duplex == "off":
+            _mem.txfreq = 0xFFFFFFFF
+        elif mem.duplex == "split":
+            _mem.txfreq = mem.offset / 10
+        elif mem.duplex == "+":
+            _mem.txfreq = (mem.freq + mem.offset) / 10
+        elif mem.duplex == "-":
+            _mem.txfreq = (mem.freq - mem.offset) / 10
+        else:
+            _mem.txfreq = _mem.rxfreq
+
+        (txmode, txval, txpol), (rxmode, rxval, rxpol) = \
+            chirp_common.split_tone_encode(mem)
+
+        self._encode_tone(_mem.tx_tone, txmode, txval, txpol)
+        self._encode_tone(_mem.rx_tone, rxmode, rxval, rxpol)
+
+        _mem.scanadd = mem.skip == "S"
+        _mem.narrow = mem.mode == "NFM"
+
+        _mem.ishighpower = mem.power == self.POWER_LEVELS[1]
+
+        # resetting unknowns, this have to be set by hand
+        _mem.unknown0 = 3
+        _mem.unknown1 = 1
+
+        for setting in mem.extra:
+            setting.apply_to_memobj(_mem)
+
+    def get_settings(self):
+        _settings = self._memobj.settings
+        basic = RadioSettingGroup("basic", "Basic Settings")
+        group = RadioSettings(basic)
+
+        # Squelch
+        rs = RadioSettingValueInteger(0, 9, _settings.squelch)
+        rset = MemSetting("squelch", "Squelch", rs)
+        rset.set_doc("Squelch Level: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9")
+        basic.append(rset)
+
+        # Time Out Timer
+        rs = RadioSettingValueList(TIMEOUTTIMER_LIST,
+                                   current_index=_settings.tot)
+        rset = MemSetting("tot", "Time-out Timer", rs)
+        rset.set_doc("TX Time-out Timer: Off, 30, 60, 90, 120, 150," +
+                     " 180 seconds")
+        basic.append(rset)
+
+        # Vox Level
+        rs = RadioSettingValueList(OFF1TO9_LIST, current_index=_settings.vox)
+        rset = MemSetting("vox", "VOX Level", rs)
+        rset.set_doc("VOX Level: Off, 1, 2, 3, 4, 5, 6, 7, 8, 9")
+        basic.append(rset)
+
+        # Vox Delay
+        rs = RadioSettingValueList(VOXD_LIST,
+                                   current_index=_settings.voxd)
+        rset = MemSetting("voxd", "Vox Delay", rs)
+        rset.set_doc("VOX Delay: 0.5, 1.0, 1.5, 2.0, 2.5, 3.0 seconds")
+        basic.append(rset)
+
+        # Scan Mode
+        rs = RadioSettingValueList(SCANM_LIST, current_index=_settings.scanm)
+        rset = MemSetting("scanm", "Scan Mode", rs)
+        rset.set_doc("Scan Mode: Carrier, Time")
+        basic.append(rset)
+
+        # Voice Annunciation
+        rs = RadioSettingValueList(VOICE_LIST,
+                                   current_index=_settings.voice)
+        rset = MemSetting("voice", "Voice", rs)
+        rset.set_doc("Voice Prompts: Off, English")
+        basic.append(rset)
+
+        # Side Key 2 (long)
+        rs = RadioSettingValueList(SKEY2_LIST,
+                                   current_index=_settings.skey2)
+        rset = MemSetting("skey2", "Side Key 2", rs)
+        rset.set_doc("Side Key 2 (long press): Off, VOX, Power, Scan")
+        basic.append(rset)
+
+        # Code Switch
+        rs = RadioSettingValueBoolean(_settings.codesw)
+        rset = MemSetting("codesw", "Code Switch", rs)
+        rset.set_doc("Code Switch: Off, Enabled")
+        basic.append(rset)
+
+        # Battery Save
+        rs = RadioSettingValueBoolean(_settings.save)
+        rset = MemSetting("save", "Battery Save", rs)
+        rset.set_doc("Battery Save: Off, Enabled")
+        basic.append(rset)
+
+        # Beep Tone
+        rs = RadioSettingValueBoolean(_settings.beep)
+        rset = MemSetting("beep", "Beep", rs)
+        rset.set_doc("Beep Prompt: Off, Enabled")
+        basic.append(rset)
+
+        # VOX Switch
+        rs = RadioSettingValueBoolean(_settings.voxs)
+        rset = MemSetting("voxs", "VOX Switch", rs)
+        rset.set_doc("VOX Switch: Off, Enabled")
+        basic.append(rset)
+
+        # Roger
+        rs = RadioSettingValueBoolean(_settings.roger)
+        rset = MemSetting("roger", "Roger", rs)
+        rset.set_doc("Roger: Off, Enabled")
+        basic.append(rset)
+
+        return group
+
+    def set_settings(self, settings):
+        others = settings.apply_to(self._memobj.settings)
+        if others:
+            LOG.error('Did not apply %s' % others)
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        # This radio has always been post-metadata, so never do
+        # old-school detection
+        return False
+
+
+@directory.register
+@directory.detected_by(h777.RetevisH777)
+class H777V4(H777V4BaseRadio):
+    """RETEVIS H777V4"""
+    VENDOR = "Retevis"
+    MODEL = "H777"
+    VARIANT = 'V4'
+    IDENT = [b'\x00' * 6, b'\xFF' * 6]
+
+    # SKU #: A9294A (sold as FRS radio but supports full band TX/RX)
+    # Serial #: 2412R777XXXXXXX
+
+    # SKU #: A9294C (same as SKU #: A9294A (2 pack))
+    # Serial #: 2406R777XXXXXXX
+
+    # SKU #: A9294B (sold as PMR radio but supports full band TX/RX)
+    # Serial #: 2412R777XXXXXXX
+
+    # SKU #: A9294D (same as SKU #: A9294B (2 pack))
+    # Serial #: 24XXR777XXXXXXX
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20250801/chirp/drivers/uvk5.py 
new/chirp-20250808/chirp/drivers/uvk5.py
--- old/chirp-20250801/chirp/drivers/uvk5.py    2025-08-01 00:30:55.000000000 
+0200
+++ new/chirp-20250808/chirp/drivers/uvk5.py    2025-08-06 23:43:59.000000000 
+0200
@@ -295,9 +295,9 @@
 # bands supported by the UV-K5
 BANDS = {
         0: [50.0, 76.0],
-        1: [108.0, 135.9999],
-        2: [136.0, 199.9990],
-        3: [200.0, 299.9999],
+        1: [108.0, 136.9999],
+        2: [137.0, 173.9999],
+        3: [174.0, 349.9999],
         4: [350.0, 399.9999],
         5: [400.0, 469.9999],
         6: [470.0, 600.0]
@@ -306,9 +306,9 @@
 # for radios with modified firmware:
 BANDS_NOLIMITS = {
         0: [18.0, 76.0],
-        1: [108.0, 135.9999],
-        2: [136.0, 199.9990],
-        3: [200.0, 299.9999],
+        1: [108.0, 136.9999],
+        2: [137.0, 173.9999],
+        3: [174.0, 349.9999],
         4: [350.0, 399.9999],
         5: [400.0, 469.9999],
         6: [470.0, 1300.0]
@@ -737,7 +737,8 @@
     # Return a raw representation of the memory object, which
     # is very helpful for development
     def get_raw_memory(self, number):
-        return repr(self._memobj.channel[number-1])
+        return '\n'.join([repr(self._memobj.channel[number-1]),
+                          repr(self._memobj.channel_attributes[number-1])])
 
     def _find_band(self, hz):
         return _find_band(self._expanded_limits, hz)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/chirp-20250801/chirp/locale/pl.po 
new/chirp-20250808/chirp/locale/pl.po
--- old/chirp-20250801/chirp/locale/pl.po       2025-08-01 00:30:55.000000000 
+0200
+++ new/chirp-20250808/chirp/locale/pl.po       2025-08-06 23:43:59.000000000 
+0200
@@ -1518,16 +1518,16 @@
 #: ../drivers/ga510.py:1072 ../drivers/baofeng_uv17Pro.py:1244
 #: ../drivers/tdh8.py:2609 ../drivers/mml_jc8810.py:1393
 msgid "Frequency in this range must not be AM mode"
-msgstr ""
+msgstr "Częstotliwość w tym zakresie nie może używać trybu AM"
 
 #: ../drivers/ga510.py:1065 ../drivers/baofeng_uv17Pro.py:1240
 #: ../drivers/tdh8.py:2606 ../drivers/mml_jc8810.py:1390
 msgid "Frequency in this range requires AM mode"
-msgstr ""
+msgstr "Częstotliwość w tym zakresie musi używać trybu AM"
 
 #: ../drivers/tdh8.py:2613
 msgid "Frequency outside TX bands must be duplex=off"
-msgstr ""
+msgstr "Częstotliwość poza pasmami TX musi używać duplex=off"
 
 #: ../wxui/query_sources.py:354 ../wxui/query_sources.py:434
 #: ../wxui/query_sources.py:534
@@ -1595,7 +1595,7 @@
 
 #: ../wxui/main.py:1482
 msgid "Import messages"
-msgstr ""
+msgstr "Importuj wiadomości"
 
 #: ../wxui/main.py:1476
 msgid "Import not recommended"
@@ -1749,15 +1749,15 @@
 
 #: ../wxui/bugreport.py:326
 msgid "Login failed: Check your username and password"
-msgstr ""
+msgstr "Błąd logowania: Sprawdź nazwę użytkownika i hasło"
 
 #: ../drivers/uvk5.py:1799
 msgid "Logo string 1 (12 characters)"
-msgstr ""
+msgstr "Napis powitalny 1 (12 znaków)"
 
 #: ../drivers/uvk5.py:1806
 msgid "Logo string 2 (12 characters)"
-msgstr ""
+msgstr "Napis powitalny 2 (12 znaków)"
 
 #: ../wxui/query_sources.py:97 ../wxui/query_sources.py:374
 #: ../wxui/query_sources.py:673 ../wxui/query_sources.py:825
@@ -1767,7 +1767,7 @@
 #: ../wxui/memedit.py:1955
 #, python-format
 msgid "Manual edit of memory %i"
-msgstr ""
+msgstr "Ręczna edycja pamięci %i"
 
 #: ../wxui/main.py:202
 msgid "Memories"
@@ -1775,7 +1775,7 @@
 
 #: ../drivers/uvk5.py:2118
 msgid "Memories are read-only due to unsupported firmware version"
-msgstr ""
+msgstr "Pamięć tylko do odczytu z powodu nie wspieranej wersji firmware"
 
 #: ../wxui/memedit.py:1984
 #, python-format
@@ -1960,7 +1960,7 @@
 
 #: ../wxui/query_sources.py:401
 msgid "Open repeaters only"
-msgstr ""
+msgstr "Otórz tylko przemienniki"
 
 #: ../wxui/main.py:648
 msgid "Open stock config directory"
@@ -2021,7 +2021,7 @@
 
 #: ../wxui/bugreport.py:259
 msgid "Password"
-msgstr ""
+msgstr "Hasło"
 
 #: ../wxui/memedit.py:2120
 msgid "Paste"
@@ -2140,12 +2140,12 @@
 #: ../wxui/memquery.py:139
 #, fuzzy
 msgid "Property Name"
-msgstr "Właściwości"
+msgstr "Nazwa"
 
 #: ../wxui/memquery.py:147
 #, fuzzy
 msgid "Property Value"
-msgstr "Właściwości"
+msgstr "Wartość"
 
 #: ../wxui/query_sources.py:831
 msgid "QTH Locator"
@@ -2162,7 +2162,7 @@
 
 #: ../wxui/memquery.py:330
 msgid "Query string is invalid"
-msgstr ""
+msgstr "Błędne zapytanie"
 
 #: ../wxui/memquery.py:186
 #, fuzzy
@@ -2261,11 +2261,11 @@
 
 #: ../wxui/clone.py:616
 msgid "Remove"
-msgstr ""
+msgstr "Usuń"
 
 #: ../wxui/clone.py:617
 msgid "Remove selected model from list"
-msgstr ""
+msgstr "Usuń wybrany model z listy"
 
 #: ../wxui/bankedit.py:205
 msgid "Rename bank"
@@ -2340,7 +2340,7 @@
 
 #: ../drivers/uvk5.py:893
 msgid "Scanlists"
-msgstr ""
+msgstr "Lista skanowania"
 
 #: ../drivers/uvk5.py:889 ../drivers/ksun_m6.py:418
 msgid "Scrambler"
@@ -2548,7 +2548,7 @@
 
 #: ../wxui/memedit.py:2146
 msgid "This Memory"
-msgstr "Tą pamięć"
+msgstr "Tę pamięć"
 
 #: ../drivers/id5100.py:332
 msgid "This driver has been tested with v3 of the ID-5100. If your radio is 
not fully updated please help by opening a bug report with a debug log so we 
can add support for the other revisions."
@@ -2602,11 +2602,11 @@
 
 #: ../wxui/memedit.py:2150
 msgid "This memory and shift all up"
-msgstr "Tą pamięć i przeusń wszystkie wyżej"
+msgstr "Tę pamięć i przeusń wszystkie wyżej"
 
 #: ../wxui/memedit.py:2148
 msgid "This memory and shift block up"
-msgstr "Tą pamięć i przesuń blok wyżej"
+msgstr "Tę pamięć i przesuń blok wyżej"
 
 #: ../drivers/uvk5_egzumer.py:1468
 msgid "This option may break your radio! Each radio has a unique set of 
calibration data and uploading the data from the image will cause physical harm 
to the radio if it is from a different piece of hardware. Do not use this 
unless you know what you are doing and accept the risk of destroying your 
radio!"
@@ -3068,7 +3068,7 @@
 #~ msgstr "Plik ICF nie może być edytowany, może być wyświetlany oraz 
importowany do innego pliku.Otworzyć w trybie do odczytu?"
 
 #~ msgid "If you wish to disable this feature you may do so in the <u>Help</u> 
menu"
-#~ msgstr "Jeśli chcesz wyłączyć tą funkcję możesz to zrobić w menu 
<u>Pomoc</>"
+#~ msgstr "Jeśli chcesz wyłączyć tę funkcję możesz to zrobić w menu 
<u>Pomoc</>"
 
 #~ msgid "Import from RepeaterBook"
 #~ msgstr "Importuj z RepeaterBook"
Binary files old/chirp-20250801/tests/images/Retevis_H777_V4.img and 
new/chirp-20250808/tests/images/Retevis_H777_V4.img differ

++++++ chirp.obsinfo ++++++
--- /var/tmp/diff_new_pack.dX0eLa/_old  2025-08-09 20:06:02.437832230 +0200
+++ /var/tmp/diff_new_pack.dX0eLa/_new  2025-08-09 20:06:02.437832230 +0200
@@ -1,5 +1,5 @@
 name: chirp
-version: 20250801
-mtime: 1754001055
-commit: 477a28489d173e582f859e6f7242bbbeb840fd60
+version: 20250808
+mtime: 1754516639
+commit: 600a65c103b6c44c1d0d28f5a98d3025aebacd36
 

Reply via email to