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-04-18 21:39:27 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/chirp (Old) and /work/SRC/openSUSE:Factory/.chirp.new.11940 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "chirp" Sat Apr 18 21:39:27 2026 rev:66 rq:1347976 version:20260417 Changes: -------- --- /work/SRC/openSUSE:Factory/chirp/chirp.changes 2026-04-11 22:29:49.876874216 +0200 +++ /work/SRC/openSUSE:Factory/.chirp.new.11940/chirp.changes 2026-04-18 21:39:49.142042088 +0200 @@ -1,0 +2,15 @@ +Sat Apr 18 19:11:01 UTC 2026 - Andreas Stieger <[email protected]> + +- Update to version 20260417: + * Add Wouxun XS20 series channel memory support + * Wouxun 935/8h series extra memory settings + * TK-372G correct variants and names + * Add BF-V12D support + * Improve cache performance for RepeaterBook queries + * Improve reliability of network source requests and bug reports + * radtel_rt900: Fix flake8 E501 line too long in FHSS Code setting + * radtel_rt900: Fix FHSS Code init crash on out-of-range image values + * radtel_rt900: Expose FHSS Code per-channel setting + * radtel_rt900: Expose LearnFHSS per-channel setting + +------------------------------------------------------------------- Old: ---- chirp-20260410.obscpio New: ---- chirp-20260417.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ chirp.spec ++++++ --- /var/tmp/diff_new_pack.BjXYI9/_old 2026-04-18 21:39:49.878072228 +0200 +++ /var/tmp/diff_new_pack.BjXYI9/_new 2026-04-18 21:39:49.882072391 +0200 @@ -20,7 +20,7 @@ %define pythons python3 Name: chirp -Version: 20260410 +Version: 20260417 Release: 0 Summary: Tool for programming amateur radio sets License: GPL-3.0-only ++++++ _service ++++++ --- /var/tmp/diff_new_pack.BjXYI9/_old 2026-04-18 21:39:49.946075013 +0200 +++ /var/tmp/diff_new_pack.BjXYI9/_new 2026-04-18 21:39:49.950075176 +0200 @@ -4,8 +4,8 @@ <param name="scm">git</param> <param name="changesgenerate">enable</param> <param name="filename">chirp</param> - <param name="versionformat">20260410</param> - <param name="revision">6025bd4d75cbdf54cea51688e5645c63568701ed</param> + <param name="versionformat">20260417</param> + <param name="revision">fc18cb8d9128739e386936412bf203dcfd71f671</param> </service> <service mode="manual" name="set_version"/> <service name="tar" mode="buildtime"/> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.BjXYI9/_old 2026-04-18 21:39:50.002077306 +0200 +++ /var/tmp/diff_new_pack.BjXYI9/_new 2026-04-18 21:39:50.010077633 +0200 @@ -1,7 +1,7 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/kk7ds/chirp.git</param> - <param name="changesrevision">6025bd4d75cbdf54cea51688e5645c63568701ed</param> + <param name="changesrevision">fc18cb8d9128739e386936412bf203dcfd71f671</param> </service> </servicedata> (No newline at EOF) ++++++ chirp-20260410.obscpio -> chirp-20260417.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/chirp/drivers/baofeng_digital.py new/chirp-20260417/chirp/drivers/baofeng_digital.py --- old/chirp-20260410/chirp/drivers/baofeng_digital.py 2026-04-10 03:18:54.000000000 +0200 +++ new/chirp-20260417/chirp/drivers/baofeng_digital.py 2026-04-17 00:26:24.000000000 +0200 @@ -484,3 +484,68 @@ ] _serial_id = 0x05 _proto = PROTO_D + + [email protected] +class BaofengBFV12D(BaofengDigital): + """Baofeng BF-V12D""" + MODEL = 'BF-V12D' + VALID_BANDS = [(400000000, 480000000)] + POWER_LEVELS = [ + chirp_common.PowerLevel("Low", watts=0.5), + chirp_common.PowerLevel("High", watts=2.00), + ] + _serial_id = 0x0a + _proto = PROTO_A + + def _get_radio_id(self): + # The V12D requires a ucbfpwd unlock sequence before entering + # programming mode, observed from factory software captures. + for attempt in range(5): + try: + baofeng_common._clean_buffer(self) + + # Send unlock command + self.pipe.write(b'\x02ucbfpwd') + resp = self.pipe.read(1) + LOG.debug('ucbfpwd response: %s', + resp.hex() if resp else 'empty') + if resp != CMD_ACK: + raise errors.RadioError( + 'No ACK on unlock command (got %s)' + % (resp.hex() if resp else 'empty')) + + # Send unlock data and model identifier + # (observed from factory software captures) + self.pipe.write(b'\x10\xc5\xea\x35') + resp = self.pipe.read(1) + LOG.debug('unlock data response: %s', + resp.hex() if resp else 'empty') + if resp != CMD_ACK: + raise errors.RadioError( + 'No ACK on unlock data') + + # Base64-encoded model identifier from factory software + self.pipe.write( + b'Q041OTUwMA==\x00\x00\x00\x00') + resp = self.pipe.read(1) + LOG.debug('model id response: %s', + resp.hex() if resp else 'empty') + if resp != CMD_ACK: + raise errors.RadioError( + 'No ACK on model identifier') + + # Now request radio ID + self.pipe.write(b'\x02prOGRAM') + resp = self.pipe.read(4) + LOG.debug('radio ID: %s', + resp.hex() if resp else 'empty') + if len(resp) != 4: + raise errors.RadioError( + 'Radio did not send identification') + return resp + except errors.RadioError: + if attempt == 4: + raise + self._exit_programming() + time.sleep(1) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/chirp/drivers/kg935g.py new/chirp-20260417/chirp/drivers/kg935g.py --- old/chirp-20260410/chirp/drivers/kg935g.py 2026-04-10 03:18:54.000000000 +0200 +++ new/chirp-20260417/chirp/drivers/kg935g.py 2026-04-17 00:26:24.000000000 +0200 @@ -92,6 +92,21 @@ (0x4780, 32, 375), # Memory Names 0-999 (0x7670, 8, 125), # Ch Valid bytes 0-999 ) +config_map_x20h = ( # map address, write size, write count + # (0x00, 64, 512), #- Use for full upload testing + # (0x44, 32, 1), # Freq Limits + # (0x440, 8, 1), # Area Message + # (0x480, 8, 5), # Scan Groups + # (0x500, 8, 15), # Call Codes + # (0x580, 8, 15), # Call Names + # (0x600, 8, 5), # FM Presets + # (0x800, 64, 2), # settings + # (0x880, 16, 1), # VFO A + # (0x8C0, 16, 1), # VFO B + (0x900, 64, 250), # Channel Memory 0-999 + (0x4780, 32, 375), # Memory Names 0-999 + (0x7670, 8, 125), # Ch Valid bytes 0-999 + ) AB_LIST = ["A", "B"] STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0] @@ -697,7 +712,7 @@ power:4; u8 unknown3:2, scan_add:1, - unknown4:1, + favorite:1, compander:1, mute_mode:2, iswide:1; @@ -1246,7 +1261,7 @@ power:4; u8 unknown3:2, scan_add:1, - unknown4:1, + favorite:1, compander:1, mute_mode:2, iswide:1; @@ -1331,6 +1346,8 @@ chirp_common.PowerLevel("H", watts=5.5)] _record_start = 0x7C config_map = config_map_935G + HAS_SCRAMBLER = True + HAS_FAVORITE = False def __init__(self, pipe): super().__init__(pipe) @@ -1339,6 +1356,29 @@ pol_mask=0x2000, tone_init=0x0000) + def get_extra(self, _mem, mem): + mem.extra = RadioSettingGroup("Extra", "Extra") + _mem.mute_mode = 2 if _mem.mute_mode > 2 else _mem.mute_mode + rs = RadioSetting("mute_mode", "Mute Mode", + RadioSettingValueList( + SPMUTE_LIST, current_index=_mem.mute_mode)) + mem.extra.append(rs) + if self.HAS_SCRAMBLER: + rs = RadioSetting("scrambler", "Scramble Descramble", + RadioSettingValueList( + SCRAMBLE_LIST, current_index=_mem.scrambler)) + mem.extra.append(rs) + rs = RadioSetting("compander", "Compander", + RadioSettingValueList( + ONOFF_LIST, current_index=_mem.compander)) + mem.extra.append(rs) + if self.HAS_FAVORITE: + rs = RadioSetting("favorite", "Favorite", + RadioSettingValueList( + ONOFF_LIST, current_index=_mem.favorite)) + mem.extra.append(rs) + return + def _write_record(self, cmd, payload=b''): _packet = struct.pack('BBBB', self._record_start, cmd, 0xFF, len(payload)) @@ -1557,6 +1597,17 @@ def get_raw_memory(self, number): return repr(self._memobj.memory[number]) + def _get_power(self, _mem, mem): + _mem.power = _mem.power & 0x3 + try: + mem.power = self.POWER_LEVELS[_mem.power] + except IndexError: + mem.power = self.POWER_LEVELS[-1] + + def _set_power(self, mem): + temp_val = self.POWER_LEVELS.index(mem.power) + return temp_val + def get_memory(self, number): _mem = self._memobj.memory[number] _nam = self._memobj.names[number] @@ -1591,13 +1642,11 @@ mem.name += chr(char) mem.name = mem.name.rstrip() + self.get_extra(_mem, mem) self.tone_model.get_tone(_mem, mem) mem.skip = "" if bool(_mem.scan_add) else "S" - _mem.power = _mem.power & 0x3 - if _mem.power > 2: - _mem.power = 2 - mem.power = self.POWER_LEVELS[_mem.power] + self._get_power(_mem, mem) mem.mode = _mem.iswide and "FM" or "NFM" return mem @@ -1631,21 +1680,15 @@ _mem.iswide = int(mem.mode == "FM") # set the tone self.tone_model.set_tone(mem, _mem) - # MRT set the scrambler and compander to off by default - # MRT This changes them in the channel memory - _mem.scrambler = 0 - _mem.compander = 0 # set the power _mem.power = _mem.power & 0x3 - if mem.power: - if _mem.power > 2: - _mem.power = 2 - _mem.power = self.POWER_LEVELS.index(mem.power) + if mem.power is not None: + _mem.power = self._set_power(mem) else: _mem.power = True - # MRT set to mute mode to QT (not QT+DTMF or QT*DTMF) by default - # MRT This changes them in the channel memory - _mem.mute_mode = 0 + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) # MRT it is unknown what impact these values have # MRT This changes them in the channel memory to match what @@ -1655,8 +1698,6 @@ # _mem.unknown1 = 0 # MRT Set to 3 to TO MATCH CPS VALUES _mem.unknown3 = 3 - # MRT Set to 1 to TO MATCH CPS VALUES - _mem.unknown4 = 1 # MRT set unknown5 to 1 and unknown6 to 0 _mem.unknown5 = 1 _mem.unknown6 = 255 @@ -2419,6 +2460,8 @@ """Wouxun KG-935G Plus""" VENDOR = "Wouxun" MODEL = "KG-935G Plus" + HAS_SCRAMBLER = True + HAS_FAVORITE = True def process_mmap(self): self._memobj = bitwise.parse(_MEM_FORMAT_935GPLUS, self._mmap) @@ -2445,6 +2488,58 @@ chirp_common.PowerLevel("M", watts=5.0), chirp_common.PowerLevel("H", watts=8.0)] config_map = config_map_935H + HAS_SCRAMBLER = True + HAS_FAVORITE = True def process_mmap(self): self._memobj = bitwise.parse(_MEM_FORMAT_935H, self._mmap) + + [email protected] +class KGXS20G(KG935GRadio): + + """Wouxun KG-XS20G""" + VENDOR = "Wouxun" + MODEL = "KG-XS20G" + _model = b"KG-UV8D-A" + _record_start = 0x79 + config_map = config_map_x20h + POWER_LEVELS = [chirp_common.PowerLevel("L", watts=0.5), + chirp_common.PowerLevel("H", watts=5.5)] + HAS_SCRAMBLER = False + HAS_FAVORITE = False + + def get_features(self): + rf = super().get_features() + rf.has_settings = False # Set to False until settings are mapped + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(_MEM_FORMAT_935GPLUS, self._mmap) + + def _get_power(self, _mem, mem): + if _mem.power == 0: + mem.power = self.POWER_LEVELS[0] + else: + mem.power = self.POWER_LEVELS[1] + + def _set_power(self, mem): + temp_val = 0 if mem.power == self.POWER_LEVELS[0] else 2 + return temp_val + + [email protected] +class KGXS20HRadio(KGXS20G): + + """Wouxun KG-XS20H""" + VENDOR = "Wouxun" + MODEL = "KG-XS20H" + HAS_SCRAMBLER = True + + [email protected] +class KGXS20GPlusRadio(KGXS20G): + + """Wouxun KG-XS20G Plus""" + VENDOR = "Wouxun" + MODEL = "KG-XS20G Plus" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/chirp/drivers/radtel_rt900.py new/chirp-20260417/chirp/drivers/radtel_rt900.py --- old/chirp-20260410/chirp/drivers/radtel_rt900.py 2026-04-10 03:18:54.000000000 +0200 +++ new/chirp-20260417/chirp/drivers/radtel_rt900.py 2026-04-17 00:26:24.000000000 +0200 @@ -53,6 +53,37 @@ LOG = logging.getLogger(__name__) +# FHSS Code is a 24-bit per-channel value. The OEM CPS represents an +# unset code as 0xFFFFFF in raw memory and as a blank field in the UI. +# When a code is set, the OEM also writes 0xA0 into the adjacent flag +# byte; when the code is cleared, that flag byte is restored to 0xFF. +FHSS_CODE_NULL = 0xFFFFFF +FHSS_CODE_FLAG_ACTIVE = 0xA0 +FHSS_CODE_FLAG_NULL = 0xFF + + +def _fhss_code_to_text(raw_code): + raw = int(raw_code) + if raw == FHSS_CODE_NULL: + return "" + return "%06X" % (raw & 0x7FFFFF) + + +def _validate_fhss_code(value): + s = str(value).strip() + if s == "": + return value + try: + v = int(s, 16) + except ValueError: + raise InvalidValueError( + "FHSS Code must be a hex value (e.g. 1A2B3C) or blank") + if not (0 <= v <= 0x7FFFFF): + raise InvalidValueError( + "FHSS Code must be between 000000 and 7FFFFF") + return value + + MEM_FORMAT = """ struct { lbcd rxfreq[4]; // 0-3 @@ -71,9 +102,9 @@ bcl:1, // BCL scan:1, // Scan 0 = Skip, 1 = Scan am_modulation:1, // Per chan AM modulation - learning:1; // Learning - lbcd code[3]; // 0-2 Code - u8 unknown6; // 3 + learning:1; // FHSS Learning + ul24 code; // 0-2 FHSS Code (little-endian, 0-0x7FFFFF) + u8 code_flag; // 3 0xA0 when Code set, 0xFF when blank char name[12]; // 4-F 12-character Alpha Tag } memory[%d]; @@ -1240,6 +1271,23 @@ rset = RadioSetting("bcl", "BCL", rs) mem.extra.append(rset) + # LearnFHSS (per-channel learn / FHSS flag). The OEM CPS labels + # this column "LearnFHSS"; the open-source firmware reads the + # same bit as chFlag3.b0 / fhssFlag (Core/Radio.c). + rs = RadioSettingValueBoolean(_mem.learning) + rset = RadioSetting("learning", "LearnFHSS", rs) + mem.extra.append(rset) + + # FHSS Code (24-bit little-endian, range 0x000000-0x7FFFFF). + # Displayed and accepted as a 6-digit uppercase hex string, or + # blank to clear the code (raw 0xFFFFFF). Validation is hoisted + # to a module-level function so this RadioSetting stays + # picklable for clipboard copy. + rs = RadioSettingValueString(0, 6, _fhss_code_to_text(_mem.code)) + rs.set_validate_callback(_validate_fhss_code) + rset = RadioSetting("fhss_code", "FHSS Code (hex)", rs) + mem.extra.append(rset) + # PTT-ID rs = RadioSettingValueList(PTTID_LIST, current_index=_mem.pttid) rset = RadioSetting("pttid", "PTT ID", rs) @@ -1321,7 +1369,19 @@ _mem.narrow = 0b1 for setting in mem.extra: - setattr(_mem, setting.get_name(), setting.value) + if setting.get_name() == 'fhss_code': + # Mirror OEM CPS: blank input clears Code (0xFFFFFF) and + # restores the adjacent flag byte to 0xFF; any value + # writes Code and sets the flag byte to 0xA0. + s = str(setting.value).strip() + if s == "": + _mem.code = FHSS_CODE_NULL + _mem.code_flag = FHSS_CODE_FLAG_NULL + else: + _mem.code = int(s, 16) + _mem.code_flag = FHSS_CODE_FLAG_ACTIVE + else: + setattr(_mem, setting.get_name(), setting.value) def validate_memory(self, mem): msgs = [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/chirp/drivers/tk760g.py new/chirp-20260417/chirp/drivers/tk760g.py --- old/chirp-20260410/chirp/drivers/tk760g.py 2026-04-10 03:18:54.000000000 +0200 +++ new/chirp-20260417/chirp/drivers/tk760g.py 2026-04-17 00:26:24.000000000 +0200 @@ -1581,20 +1581,20 @@ @directory.register class TK372G_Radios(Kenwood_Series_60G): - """Kenwood TK-372 Radio [K/E/M/NE]""" + """Kenwood TK-372G Radio [K/K2/K3/K4]""" MODEL = "TK-372G" TYPE = b"P3720" VARIANTS = { - b"P3720\x06\xff": (32, 450, 470, "K"), - b"P3720\x07\xff": (32, 470, 490, "K1"), - b"P3720\x08\xff": (32, 490, 512, "K2"), - b"P3720\x09\xff": (32, 403, 430, "K3") + b"P3720\x06\xfb": (32, 450, 470, "K"), + b"P3720\x07\xfb": (32, 470, 490, "K2"), + b"P3720\x08\xfb": (32, 490, 512, "K3"), + b"P3720\x09\xfb": (32, 403, 430, "K4") } @directory.register class TK370G_Radios(Kenwood_Series_60G): - """Kenwood TK-370 Radio [K/E/M/NE]""" + """Kenwood TK-370G Radio [K/E/M/NE]""" MODEL = "TK-370G" TYPE = b"P3700" VARIANTS = { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/chirp/sources/base.py new/chirp-20260417/chirp/sources/base.py --- old/chirp-20260410/chirp/sources/base.py 2026-04-10 03:18:54.000000000 +0200 +++ new/chirp-20260417/chirp/sources/base.py 2026-04-17 00:26:24.000000000 +0200 @@ -7,7 +7,7 @@ LOG = logging.getLogger(__name__) HEADERS = { - 'User-Agent': 'chirp/%s Python %i.%i.%i %s' % ( + 'User-Agent': 'CHIRP/%s Python %i.%i.%i %s' % ( CHIRP_VERSION, sys.version_info.major, sys.version_info.minor, sys.version_info.micro, sys.platform), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/chirp/sources/repeaterbook.py new/chirp-20260417/chirp/sources/repeaterbook.py --- old/chirp-20260410/chirp/sources/repeaterbook.py 2026-04-10 03:18:54.000000000 +0200 +++ new/chirp-20260417/chirp/sources/repeaterbook.py 2026-04-17 00:26:24.000000000 +0200 @@ -119,30 +119,59 @@ LOG.debug('RepeaterBook database %s too old: %s', fn, modified_dt) - r = requests.get('https://data.chirpmyradio.com/rb/%s.xz' % fn, - headers=base.HEADERS, - stream=True) - if r.status_code != 200: + try: + with open(data_file, 'rb') as f: + data = json.loads(f.read()) + etag = data.get('ETag') + except Exception as e: + LOG.warning('Failed to load cached data from %s: %s', + data_file, e) + etag = None + + headers = dict(base.HEADERS) + if etag: + headers['If-None-Match'] = etag + try: + r = requests.get('https://data.chirpmyradio.com/rb/%s.xz' % fn, + headers=headers, + stream=True) + except requests.exceptions.RequestException as e: + LOG.warning('Failed to fetch data from RepeaterBook: %s' % e) + if modified: + # If we have a cached file, it's better than nothing so use + # it since the server was not contactable + status.send_status('Using cached data', 50) + return data_file + if r.status_code == 304: + status.send_status('Using cached data', 50) + LOG.debug('Server reports no changes so marking our cache ' + 'as current') + os.utime(data_file, None) + status.send_status('Complete', 50) + return data_file + elif r.status_code != 200: if modified: + # If we have a cached file, it's better than nothing, so use + # it since the server refused. status.send_status('Using cached data', 50) + return data_file status.send_fail('Got error code %i (%s) from server' % ( r.status_code, r.reason)) LOG.error('Repeaterbook query %r returned %i (%s)', r.url, r.status_code, r.reason) return - tmp = data_file + '.tmp' + chunk_size = 8192 probable_end = 3 << 20 counter = 0 data = b'' decomp = lzma.LZMADecompressor(format=lzma.FORMAT_XZ) - with open(tmp, 'wb') as f: - for chunk in r.iter_content(chunk_size=chunk_size): - chunk = decomp.decompress(chunk) - f.write(chunk) - data += chunk - counter += len(chunk) - status.send_status('Downloading', counter / probable_end * 50) + for chunk in r.iter_content(chunk_size=chunk_size): + chunk = decomp.decompress(chunk) + data += chunk + counter += len(chunk) + status.send_status('Downloading', counter / probable_end * 50) + try: results = json.loads(data) except Exception as e: @@ -153,6 +182,16 @@ status.send_fail('RepeaterBook returned invalid response') return + try: + results['ETag'] = r.headers.get('ETag') + tmp = data_file + '.tmp' + with open(tmp, 'wb') as f: + f.write(json.dumps(results).encode('utf-8')) + except Exception as e: + LOG.exception('Failed to write data to %s: %s' % (tmp, e)) + status.send_fail('Failed to write RepeaterBook data to disk') + return + if results['count']: try: os.rename(tmp, data_file) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/chirp/wxui/bugreport.py new/chirp-20260417/chirp/wxui/bugreport.py --- old/chirp-20260410/chirp/wxui/bugreport.py 2026-04-10 03:18:54.000000000 +0200 +++ new/chirp-20260417/chirp/wxui/bugreport.py 2026-04-17 00:26:24.000000000 +0200 @@ -34,10 +34,13 @@ from chirp.wxui import common from chirp.wxui import config from chirp.wxui import serialtrace +from chirp.wxui import report + _ = wx.GetTranslation CONF = config.get() -BASE = CONF.get('baseurl', 'chirpmyradio') or 'https://www.chirpmyradio.com' +BASE = (CONF.get('baseurl', 'chirpmyradio') or + 'https://data.chirpmyradio.com/redmine') LOG = logging.getLogger(__name__) ReportThreadEvent, EVT_REPORT_THREAD = wx.lib.newevent.NewCommandEvent() @@ -146,10 +149,11 @@ self.chirpmain = chirpmain self.editor = chirpmain.current_editorset self.session = requests.Session() - self.session.headers = { - 'User-Agent': 'CHIRP/%s' % CHIRP_VERSION, + report.ensure_session() + self.session.headers = dict(report.SESSION.headers) + self.session.headers.update({ 'Referer': 'https://chirpmyradio.com/projects/chirp/issues/new', - } + }) def get_page(self, name, cls): if not hasattr(self, 'page_%s' % name): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/chirp/wxui/memedit.py new/chirp-20260417/chirp/wxui/memedit.py --- old/chirp-20260410/chirp/wxui/memedit.py 2026-04-10 03:18:54.000000000 +0200 +++ new/chirp-20260417/chirp/wxui/memedit.py 2026-04-17 00:26:24.000000000 +0200 @@ -2415,8 +2415,11 @@ 'source': self.GetId()} data = wx.DataObjectComposite() memdata = wx.CustomDataObject(common.CHIRP_DATA_MEMORY) - data.Add(memdata) - memdata.SetData(pickle.dumps(payload)) + try: + memdata.SetData(pickle.dumps(payload)) + data.Add(memdata) + except Exception as e: + LOG.exception('Failed to get native memory for paste: %s', e) if portable: strfmt = chirp_common.mem_to_text(mems[0]) textdata = wx.TextDataObject(strfmt) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/chirp/wxui/report.py new/chirp-20260417/chirp/wxui/report.py --- old/chirp-20260410/chirp/wxui/report.py 2026-04-10 03:18:54.000000000 +0200 +++ new/chirp-20260417/chirp/wxui/report.py 2026-04-17 00:26:24.000000000 +0200 @@ -26,6 +26,7 @@ wx = None from chirp import CHIRP_VERSION +from chirp.sources import base from chirp.wxui import config CONF = config.get() @@ -75,6 +76,8 @@ 'X-CHIRP-UUID': CONF.get('seat', 'state'), 'X-CHIRP-Environment': get_environment(), } + for k, v in SESSION.headers.items(): + base.HEADERS[k] = v def with_session(fn): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/flathub/README.md new/chirp-20260417/flathub/README.md --- old/chirp-20260410/flathub/README.md 1970-01-01 01:00:00.000000000 +0100 +++ new/chirp-20260417/flathub/README.md 2026-04-17 00:26:24.000000000 +0200 @@ -0,0 +1 @@ +This directory contains the upstream data required by [flathub](https://flathub.org). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/flathub/com.chirpmyradio.chirp.desktop new/chirp-20260417/flathub/com.chirpmyradio.chirp.desktop --- old/chirp-20260410/flathub/com.chirpmyradio.chirp.desktop 1970-01-01 01:00:00.000000000 +0100 +++ new/chirp-20260417/flathub/com.chirpmyradio.chirp.desktop 2026-04-17 00:26:24.000000000 +0200 @@ -0,0 +1,11 @@ +[Desktop Entry] +Version=1.0 +Type=Application + +Name=CHIRP +Comment=A free, open-source tool for programming your radio. +Categories=Settings;HardwareSettings; + +Icon=com.chirpmyradio.chirp +Exec=chirp-wrapper.sh +Terminal=false diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/flathub/com.chirpmyradio.chirp.metainfo.xml new/chirp-20260417/flathub/com.chirpmyradio.chirp.metainfo.xml --- old/chirp-20260410/flathub/com.chirpmyradio.chirp.metainfo.xml 1970-01-01 01:00:00.000000000 +0100 +++ new/chirp-20260417/flathub/com.chirpmyradio.chirp.metainfo.xml 2026-04-17 00:26:24.000000000 +0200 @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<component type="desktop-application"> + <id>com.chirpmyradio.chirp</id> + <name>CHIRP</name> + <summary>A free, open-source tool for programming your radio.</summary> + + <metadata_license>CC0-1.0</metadata_license> + <project_license>GPL-3.0-only</project_license> + + <description> + <p> + CHIRP is a free, open-source tool for programming your radio. It supports a large number of manufacturers and models, as well as provides a way to interface with multiple data sources and formats. + </p> + <p> + NOTE: For the app to fully function, adding your Linux user to the dialout group will be required in order for CHIRP to access serial ports. This is usually done like this: <code>sudo usermod -aG dialout $USER</code> + </p> + </description> + + <launchable type="desktop-id">com.chirpmyradio.chirp.desktop</launchable> + <screenshots> + <screenshot type="default"> + <image>https://raw.githubusercontent.com/kk7ds/chirp/refs/heads/master/flathub/screenshots/main.png</image> + </screenshot> + <screenshot> + <image>https://raw.githubusercontent.com/kk7ds/chirp/refs/heads/master/flathub/screenshots/example.png</image> + </screenshot> + </screenshots> + + <url type="homepage">https://chirpmyradio.com</url> + <url type="bugtracker">https://chirpmyradio.com/projects/chirp/issues</url> + <url type="help">https://chirpmyradio.com/projects/chirp/wiki/Documentation</url> + + <content_rating type="oars-1.1"/> + +</component> Binary files old/chirp-20260410/flathub/com.chirpmyradio.chirp.png and new/chirp-20260417/flathub/com.chirpmyradio.chirp.png differ Binary files old/chirp-20260410/flathub/screenshots/example.png and new/chirp-20260417/flathub/screenshots/example.png differ Binary files old/chirp-20260410/flathub/screenshots/main.png and new/chirp-20260417/flathub/screenshots/main.png differ Binary files old/chirp-20260410/tests/images/Baofeng_BF-V12D.img and new/chirp-20260417/tests/images/Baofeng_BF-V12D.img differ Binary files old/chirp-20260410/tests/images/Wouxun_KG-XS20G.img and new/chirp-20260417/tests/images/Wouxun_KG-XS20G.img differ Binary files old/chirp-20260410/tests/images/Wouxun_KG-XS20G_Plus.img and new/chirp-20260417/tests/images/Wouxun_KG-XS20G_Plus.img differ Binary files old/chirp-20260410/tests/images/Wouxun_KG-XS20H.img and new/chirp-20260417/tests/images/Wouxun_KG-XS20H.img differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/chirp-20260410/tests/unit/test_repeaterbook.py new/chirp-20260417/tests/unit/test_repeaterbook.py --- old/chirp-20260410/tests/unit/test_repeaterbook.py 2026-04-10 03:18:54.000000000 +0200 +++ new/chirp-20260417/tests/unit/test_repeaterbook.py 2026-04-17 00:26:24.000000000 +0200 @@ -10,6 +10,7 @@ import pytest from chirp import chirp_common +from chirp.sources import base from chirp.sources import repeaterbook @@ -173,15 +174,14 @@ with mock.patch('requests.get') as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.iter_content.return_value = [b'foo'] + mock_get.return_value.headers = {'ETag': 'foo'} status = mock.MagicMock() r = rb.get_data(status, 'US', 'OR', '') self.assertIsNone(r) files = os.listdir(os.path.join(self.tempdir, 'repeaterbook')) - # Make sure we only wrote one file and that it is a tempfile - # not one we will find as a data file later - self.assertEqual(1, len(files)) - self.assertTrue(files[0].endswith('tmp')) + # Make sure we never wrote the bad data to file + self.assertEqual(0, len(files)) status.send_fail.assert_called() def test_get_data_no_results(self): @@ -192,6 +192,7 @@ with mock.patch('requests.get') as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.iter_content.return_value = [fake_data] + mock_get.return_value.headers = {'ETag': 'foo'} status = mock.MagicMock() r = rb.get_data(status, 'US', 'OR', '') self.assertIsNone(r) @@ -212,6 +213,7 @@ with mock.patch('requests.get') as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.iter_content.return_value = [fake_data] + mock_get.return_value.headers = {'ETag': 'foo'} status = mock.MagicMock() r = rb.get_data(status, 'US', 'OR', '') self.assertIsNotNone(r) @@ -259,3 +261,34 @@ 'United States', 'Oregon', '') # Cache file is 45 days old, we should re-fetch mock_get.assert_called() + + def test_get_data_honors_etag(self): + os.mkdir(os.path.join(self.tempdir, 'repeaterbook')) + cache_file = os.path.join(self.tempdir, + 'repeaterbook', + os.path.basename(self.testfile)) + with open(cache_file, 'w') as f: + f.write(json.dumps({'ETag': 'foo'})) + rb = repeaterbook.RepeaterBook() + with mock.patch('requests.get') as mock_get: + mock_get.return_value.status_code = 304 + real_timedelta = datetime.timedelta + real_datetime = datetime.datetime + + future = datetime.datetime.now() + datetime.timedelta(days=45) + with mock.patch.object(repeaterbook, 'datetime') as mock_dt: + mock_dt.datetime.fromtimestamp = real_datetime.fromtimestamp + mock_dt.timedelta = real_timedelta + mock_dt.datetime.now.return_value = future + r = rb.get_data(mock.MagicMock(), + 'United States', 'Oregon', '') + # Cache file is 45 days old, we should re-fetch, but the server + # said it was unchanged, so we should use the cache + mock_get.assert_called() + self.assertEqual(cache_file, r) + expected_headers = dict(base.HEADERS) + expected_headers['If-None-Match'] = 'foo' + mock_get.assert_called_once_with( + mock.ANY, + headers=expected_headers, + stream=True) ++++++ chirp.obsinfo ++++++ --- /var/tmp/diff_new_pack.BjXYI9/_old 2026-04-18 21:39:51.198126284 +0200 +++ /var/tmp/diff_new_pack.BjXYI9/_new 2026-04-18 21:39:51.206126612 +0200 @@ -1,5 +1,5 @@ name: chirp -version: 20260410 -mtime: 1775783934 -commit: 6025bd4d75cbdf54cea51688e5645c63568701ed +version: 20260417 +mtime: 1776378384 +commit: fc18cb8d9128739e386936412bf203dcfd71f671
