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
