[chirp_devel] [PATCH] [UV-50X3] Add Support for BTech UV-50X3
# HG changeset patch # User Jim Unroe rock.unroe@gmail.com # Date 1468366999 14400 # Node ID b717d6b593bcb8e9f170e571173c51129a6a62da # Parent 971c5f5430d90e7bfe37b8d9235790ae5cae7d9f [UV-50X3] Add Support for BTech UV-50X3
This patch adds basic support (500 left memories/500 right memories) plus all per-channel settings. It also and exposes the structures that will be used to add settings in future patches.
related to #3815
diff -r 971c5f5430d9 -r b717d6b593bc chirp/drivers/vgc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/drivers/vgc.py Tue Jul 12 19:43:19 2016 -0400 @@ -0,0 +1,828 @@ +# Copyright 2016: +# * Jim Unroe KC9HI, rock.unroe@gmail.com +# * Pavel Milanes CO7WT pavelmc@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 time +import struct +import logging +import re + +LOG = logging.getLogger(__name__) + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings +from textwrap import dedent + +MEM_FORMAT = """ +struct mem { + lbcd rxfreq[4]; + lbcd txfreq[4]; + lbcd rxtone[2]; + lbcd txtone[2]; + u8 unknown0:2, + txp:2, + wn:2, + unknown1:1, + bcl:1; + u8 unknown2:2, + revert:1, + dname:1, + unknown3:4; + u8 unknown4[2]; +}; + +struct nam { + char name[6]; + u8 unknown1[2]; +}; + +#seekto 0x0000; +struct mem left_memory[500]; + +#seekto 0x2000; +struct mem right_memory[500]; + +#seekto 0x4000; +struct nam left_names[500]; + +#seekto 0x5000; +struct nam right_names[500]; + +#seekto 0x6000; +u8 left_usedflags[64]; + +#seekto 0x6040; +u8 left_scanflags[64]; + +#seekto 0x6080; +u8 right_usedflags[64]; + +#seekto 0x60C0; +u8 right_scanflags[64]; + +#seekto 0x6160; +struct { + char line32[32]; +} embedded_msg; + +#seekto 0x6180; +struct { + u8 sbmute:2, // sub band mute + unknown1:1, + workmodb:1, // work mode (right side) + dw:1, // dual watch + audio:1, // audio output mode (stereo/mono) + unknown2:1, + workmoda:1; // work mode (left side) + u8 scansb:1, // scan stop beep + aftone:3, // af tone control + scand:1, // scan directon + scanr:3; // scan resume + u8 rxexp:1, // rx expansion + ptt:1, // ptt mode + display:1, // display select (frequency/clock) + omode:1, // operaton mode + beep:2, // beep volume + spkr:2; // speaker + u8 cpuclk:1, // operating mode(cpu clock) + fkey:3, // fkey function + mrscan:1, // memory scan type + color:3; // lcd backlight color + u8 vox:2, // vox + voxs:3, // vox sensitivity + mgain:3; // mic gain + u8 wbandb:4, // work band (right side) + wbanda:4; // work band (left side) + u8 sqlb:4, // squelch level (right side) + sqla:4; // squelch level (left side) + u8 apo:4, // auto power off + ars:1, // automatic repeater shift + tot:3; // time out timer + u8 stepb:4, // auto step (right side) + stepa:4; // auto step (left side) + u8 rxcoverm:1, // rx coverage-memory + lcdc:3, // lcd contrast + rxcoverv:1, // rx coverage-vfo + lcdb:3; // lcd brightness + u8 smode:1, // smart function mode + timefmt:1, // time format + datefmt:2, // date format + timesig:1, // time signal + keyb:3; // key/led brightness + u8 dwstop:1, // dual watch stop + unknown3:1, + sqlexp:1, // sql expansion + decbandsel:1, // decoding band select + dtmfmodenc:1, // dtmf mode encode + bell:3; // bell ringer + u8 unknown4:2, + btime:6; // lcd backlight time + u8 unknown5:2, + tz:6; // time zone + u8 unknown618E; + u8 unknown618F; + ul16 offseta; // work offset (left side) + ul16 offsetb; // work offset (right side) + ul16 mrcha; // selected memory channel (left) + ul16 mrchb; // selected memory channel (right) + ul16 wpricha; // work priority channel (left) + ul16 wprichb; // work priority channel (right) + u8 unknown6:3, + datasql:2, // data squelch + dataspd:1, // data speed + databnd:2; // data band select + u8 unknown7:1, + pfkey2:3, // mic p2 key + unknown8:1, + pfkey1:3; // mic p1 key + u8 unknown9:1, + pfkey4:3, // mic p4 key + unknowna:1, + pfkey3:3; // mic p3 key + u8 unknownb:7, + dtmfmoddec:1; // dtmf mode decode +} settings; + +#seekto 0x61B0; +struct { + char line16[16]; +} poweron_msg; + +#seekto 0x6300; +struct { + u8 unknown1:3, + ttdgt:5; // dtmf digit time + u8 unknown2:3, + ttint:5; // dtmf interval time + u8 unknown3:3, + tt1stdgt:5; // dtmf 1st digit time + u8 unknown4:3, + tt1stdly:5; // dtmf 1st digit delay + u8 unknown5:3, + ttdlyqt:5; // dtmf delay when use qt + u8 unknown6:3, + ttdkey:5; // dtmf d key function + u8 unknown7; + u8 unknown8:4, + ttautod:4; // dtmf auto dial group +} dtmf; + +#seekto 0x6330; +struct { + u8 unknown1:7, + ttsig:1; // dtmf signal + u8 unknown2:4, + ttintcode:4; // dtmf interval code + u8 unknown3:5, + ttgrpcode:3; // dtmf group code + u8 unknown4:4, + ttautorst:4; // dtmf auto reset time + u8 unknown5:5, + ttalert:3; // dtmf alert tone/transpond +} dtmf2; + +#seekto 0x6360; +struct { + u8 code1[8]; // dtmf code + u8 code1_len; // dtmf code length + u8 unknown1[7]; + u8 code2[8]; // dtmf code + u8 code2_len; // dtmf code length + u8 unknown2[7]; + u8 code3[8]; // dtmf code + u8 code3_len; // dtmf code length + u8 unknown3[7]; + u8 code4[8]; // dtmf code + u8 code4_len; // dtmf code length + u8 unknown4[7]; + u8 code5[8]; // dtmf code + u8 code5_len; // dtmf code length + u8 unknown5[7]; + u8 code6[8]; // dtmf code + u8 code6_len; // dtmf code length + u8 unknown6[7]; + u8 code7[8]; // dtmf code + u8 code7_len; // dtmf code length + u8 unknown7[7]; + u8 code8[8]; // dtmf code + u8 code8_len; // dtmf code length + u8 unknown8[7]; + u8 code9[8]; // dtmf code + u8 code9_len; // dtmf code length + u8 unknown9[7]; +} dtmfcode; + +""" + +MEM_SIZE = 0x8000 +BLOCK_SIZE = 0x40 +MODES = ["FM", "Auto", "NFM", "AM"] +SKIP_VALUES = ["", "S"] +TONES = chirp_common.TONES +DTCS_CODES = chirp_common.DTCS_CODES +NAME_LENGTH = 6 +DTMF_CHARS = list("0123456789ABCD*#") +STIMEOUT = 1 + +# valid chars on the LCD +VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "`{|}!"#$%&'()*+,-./:;<=>?@[]^_" + +# Power Levels +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Mid", watts=20), + chirp_common.PowerLevel("High", watts=50)] + +# B-TECH UV-50X3 id string +UV50X3_id = "VGC6600MD" + + +def _clean_buffer(radio): + radio.pipe.timeout = 0.005 + junk = radio.pipe.read(256) + radio.pipe.timeout = STIMEOUT + if junk: + Log.debug("Got %i bytes of junk before starting" % len(junk)) + + +def _check_for_double_ack(radio): + radio.pipe.timeout = 0.005 + c = radio.pipe.read(1) + radio.pipe.timeout = STIMEOUT + if c and c != '\x06': + _exit_program_mode(radio) + raise errors.RadioError('Expected nothing or ACK, got %r' % c) + + +def _rawrecv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + except: + _exit_program_mode(radio) + msg = "Generic error reading data from radio; check your cable." + raise errors.RadioError(msg) + + if len(data) != amount: + _exit_program_mode(radio) + msg = "Error reading data from radio: not the amount of data we want." + raise errors.RadioError(msg) + + return data + + +def _rawsend(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + except: + raise errors.RadioError("Error sending data to radio") + + +def _make_frame(cmd, addr, length, data=""): + """Pack the info in the headder format""" + frame = struct.pack(">BHB", ord(cmd), addr, length) + # add the data if set + if len(data) != 0: + frame += data + # return the data + return frame + + +def _recv(radio, addr, length=BLOCK_SIZE): + """Get data from the radio """ + # read 4 bytes of header + hdr = _rawrecv(radio, 4) + + # check for unexpected extra command byte + c, a, l = struct.unpack(">BHB", hdr) + if hdr[0:2] == "WW" and a != addr: + # extra command byte detected + # throw away the 1st byte and add the next byte in the buffer + hdr = hdr[1:] + _rawrecv(radio, 1) + + # read 64 bytes (0x40) of data + data = _rawrecv(radio, (BLOCK_SIZE)) + + # DEBUG + LOG.info("Response:") + LOG.debug(util.hexprint(hdr + data)) + + c, a, l = struct.unpack(">BHB", hdr) + if a != addr or l != length or c != ord("W"): + _exit_program_mode(radio) + LOG.error("Invalid answer for block 0x%04x:" % addr) + LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (c, a, l)) + raise errors.RadioError("Unknown response from the radio") + + return data + + +def _do_ident(radio): + """Put the radio in PROGRAM mode & identify it""" + # set the serial discipline + radio.pipe.baudrate = 115200 + radio.pipe.parity = "N" + radio.pipe.timeout = STIMEOUT + + # flush input buffer + _clean_buffer(radio) + + magic = "V66LINK" + + _rawsend(radio, magic) + + # Ok, get the ident string + ident = _rawrecv(radio, 9) + + # check if ident is OK + if ident != radio.IDENT: + # bad ident + msg = "Incorrect model ID, got this:" + msg += util.hexprint(ident) + LOG.debug(msg) + raise errors.RadioError("Radio identification failed.") + + # DEBUG + LOG.info("Positive ident, got this:") + LOG.debug(util.hexprint(ident)) + + return True + + +def _exit_program_mode(radio): + endframe = "\x45" + _rawsend(radio, endframe) + + +def _download(radio): + """Get the memory map""" + + # put radio in program mode and identify it + _do_ident(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data = "" + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + frame = _make_frame("R", addr, BLOCK_SIZE) + # DEBUG + LOG.info("Request sent:") + LOG.debug(util.hexprint(frame)) + + # sending the read request + _rawsend(radio, frame) + + # now we read + d = _recv(radio, addr) + + # aggregate the data + data += d + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + return data + + +def _upload(radio): + """Upload procedure""" + + MEM_SIZE = 0x7000 + + # put radio in program mode and identify it + _do_ident(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # the fun start here + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + # sending the data + data = radio.get_mmap()[addr:addr + BLOCK_SIZE] + + frame = _make_frame("W", addr, BLOCK_SIZE, data) + + _rawsend(radio, frame) + + # receiving the response + ack = _rawrecv(radio, 1) + if ack != "\x06": + _exit_program_mode(radio) + msg = "Bad ack writing block 0x%04x" % addr + raise errors.RadioError(msg) + + _check_for_double_ack(radio) + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = data[0x6140:0x6148] + + #if rid in cls._fileid: + if rid in cls.IDENT: + return True + + return False + + +class VGCStyleRadio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """BTECH's UV-50X3""" + VENDOR = "BTECH" + _air_range = (108000000, 136000000) + _vhf_range = (136000000, 174000000) + _vhf2_range = (174000000, 250000000) + _220_range = (222000000, 225000000) + _gen1_range = (300000000, 400000000) + _uhf_range = (400000000, 480000000) + _gen2_range = (480000000, 520000000) + _upper = 499 + MODEL = "" + IDENT = "" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('The UV-50X3 driver is a beta version.\n' + '\n' + 'Please save an unedited copy of your first successful\n' + 'download to a CHIRP Radio Images(*.img) file.' + ) + rp.pre_download = _(dedent("""\ + Follow this instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = False + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.has_sub_devices = self.VARIANT == "" + rf.valid_modes = MODES + rf.valid_characters = VALID_CHARS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_skips = SKIP_VALUES + rf.valid_name_length = NAME_LENGTH + rf.valid_dtcs_codes = DTCS_CODES + rf.valid_bands = [self._air_range, + self._vhf_range, + self._vhf2_range, + self._220_range, + self._gen1_range, + self._uhf_range, + self._gen2_range] + rf.memory_bounds = (0, self._upper) + return rf + + def get_sub_devices(self): + return [UV50X3Left(self._mmap), UV50X3Right(self._mmap)] + + def sync_in(self): + """Download from radio""" + try: + data = _download(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except: + # 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 = memmap.MemoryMap(data) + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + try: + _upload(self) + except: + # 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 process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + if val.get_raw() == "\xFF\xFF": + return '', None, None + + val = int(val) + if val >= 12000: + a = val - 12000 + return 'DTCS', a, 'R' + elif val >= 8000: + a = val - 8000 + return 'DTCS', a, 'N' + else: + a = val / 10.0 + return 'Tone', a, None + + def encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def _memory_obj(self, suffix=""): + return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix)) + + def _name_obj(self, suffix=""): + return getattr(self._memobj, "%s_names%s" % (self._vfo, suffix)) + + def _scan_obj(self, suffix=""): + return getattr(self._memobj, "%s_scanflags%s" % (self._vfo, suffix)) + + def _used_obj(self, suffix=""): + return getattr(self._memobj, "%s_usedflags%s" % (self._vfo, suffix)) + + def get_memory(self, number): + """Get the mem representation from the radio image""" + bitpos = (1 << (number % 8)) + bytepos = (number / 8) + + _mem = self._memory_obj()[number] + _names = self._name_obj()[number] + _scn = self._scan_obj()[bytepos] + _usd = self._used_obj()[bytepos] + + isused = bitpos & int(_usd) + isscan = bitpos & int(_scn) + + # Create a high-level memory object to return to the UI + mem = chirp_common.Memory() + + # Memory number + mem.number = number + + if not isused: + mem.empty = True + return mem + + # Freq and offset + mem.freq = int(_mem.rxfreq) * 10 + # tx freq can be blank + if _mem.get_raw()[4] == "\xFF": + # TX freq not set + mem.offset = 0 + mem.duplex = "off" + else: + # TX feq set + offset = (int(_mem.txfreq) * 10) - mem.freq + if offset < 0: + mem.offset = abs(offset) + mem.duplex = "-" + elif offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + # skip + if not isscan: + mem.skip = "S" + + # name TAG of the channel + mem.name = str(_names.name).strip("\xFF") + + # power + mem.power = POWER_LEVELS[int(_mem.txp)] + + # wide/narrow + mem.mode = MODES[int(_mem.wn)] + + # tone data + rxtone = txtone = None + txtone = self.decode_tone(_mem.txtone) + rxtone = self.decode_tone(_mem.rxtone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + # Extra + mem.extra = RadioSettingGroup("extra", "Extra") + + bcl = RadioSetting("bcl", "Busy channel lockout", + RadioSettingValueBoolean(bool(_mem.bcl))) + mem.extra.append(bcl) + + revert = RadioSetting("revert", "Revert", + RadioSettingValueBoolean(bool(_mem.revert))) + mem.extra.append(revert) + + dname = RadioSetting("dname", "Display name", + RadioSettingValueBoolean(bool(_mem.dname))) + mem.extra.append(dname) + + return mem + + def set_memory(self, mem): + """Set the memory data in the eeprom img from the UI""" + bitpos = (1 << (mem.number % 8)) + bytepos = (mem.number / 8) + + _mem = self._memory_obj()[mem.number] + _names = self._name_obj()[mem.number] + _scn = self._scan_obj()[bytepos] + _usd = self._used_obj()[bytepos] + + if mem.empty: + _usd &= ~bitpos + _scn &= ~bitpos + _mem.set_raw("\xFF" * 16) + _names.name = ("\xFF" * 6) + return + else: + _usd |= bitpos + + # frequency + _mem.rxfreq = mem.freq / 10 + + # duplex + if mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "off": + for i in _mem.txfreq: + i.set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + else: + _mem.txfreq = mem.freq / 10 + + # tone data + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self.encode_tone(_mem.txtone, txmode, txtone, txpol) + self.encode_tone(_mem.rxtone, rxmode, rxtone, rxpol) + + # name TAG of the channel + _names.name = mem.name.ljust(6, "\xFF") + + # power level, # default power level is low + _mem.txp = 0 if mem.power is None else POWER_LEVELS.index(mem.power) + + # wide/narrow + _mem.wn = MODES.index(mem.mode) + + if mem.skip == "S": + _scn &= ~bitpos + else: + _scn |= bitpos + + # autoset display to display name if filled + if mem.extra: + # mem.extra only seems to be populated when called from edit panel + dname = mem.extra["dname"] + else: + dname = None + if mem.name: + _mem.dname = True + if dname and not dname.changed(): + dname.value = True + else: + _mem.dname = False + if dname and not dname.changed(): + dname.value = False + + # reseting unknowns, this has to be set by hand + _mem.unknown0 = 0 + _mem.unknown1 = 0 + _mem.unknown2 = 0 + _mem.unknown3 = 0 + + # extra settings + if len(mem.extra) > 0: + # there are setting, parse + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + else: + # there are no extra settings, load defaults + _mem.bcl = 0 + _mem.revert = 0 + _mem.dname = 1 + + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == MEM_SIZE: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + +@directory.register +class UV50X3(VGCStyleRadio): + """BTech UV-50X3""" + MODEL = "UV-50X3" + IDENT = UV50X3_id + + +class UV50X3Left(UV50X3): + VARIANT = "Left" + _vfo = "left" + + +class UV50X3Right(UV50X3): + VARIANT = "Right" + _vfo = "right"
# HG changeset patch # User Jim Unroe rock.unroe@gmail.com # Date 1468366999 14400 # Node ID b717d6b593bcb8e9f170e571173c51129a6a62da # Parent 971c5f5430d90e7bfe37b8d9235790ae5cae7d9f [UV-50X3] Add Support for BTech UV-50X3
This patch adds basic support (500 left memories/500 right memories) plus all per-channel settings. It also and exposes the structures that will be used to add settings in future patches.
Thanks, can you make sure this makes it to the supported list on the wiki?
--Dan
Certainly. Thanks for the reminder.
Jim
On Tue, Jul 12, 2016 at 8:16 PM, Dan Smith dsmith@danplanet.com wrote:
# HG changeset patch # User Jim Unroe rock.unroe@gmail.com # Date 1468366999 14400 # Node ID b717d6b593bcb8e9f170e571173c51129a6a62da # Parent 971c5f5430d90e7bfe37b8d9235790ae5cae7d9f [UV-50X3] Add Support for BTech UV-50X3
This patch adds basic support (500 left memories/500 right memories) plus all per-channel settings. It also and exposes the structures that will be used to add settings in future patches.
Thanks, can you make sure this makes it to the supported list on the wiki?
--Dan
participants (2)
-
Dan Smith
-
Jim Unroe