[chirp_devel] [PATCH] Driver for TG-UV2+ (and probably TG-UV2)
Ran Katz
Tue Feb 8 14:57:21 PST 2022
Hi,
I added the image file to issue 8591
Ran
On Wed, Feb 9, 2022 at 12:55 AM Ran Katz <rankatz at gmail.com> wrote:
> # HG changeset patch
> # User Ran Katz <rankatz at gmai.com>
> # Date 1644360111 -7200
> # Wed Feb 09 00:41:51 2022 +0200
> # Node ID 7cfb9fdcbb21c14217859e5ac61e42f99c157b05
> # Parent 164528caafdcef4cc871bdced922ca7985c71ec1
> Driver for TG-UV2+ (and probably TG-UV2)
> See Issues #8591 and #177
> Tested on TG-UV2+ , however teh code base (a 'C' utility) was developed a
> decade ago for the TG-UV2,
> and I could not find any differences.
>
> ---------------
> user: Ran Katz <rankatz at gmai.com>
> branch 'default'
> added chirp/drivers/tg_uv2p.py
> added tests/images/Quansheng_TG-UV2+.img
>
> diff --git a/chirp/drivers/tg_uv2p.py b/chirp/drivers/tg_uv2p.py
> new file mode 100644
> --- /dev/null
> +++ b/chirp/drivers/tg_uv2p.py
> @@ -0,0 +1,603 @@
> +# Copyright 2013 Dan Smith <dsmith at 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 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/>.
> +
> +# This driver was derived from the:
> +# Quansheng TG-UV2 Utility by Mike Nix <mnix at wanm.com.au>
> +# (So thanks Mike!)
> +
> +import struct
> +import logging
> +from chirp import chirp_common, directory, bitwise, memmap, errors, util
> +from chirp.settings import RadioSetting, RadioSettingGroup, \
> + RadioSettingValueBoolean, RadioSettingValueList, \
> + RadioSettingValueInteger, RadioSettingValueString, \
> + RadioSettingValueFloat, RadioSettings
> +from textwrap import dedent
> +
> +LOG = logging.getLogger(__name__)
> +
> +mem_format = """
> +struct memory {
> + bbcd freq[4];
> + bbcd offset[4];
> + u8 rxtone;
> + u8 txtone;
> + u8 unknown1:2,
> + txtmode:2,
> + unknown2:2,
> + rxtmode:2;
> + u8 duplex;
> + u8 unknown3:3,
> + isnarrow:1,
> + unknown4:2,
> + not_scramble:1,
> + not_revfreq:1;
> + u8 flag3;
> + u8 step;
> + u8 power;
> +};
> +
> +struct bandflag {
> + u8 scanadd:1,
> + unknown1:3,
> + band:4;
> +};
> +
> +struct tguv2_config {
> + u8 unknown1;
> + u8 squelch;
> + u8 time_out_timer;
> + u8 priority_channel;
> +
> + u8 unknown2:7,
> + keyunlocked:1;
> + u8 busy_lockout;
> + u8 vox;
> + u8 unknown3;
> +
> + u8 beep_tone_disabled;
> + u8 display;
> + u8 step;
> + u8 unknown4;
> +
> + u8 unknown5;
> + u8 rxmode;
> + u8 unknown6:7,
> + no_end_tone:1;
> + u8 vfo_model;
> +};
> +
> +struct vfo {
> + u8 current;
> + u8 chan;
> + u8 memno;
> +};
> +
> +struct name {
> + u8 name[6];
> + u8 unknown1[10];
> +};
> +
> +#seekto 0x0000;
> +char ident[32];
> +u8 blank[16];
> +
> +struct memory channels[200];
> +struct memory bands[5];
> +
> +#seekto 0x0D30;
> +struct bandflag bandflags[200];
> +
> +#seekto 0x0E30;
> +struct tguv2_config settings;
> +struct vfo vfos[2];
> +u8 unk5;
> +u8 reserved2[9];
> +u8 band_restrict;
> +u8 txen350390;
> +
> +#seekto 0x0F30;
> +struct name names[200];
> +
> +"""
> +
> +def do_ident(radio):
> + radio.pipe.timeout = 3
> + radio.pipe.write("\x02PnOGdAM")
> + for x in xrange(10):
> + ack = radio.pipe.read(1)
> + if ack == '\x06':
> + break
> + else:
> + raise errors.RadioError("Radio did not ack programming mode")
> + radio.pipe.write("\x40\x02")
> + ident = radio.pipe.read(8)
> + LOG.debug(util.hexprint(ident))
> + if not ident.startswith('P5555'):
> + raise errors.RadioError("Unsupported model")
> + radio.pipe.write("\x06")
> + ack = radio.pipe.read(1)
> + if ack != "\x06":
> + raise errors.RadioError("Radio did not ack ident")
> +
> +
> +def do_status(radio, direction, addr):
> + status = chirp_common.Status()
> + status.msg = "Cloning %s radio" % direction
> + status.cur = addr
> + status.max = 0x2000
> + radio.status_fn(status)
> +
> +
> +def do_download(radio):
> + do_ident(radio)
> + data = "TG-UV2+ Radio Program Data v1.0\x00"
> + data += ("\x00" * 16)
> +
> + firstack = None
> + for i in range(0, 0x2000, 8):
> + frame = struct.pack(">cHB", "R", i, 8)
> + radio.pipe.write(frame)
> + result = radio.pipe.read(12)
> + if not (result[0]=="W" and frame[1:4]==result[1:4]):
> + LOG.debug(util.hexprint(result))
> + raise errors.RadioError("Invalid response for address 0x%04x"
> % i)
> + radio.pipe.write("\x06")
> + ack = radio.pipe.read(1)
> + if not firstack:
> + firstack = ack
> + else:
> + if not ack == firstack:
> + LOG.debug("first ack: %s ack received: %s",
> + util.hexprint(firstack), util.hexprint(ack))
> + raise errors.RadioError("Unexpected response")
> + data += result[4:]
> + do_status(radio, "from", i)
> +
> + return memmap.MemoryMap(data)
> +
> +
> +def do_upload(radio):
> + do_ident(radio)
> + data = radio._mmap[0x0030:]
> +
> + for i in range(0, 0x2000, 8):
> + frame = struct.pack(">cHB", "W", i, 8)
> + frame += data[i:i + 8]
> + radio.pipe.write(frame)
> + ack = radio.pipe.read(1)
> + if ack != "\x06":
> + LOG.debug("Radio NAK'd block at address 0x%04x" % i)
> + raise errors.RadioError(
> + "Radio NAK'd block at address 0x%04x" % i)
> + LOG.debug("Radio ACK'd block at address 0x%04x" % i)
> + do_status(radio, "to", i)
> +
> +DUPLEX = ["", "+", "-"]
> +TGUV2P_STEPS = [5, 6.25, 10, 12.5, 15, 20, 25, 30, 50, 100,]
> +CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_|* +-"
> +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=10),
> + chirp_common.PowerLevel("Med", watts=5),
> + chirp_common.PowerLevel("Low", watts=1)]
> +POWER_LEVELS_STR = ["High", "Med", "Low"]
> +VALID_BANDS = [(88000000, 108000000),
> + (136000000, 174000000),
> + (350000000, 390000000),
> + (400000000, 470000000),
> + (470000000, 520000000)]
> +
> + at directory.register
> +class QuanshengTGUV2P(chirp_common.CloneModeRadio,
> + chirp_common.ExperimentalRadio):
> + """Quansheng TG-UV2+"""
> + VENDOR = "Quansheng"
> + MODEL = "TG-UV2+"
> + BAUD_RATE = 9600
> +
> + _memsize = 0x2000
> +
> + @classmethod
> + def get_prompts(cls):
> + rp = chirp_common.RadioPrompts()
> + rp.experimental = \
> + ('Experimental version for TG-UV2/2+ radios '
> + 'Proceed at your own risk!')
> + rp.pre_download = _(dedent("""\
> + 1. Turn radio off.
> + 2. Connect cable to mic/spkr connector.
> + 3. Make sure connector is firmly connected.
> + 4. Turn radio on.
> + 5. Ensure that the radio is tuned to channel with no activity.
> + 6. Click OK to download image from device."""))
> + rp.pre_upload = _(dedent("""\
> + 1. Turn radio off.
> + 2. Connect cable to mic/spkr connector.
> + 3. Make sure connector is firmly connected.
> + 4. Turn radio on.
> + 5. Ensure that the radio is tuned to channel with no activity.
> + 6. Click OK to upload image to device."""))
> + return rp
> +
> + def get_features(self):
> + rf = chirp_common.RadioFeatures()
> + rf.has_settings = True
> + rf.has_cross = True
> + rf.has_rx_dtcs = True
> + rf.has_dtcs_polarity = True
> + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
> + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
> + "->Tone", "->DTCS", "DTCS->",
> "DTCS->DTCS"]
> + rf.valid_duplexes = DUPLEX
> + rf.can_odd_split = False
> + rf.valid_skips = ["", "S"]
> + rf.valid_characters = CHARSET
> + rf.valid_name_length = 6
> + rf.valid_tuning_steps = TGUV2P_STEPS
> + rf.valid_bands = VALID_BANDS
> +
> + rf.valid_modes = ["FM", "NFM"]
> + rf.valid_power_levels = POWER_LEVELS
> + rf.has_ctone = True
> + rf.has_bank = False
> + rf.has_tuning_step = True
> + rf.memory_bounds = (1, 200)
> + return rf
> +
> + def sync_in(self):
> + try:
> + self._mmap = do_download(self)
> + except errors.RadioError:
> + raise
> + except Exception, e:
> + raise errors.RadioError("Failed to communicate with radio:
> %s" % e)
> + self.process_mmap()
> +
> + def sync_out(self):
> + try:
> + do_upload(self)
> + except errors.RadioError:
> + raise
> + except Exception, e:
> + raise errors.RadioError("Failed to communicate with radio:
> %s" % e)
> +
> + def process_mmap(self):
> + self._memobj = bitwise.parse(mem_format, self._mmap)
> +
> + def get_raw_memory(self, number):
> + return repr(self._memobj.channels[number - 1])
> +
> + def _decode_tone(self, _mem, which):
> + def _get(field):
> + return getattr(_mem, "%s%s" % (which, field))
> +
> + value = _get('tone')
> + tmode = _get('tmode')
> +
> + if (value <= 104) and (tmode <= 3):
> + if tmode == 0:
> + mode = val = pol = None
> + elif tmode == 1:
> + mode = 'Tone'
> + val = chirp_common.TONES[value]
> + pol = None
> + else:
> + mode = 'DTCS'
> + val = chirp_common.DTCS_CODES[value]
> + pol = "N" if (tmode == 2) else "R"
> + else:
> + mode = val = pol = None
> +
> + return mode, val, pol
> +
> + def _encode_tone(self, _mem, which, mode, val, pol):
> + def _set(field, value):
> + setattr(_mem, "%s%s" % (which, field), value)
> +
> + if (mode == "Tone"):
> + _set("tone", chirp_common.TONES.index(val))
> + _set("tmode", 0x01)
> + elif mode == "DTCS":
> + _set("tone", chirp_common.DTCS_CODES.index(val))
> + if pol == "N":
> + _set("tmode", 0x02)
> + else:
> + _set("tmode", 0x03)
> + else:
> + _set("tone", 0)
> + _set("tmode", 0)
> +
> + def _get_memobjs(self, number):
> + if isinstance(number, str):
> + return (getattr(self._memobj, number.lower()), None)
> +
> + else:
> + return (self._memobj.channels[number - 1],
> + self._memobj.bandflags[number -1],
> + self._memobj.names[number - 1].name)
> +
> + def get_memory(self, number):
> + _mem, _bf, _nam = self._get_memobjs(number)
> + mem = chirp_common.Memory()
> + if isinstance(number, str):
> + mem.extd_number = number
> + else:
> + mem.number = number
> +
> + if (_mem.freq.get_raw()[0] == "\xFF") or (_bf.band == "\x0F"):
> + mem.empty = True
> + return mem
> +
> + mem.freq = int(_mem.freq) * 10
> +
> + if _mem.offset.get_raw()[0] == "\xFF" :
> + mem.offset = 0
> + else:
> + mem.offset = int(_mem.offset) * 10
> +
> +
> + chirp_common.split_tone_decode(
> + mem,
> + self._decode_tone(_mem, "tx"),
> + self._decode_tone(_mem, "rx"))
> +
> + if 'step' in _mem and _mem.step > len(TGUV2P_STEPS):
> + _mem.step = 0x00
> + mem.tuning_step = TGUV2P_STEPS[_mem.step]
> + mem.duplex = DUPLEX[_mem.duplex]
> + mem.mode = _mem.isnarrow and "NFM" or "FM"
> + mem.skip = "" if bool(_bf.scanadd) else "S"
> + mem.power = POWER_LEVELS[_mem.power]
> +
> + if _nam:
> + for char in _nam:
> + try:
> + mem.name += CHARSET[char]
> + except IndexError:
> + break
> + mem.name = mem.name.rstrip()
> +
> + mem.extra = RadioSettingGroup("Extra", "extra")
> +
> + rs = RadioSetting("not_scramble", "(not)SCRAMBLE",
> + RadioSettingValueBoolean(_mem.not_scramble))
> + mem.extra.append(rs)
> +
> + rs = RadioSetting("not_revfreq", "(not)Reverse Duplex",
> + RadioSettingValueBoolean(_mem.not_revfreq))
> + mem.extra.append(rs)
> +
> + return mem
> +
> + def set_memory(self, mem):
> + _mem, _bf, _nam = self._get_memobjs(mem.number)
> +
> + _bf.set_raw("\xFF")
> +
> +
> + if mem.empty:
> + _mem.set_raw("\xFF" * 16)
> + return
> +
> + #if _mem.get_raw() == ("\xFF" * 16):
> + _mem.set_raw("\x00" * 12 + "\xFF" * 2 + "\x00"*2)
> +
> + _bf.scanadd = int(mem.skip != "S")
> + _bf.band = 0x0F
> + for idx, ele in enumerate(VALID_BANDS):
> + if mem.freq >= ele[0] and mem.freq <= ele[1]:
> + _bf.band = idx
> +
> + _mem.freq = mem.freq / 10
> + _mem.offset = mem.offset / 10
> +
> + tx, rx = chirp_common.split_tone_encode(mem)
> + self._encode_tone(_mem, 'tx', *tx)
> + self._encode_tone(_mem, 'rx', *rx)
> +
> + _mem.duplex = DUPLEX.index(mem.duplex)
> + _mem.isnarrow = mem.mode == "NFM"
> + _mem.step = TGUV2P_STEPS.index(mem.tuning_step)
> +
> + if mem.power == None :
> + _mem.power = 0
> + else:
> + _mem.power = POWER_LEVELS.index(mem.power)
> +
> + if _nam:
> + for i in range(0, 6):
> + try:
> + _nam[i] = CHARSET.index(mem.name[i])
> + except IndexError:
> + _nam[i] = 0xFF
> +
> + for setting in mem.extra:
> + setattr(_mem, setting.get_name(), setting.value)
> +
> + def get_settings(self):
> + _settings = self._memobj.settings
> + _vfoa = self._memobj.vfos[0]
> + _vfob = self._memobj.vfos[1]
> + _bandsettings = self._memobj.bands
> +
> +
> + cfg_grp = RadioSettingGroup("cfg_grp", "Configuration")
> + vfoa_grp = RadioSettingGroup("vfoa_grp", "VFO A Settings")
> + vfob_grp = RadioSettingGroup("vfob_grp", "VFO B Settings")
> +
> +
> + group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp)
> + #
> + # Configuration Settings
> + #
> + options = ["Off"] + ["%s min" % x for x in range(1, 10)]
> + rs = RadioSetting("timeout", "Time Out Timer",
> + RadioSettingValueList(
> + options, options[_settings.time_out_timer]))
> + cfg_grp.append(rs)
> +
> + options = ["Frequency", "Channel", "Name"]
> + rs = RadioSetting("isplay", "Channel Display Moe",
> + RadioSettingValueList(
> + options, options[_settings.display]))
> + cfg_grp.append(rs)
> +
> + rs = RadioSetting("squelch", "Squelch Level",
> + RadioSettingValueInteger(0, 9,
> _settings.squelch))
> + cfg_grp.append(rs)
> +
> + if _settings.vox == 0:
> + rs = RadioSetting("vox", "VOX",
> + RadioSettingValueString(0,10,"Off"))
> + cfg_grp.append(rs)
> + else:
> + rs = RadioSetting("vox", "VOX Level",
> + RadioSettingValueInteger(1, 9,
> _settings.vox))
> + cfg_grp.append(rs)
> +
> + rs = RadioSetting("beep_tone_disabled", "Beep Prompt",
> + RadioSettingValueBoolean(
> + not _settings.beep_tone_disabled))
> + cfg_grp.append(rs)
> +
> + options = ["Dual Watch", "CrossBand", "Normal"]
> + if _settings.rxmode >=2:
> + _rxmode = 2
> + else:
> + _rxmode = _settings.rxmode
> + rs = RadioSetting("RX mode", "Dual Watch/CrossBand Monitor",
> + RadioSettingValueList(
> + options, options[_rxmode]))
> + cfg_grp.append(rs)
> +
> + rs = RadioSetting("bcl", "Busy Channel Lock",
> + RadioSettingValueBoolean(
> + not _settings.busy_lockout))
> + cfg_grp.append(rs)
> +
> + rs = RadioSetting("keylock", "Keypad Lock",
> + RadioSettingValueBoolean(
> + not _settings.keyunlocked))
> + cfg_grp.append(rs)
> +
> + if _settings.priority_channel >= 200:
> + rs = RadioSetting("pri_ch", "Priority Channel",
> + RadioSettingValueString(0,10,"Not Set"))
> + cfg_grp.append(rs)
> + else:
> + rs = RadioSetting("pri_ch", "Priority Channel",
> + RadioSettingValueInteger(0, 199,
> _settings.priority_channel))
> + cfg_grp.append(rs)
> +
> + #
> + # VFO Settings
> + #
> +
> + vfo_groups = [vfoa_grp, vfob_grp]
> + vfo_mem = [_vfoa, _vfob]
> + vfo_lower = ["vfoa", "vfob"]
> + vfo_upper = ["VFOA", "VFOB"]
> +
> + for idx,vfo_group in enumerate(vfo_groups):
> +
> + options = ["Channel", "Frequency"]
> + tempvar = 0 if (vfo_mem[idx].current < 200) else 1
> + rs = RadioSetting(vfo_lower[idx]+"_mode", vfo_upper[idx]+"
> Mode",
> + RadioSettingValueList(
> + options, options[tempvar]))
> + vfo_group.append(rs)
> +
> + if tempvar == 0:
> + rs = RadioSetting(vfo_lower[idx]+"_ch", vfo_upper[idx]+"
> Channel",
> + RadioSettingValueInteger(0, 199,
> vfo_mem[idx].current))
> + vfo_group.append(rs)
> + else:
> + band_num = vfo_mem[idx].current - 200
> + freq = int(_bandsettings[band_num].freq) * 10
> + offset = int(_bandsettings[band_num].offset) * 10
> + txtmode = _bandsettings[band_num].txtmode
> + rxtmode = _bandsettings[band_num].rxtmode
> +
> + rs = RadioSetting(vfo_lower[idx]+"_freq",
> vfo_upper[idx]+" Frequency",
> + RadioSettingValueFloat(0.0, 520.0, freq
> / 1000000.0, precision=6))
> + vfo_group.append(rs)
> +
> + if offset > 70e6:
> + offset = 0
> + rs = RadioSetting(vfo_lower[idx]+"_offset",
> vfo_upper[idx]+" Offset",
> + RadioSettingValueFloat(0.0, 69.995,
> offset / 100000.0, resolution= 0.005))
> + vfo_group.append(rs)
> +
> + rs = RadioSetting(vfo_lower[idx]+"_duplex",
> vfo_upper[idx]+" Shift",
> + RadioSettingValueList(
> + DUPLEX,
> DUPLEX[_bandsettings[band_num].duplex]))
> + vfo_group.append(rs)
> +
> + rs = RadioSetting(vfo_lower[idx]+"_step",
> vfo_upper[idx]+" Step",
> + RadioSettingValueFloat(
> + 0.0, 1000.0,
> TGUV2P_STEPS[_bandsettings[band_num].step], resolution=0.25))
> + vfo_group.append(rs)
> +
> + rs = RadioSetting(vfo_lower[idx]+"_pwr", vfo_upper[idx]+"
> Power",
> + RadioSettingValueList(
> + POWER_LEVELS_STR,
> POWER_LEVELS_STR[_bandsettings[band_num].power]))
> + vfo_group.append(rs)
> +
> + options = ["None", "Tone", "DTCS-N", "DTCS-I"]
> + rs = RadioSetting(vfo_lower[idx]+"_ttmode",
> vfo_upper[idx]+" TX tone mode",
> + RadioSettingValueList( options,
> options[txtmode]))
> + vfo_group.append(rs)
> + if txtmode == 1:
> + rs = RadioSetting(vfo_lower[idx]+"_ttone",
> vfo_upper[idx]+" TX tone",
> + RadioSettingValueFloat(
> + 0.0, 1000.0,
> chirp_common.TONES[_bandsettings[band_num].txtone], resolution=0.1))
> + vfo_group.append(rs)
> + elif txtmode >= 2:
> + txtone = _bandsettings[band_num].txtone
> + rs = RadioSetting(vfo_lower[idx]+"_tdtcs",
> vfo_upper[idx]+" TX DTCS",
> + RadioSettingValueInteger(
> + 0, 1000,
> chirp_common.DTCS_CODES[txtone]))
> + vfo_group.append(rs)
> +
> + options = ["None", "Tone", "DTCS-N", "DTCS-I" ]
> + rs = RadioSetting(vfo_lower[idx]+"_rtmode",
> vfo_upper[idx]+" RX tone mode",
> + RadioSettingValueList( options,
> options[rxtmode]))
> + vfo_group.append(rs)
> +
> + if rxtmode == 1:
> + rs = RadioSetting(vfo_lower[idx]+"_rtone",
> vfo_upper[idx]+" RX tone",
> + RadioSettingValueFloat(
> + 0.0, 1000.0,
> chirp_common.TONES[_bandsettings[band_num].rxtone], resolution=0.1))
> + vfo_group.append(rs)
> + elif rxtmode >= 2:
> + rxtone = _bandsettings[band_num].rxtone
> + rs = RadioSetting(vfo_lower[idx]+"_rdtcs",
> vfo_upper[idx]+" TX rTCS",
> + RadioSettingValueInteger(
> + 0, 1000,
> chirp_common.DTCS_CODES[rxtone]))
> + vfo_group.append(rs)
> +
> +
> + options = ["FM", "NFM"]
> + rs = RadioSetting(vfo_lower[idx]+"_fm", vfo_upper[idx]+"
> FM BW ",
> + RadioSettingValueList(
> + options,
> options[_bandsettings[band_num].isnarrow]))
> + vfo_group.append(rs)
> +
> + return group
> +
> +
> + @classmethod
> + def match_model(cls, filedata, filename):
> + return (filedata.startswith("TG-UV2+ Radio Program Data") and
> + len(filedata) == (cls._memsize + 0x30))
> diff --git a/tests/images/Quansheng_TG-UV2+.img
> b/tests/images/Quansheng_TG-UV2+.img
> new file mode 100644
> index
> 0000000000000000000000000000000000000000..71ed9aaf9a7e7f7e2b8f6846469f0a3b4d12fc22
> GIT binary patch
> literal 8417
> zc%1E*Pj3=I7>8$ef!#gW+S(W!JaB<M7!7Qr?Zw0j6qgiRs$GOr2dM0{{Aob8<)$9=
> z0~il}q((oBNz)IoE|BWZ4=*MraF{0{n`hp6^ZuCK!|v<C!$Ze<kT_sX-b?KFd#yg}
> zBy#K>OL&<_X at dB`PO`o~Uf1<;O(i?Y&!VSR!8gI7KL!0J^bcYG5cYep--CG`^n1{M
> z4*kiAhI}Y`ihw7<p+5!vCiI(PKc08)D3<(FrzCfDZzy^O>RG5eP<NsJ0qS4GI)nKP
> z<};YjU_OKS4CXVK&&2up)$n=EK9BPz`zg5J9wCOYqKxycRf71jkQb(Y+gPECm|r51
> zNF)-8WYN&yPv4mc`4L<Vvi9u8{jn=s- at b;O<UihgxlH$$q5jVSt_RnD!Jl3E-EejF
> zKmPcl5T#o18t(gwF;P|hetGM1&Y9-Oa{RtYP0<x_6+8kS1&@Ji;5xVgP6;>#SHM;9
> z2zV4c2CjkY;D+c5<WnG at 63U0H-;JAuf-B%EcmzBO9s}3Fb#O!U at Oe)h@=?e~As>Z&
> z6!L}7`_9tR`f}V~SmGKn&Cv50r%};0Q*enyB9TZW5{X12kt`;}Z at 9dF_LFy7gdUHI
> zR;}{*w7y~UJ-*Ek-#Bi)`?43Tb!~p$vO5{CR&WOPNv~zM4_ at p#PNl>Ld^fwztCoA#
> z=sthj>^P%_)%NP#eqv{fr`1Y(aQrGgD&_3pm%rG{+Jj(yRJAVL#ztvoj+1XX&PlO6
> Tnb)nkjZQnLl_&e`Zu;pj#yjz3
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://intrepid.danplanet.com/pipermail/chirp_devel/attachments/20220209/2867277a/attachment-0001.html
More information about the chirp_devel
mailing list