Image attached for testing: Baofeng_BF-T8.img
---------- Forwarded message --------- From: Jim Unroe kc9hi@comcast.net Date: Wed, May 19, 2021 at 9:00 PM Subject: [PATCH] [BF-T8] add support for the Baofeng BF-T8 To: Rock.Unroe@gmail.com
# HG changeset patch # User Jim Unroe rock.unroe@gmail.com # Date 1621472333 14400 # Wed May 19 20:58:53 2021 -0400 # Node ID ccf8f96034cd9dbb6f3462bcf10d68526c3e0572 # Parent b04ba05b7b646e144af4f074663941ca122dc4ed [BF-T8] add support for the Baofeng BF-T8
This patch adds support for the Baofeng BF-T8 (and it variants)
related to #8263
diff -r b04ba05b7b64 -r ccf8f96034cd chirp/drivers/bf-t8.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/drivers/bf-t8.py Wed May 19 20:58:53 2021 -0400 @@ -0,0 +1,702 @@ +# Copyright 2021 Jim Unroe rock.unroe@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 os +import struct +import logging + +from chirp import ( + bitwise, + chirp_common, + directory, + errors, + memmap, + util, +) +from chirp.settings import ( + RadioSetting, + RadioSettingGroup, + RadioSettings, + RadioSettingValueBoolean, + RadioSettingValueFloat, + RadioSettingValueInteger, + RadioSettingValueList, + RadioSettingValueString, +) + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0000; +struct { + lbcd rxfreq[4]; // RX Frequency + lbcd txfreq[4]; // TX Frequency + u8 rx_tmode; // RX Tone Mode + u8 rx_tone; // PL/DPL Decode + u8 tx_tmode; // TX Tone Mode + u8 tx_tone; // PL/DPL Encode + u8 unknown1:3, // + skip:1, // Scan Add: 1 = Skip, 0 = Scan + unknown2:2, + isnarrow:1, // W/N: 1 = Narrow, 0 = Wide + lowpower:1; // TX Power: 1 = Low, 0 = High + u8 unknown3[3]; // +} memory[99]; + +#seekto 0x0630; +struct { + u8 squelch; // SQL + u8 vox; // Vox Lv + u8 tot; // TOT + u8 unk1:3, // + ste:1, // Tail Clear + bcl:1, // BCL + save:1, // Save + tdr:1, // TDR + beep:1; // Beep + u8 voice; // Voice + u8 abr; // Back Light + u8 ring; // Ring + u8 unknown; // + u8 mra; // MR Channel A + u8 mrb; // MR Channel B + u8 disp_ab; // Display A/B Selected + ul16 fmcur; // Broadcast FM station + u8 workmode; // Work Mode + u8 wx; // NOAA WX ch# + u8 area; // Area Selected +} settings; +""" + +CMD_ACK = "\x06" + +TONES = chirp_common.TONES +TMODES = ["", "Tone", "DTCS", "DTCS"] + +AB_LIST = ["A", "B"] +ABR_LIST = ["OFF", "ON", "Key"] +AREA_LIST = ["China", "Japan", "Korea", "Malaysia", "American", + "Australia", "Iran", "Taiwan", "Europe", "Russia"] +MDF_LIST = ["Frequency", "Channel #", "Name"] +RING_LIST = ["OFF"] + ["%s" % x for x in range(1, 11)] +TOT_LIST = ["OFF"] + ["%s seconds" % x for x in range(30, 210, 30)] +VOICE_LIST = ["Off", "Chinese", "English"] +VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 6)] +WORKMODE_LIST = ["General", "PMR"] +WX_LIST = ["CH01 - 162.550", + "CH02 - 162.400", + "CH03 - 162.475", + "CH04 - 162.425", + "CH05 - 162.450", + "CH06 - 162.500", + "CH07 - 162.525" + ] + +SETTING_LISTS = { + "ab": AB_LIST, + "abr": ABR_LIST, + "area": AREA_LIST, + "mdf": MDF_LIST, + "ring": RING_LIST, + "tot": TOT_LIST, + "voice": VOICE_LIST, + "vox": VOX_LIST, + "workmode": WORKMODE_LIST, + "wx": WX_LIST, + } + +FRS_FREQS1 = [462.5625, 462.5875, 462.6125, 462.6375, 462.6625, + 462.6875, 462.7125] +FRS_FREQS2 = [467.5625, 467.5875, 467.6125, 467.6375, 467.6625, + 467.6875, 467.7125] +FRS_FREQS3 = [462.5500, 462.5750, 462.6000, 462.6250, 462.6500, + 462.6750, 462.7000, 462.7250] +FRS_FREQS = FRS_FREQS1 + FRS_FREQS2 + FRS_FREQS3 + + +def _enter_programming_mode(radio): + serial = radio.pipe + + exito = False + for i in range(0, 5): + serial.write(radio._magic) + ack = serial.read(1) + + try: + if ack == CMD_ACK: + exito = True + break + except: + LOG.debug("Attempt #%s, failed, trying again" % i) + pass + + # check if we had EXITO + if exito is False: + _exit_programming_mode(radio) + msg = "The radio did not accept program mode after five tries.\n" + msg += "Check you interface cable and power cycle your radio." + raise errors.RadioError(msg) + + try: + serial.write("\x02") + ident = serial.read(len(radio._fingerprint)) + except: + _exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if not ident == radio._fingerprint: + _exit_programming_mode(radio) + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio returned unknown identification string") + + try: + serial.write(CMD_ACK) + ack = serial.read(1) + except: + _exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + _exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode") + + +def _exit_programming_mode(radio): + serial = radio.pipe + try: + serial.write("E") + except: + raise errors.RadioError("Radio refused to exit programming mode") + + +def _read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'R', block_addr, block_size) + expectedresponse = "W" + cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + serial.write(cmd) + response = serial.read(4 + block_size) + if response[:4] != expectedresponse: + raise Exception("Error reading block %04x." % (block_addr)) + + block_data = response[4:] + + serial.write(CMD_ACK) + ack = serial.read(1) + except: + _exit_programming_mode(radio) + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + if ack != CMD_ACK: + _exit_programming_mode(radio) + raise Exception("No ACK reading block %04x." % (block_addr)) + + return block_data + + +def _write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'W', block_addr, block_size) + data = radio.get_mmap()[block_addr:block_addr + block_size] + + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(cmd + data)) + + try: + serial.write(cmd + data) + if serial.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + _exit_programming_mode(radio) + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + + +def do_download(radio): + LOG.debug("download") + _enter_programming_mode(radio) + + data = "" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, radio.BLOCK_SIZE): + status.cur = addr + radio.BLOCK_SIZE + radio.status_fn(status) + + block = _read_block(radio, addr, radio.BLOCK_SIZE) + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + _exit_programming_mode(radio) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _enter_programming_mode(radio) + + status.cur = 0 + status.max = radio._memsize + + for start_addr, end_addr in radio._ranges: + for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP): + status.cur = addr + radio.BLOCK_SIZE_UP + radio.status_fn(status) + _write_block(radio, addr, radio.BLOCK_SIZE_UP) + + _exit_programming_mode(radio) + + +class BFT8Radio(chirp_common.CloneModeRadio): + """Baofeng BF-T8""" + VENDOR = "Baofeng" + MODEL = "BF-T8" + BAUD_RATE = 9600 + BLOCK_SIZE = BLOCK_SIZE_UP = 0x10 + ODD_SPLIT = True + HAS_NAMES = False + SKIP_VALUES = [] + DTCS_CODES = sorted(chirp_common.DTCS_CODES) + + POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00), + chirp_common.PowerLevel("Low", watts=0.50)] + + _magic = "\x02" + "PROGRAM" + _fingerprint = "\x2E" + "BF-T6" + "\x2E" + _upper = 99 + _frs = _upper == 22 + + _ranges = [ + (0x0000, 0x0B60), + ] + _memsize = 0x0B60 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_ctone = True + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_tuning_step = False + rf.can_odd_split = self.ODD_SPLIT + rf.has_name = self.HAS_NAMES + rf.valid_skips = self.SKIP_VALUES + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_dtcs_codes = self.DTCS_CODES + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_modes = ["FM", "NFM"] # 25 kHz, 12.5 KHz. + rf.memory_bounds = (1, self._upper) + rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.] + rf.valid_bands = [(400000000, 470000000)] + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def validate_memory(self, mem): + msgs = "" + msgs = chirp_common.CloneModeRadio.validate_memory(self, mem) + + _msg_freq = 'Memory location cannot change frequency' + _msg_simplex = 'Memory location only supports Duplex:(None)' + _msg_nfm = 'Memory location only supports Mode: NFM' + _msg_txp = 'Memory location only supports Power: Low' + + # FRS only models + if self._frs: + # range of memories with values set by FCC rules + if mem.freq != int(FRS_FREQS[mem.number - 1] * 1000000): + # warn user can't change frequency + msgs.append(chirp_common.ValidationError(_msg_freq)) + + # channels 1 - 22 are simplex only + if str(mem.duplex) != "": + # warn user can't change duplex + msgs.append(chirp_common.ValidationError(_msg_simplex)) + + # channels 1 - 22 are NFM only + if str(mem.mode) != "NFM": + # warn user can't change mode + msgs.append(chirp_common.ValidationError(_msg_nfm)) + + # channels 8 - 14 are low power NFM only + if mem.number >= 8 and mem.number <= 14: + if str(mem.power) != "Low": + # warn user can't change power + msgs.append(chirp_common.ValidationError(_msg_txp)) + + return msgs + + def sync_in(self): + """Download from radio""" + try: + data = do_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 = data + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + try: + do_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 get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_tone(self, mem, _mem): + rx_tone = tx_tone = None + + tx_tmode = TMODES[_mem.tx_tmode] + rx_tmode = TMODES[_mem.rx_tmode] + + if tx_tmode == "Tone": + tx_tone = TONES[_mem.tx_tone] + elif tx_tmode == "DTCS": + tx_tone = self.DTCS_CODES[_mem.tx_tone] + + if rx_tmode == "Tone": + rx_tone = TONES[_mem.rx_tone] + elif rx_tmode == "DTCS": + rx_tone = self.DTCS_CODES[_mem.rx_tone] + + tx_pol = _mem.tx_tmode == 0x03 and "R" or "N" + rx_pol = _mem.rx_tmode == 0x03 and "R" or "N" + + chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol), + (rx_tmode, rx_tone, rx_pol)) + + def _get_mem(self, number): + return self._memobj.memory[number] + + def get_memory(self, number): + _mem = self._get_mem(number - 1) + + mem = chirp_common.Memory() + + mem.number = number + mem.freq = int(_mem.rxfreq) * 10 + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + return mem + + if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.freq = 0 + mem.empty = True + mem.mode = "NFM" + if mem.number <= 22 and not self._frs: + LOG.debug("Initializing empty memory") + _mem.set_raw("\x00" * 13 + "\xFF" * 3) + FRS_FREQ = FRS_FREQS[mem.number - 1] * 1000000 + mem.freq = FRS_FREQ + if mem.number >= 8 and mem.number <= 14: + mem.power = "Low" + else: + mem.power = "High" + + return mem + + if _mem.get_raw() == ("\xFF" * 16): + LOG.debug("Initializing empty memory") + _mem.set_raw("\x00" * 13 + "\xFF" * 3) + + if int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + # wide/narrow + mem.mode = _mem.isnarrow and "NFM" or "FM" + + # tone data + self._get_tone(mem, _mem) + + # tx power + levels = self.POWER_LEVELS + try: + mem.power = levels[_mem.lowpower] + except IndexError: + LOG.error("Radio reported invalid power level %s (in %s)" % + (_mem.power, levels)) + mem.power = levels[0] + + if mem.number <= 22 and self._frs: + FRS_IMMUTABLE = ["freq", "duplex", "offset", "mode"] + if mem.number >= 8 and mem.number <= 14: + mem.immutable = FRS_IMMUTABLE + ["power"] + else: + mem.immutable = FRS_IMMUTABLE + + return mem + + def _set_tone(self, mem, _mem): + ((txmode, txtone, txpol), + (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) + + _mem.tx_tmode = TMODES.index(txmode) + _mem.rx_tmode = TMODES.index(rxmode) + if txmode == "Tone": + _mem.tx_tone = TONES.index(txtone) + elif txmode == "DTCS": + _mem.tx_tmode = txpol == "R" and 0x03 or 0x02 + _mem.tx_tone = self.DTCS_CODES.index(txtone) + if rxmode == "Tone": + _mem.rx_tone = TONES.index(rxtone) + elif rxmode == "DTCS": + _mem.rx_tmode = rxpol == "R" and 0x03 or 0x02 + _mem.rx_tone = self.DTCS_CODES.index(rxtone) + + def set_memory(self, mem): + _mem = self._get_mem(mem.number - 1) + + # if empty memmory + if mem.empty: + if mem.number <= 22 and self._frs: + _mem.set_raw("\xFF" * 8 + "\x00" * 5 + "\xFF" * 3) + FRS_FREQ = int(FRS_FREQS[mem.number - 1] * 100000) + _mem.rxfreq = _mem.txfreq = FRS_FREQ + _mem.isnarrow = True + if mem.number >= 8 and mem.number <= 14: + _mem.lowpower = True + else: + _mem.lowpower = False + else: + _mem.set_raw("\xFF" * 8 + "\x00" * 4 + "\x03" + "\xFF" * 3) + + return mem + + _mem.set_raw("\x00" * 13 + "\xFF" * 3) + + # frequency + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + # wide/narrow + _mem.isnarrow = mem.mode == "NFM" + + # tone data + self._set_tone(mem, _mem) + + # tx power + if mem.power: + _mem.lowpower = self.POWER_LEVELS.index(mem.power) + else: + _mem.lowpower = 0 + + return mem + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + top = RadioSettings(basic) + + # Menu 03 + rs = RadioSettingValueInteger(0, 9, _settings.squelch) + rset = RadioSetting("squelch", "Squelch Level", rs) + basic.append(rset) + + # Menu 11 + rs = RadioSettingValueList(TOT_LIST, TOT_LIST[_settings.tot]) + rset = RadioSetting("tot", "Time-out timer", rs) + basic.append(rset) + + # Menu 06 + rs = RadioSettingValueList(VOX_LIST, VOX_LIST[_settings.vox]) + rset = RadioSetting("vox", "VOX Level", rs) + basic.append(rset) + + # Menu 15 (BF-T8) + rs = RadioSettingValueList(VOICE_LIST, VOICE_LIST[_settings.voice]) + rset = RadioSetting("voice", "Voice", rs) + basic.append(rset) + + # Menu 12 + rs = RadioSettingValueBoolean(_settings.bcl) + rset = RadioSetting("bcl", "Busy Channel Lockout", rs) + basic.append(rset) + + # Menu 10 + rs = RadioSettingValueBoolean(_settings.save) + rset = RadioSetting("save", "Battery Saver", rs) + basic.append(rset) + + # Menu 08 + rs = RadioSettingValueBoolean(_settings.tdr) + rset = RadioSetting("tdr", "Dual Watch", rs) + basic.append(rset) + + # Menu 05 + rs = RadioSettingValueBoolean(_settings.beep) + rset = RadioSetting("beep", "Beep", rs) + basic.append(rset) + + # Menu 04 + rs = RadioSettingValueList(ABR_LIST, ABR_LIST[_settings.abr]) + rset = RadioSetting("abr", "Back Light", rs) + basic.append(rset) + + # Menu 13 + rs = RadioSettingValueList(RING_LIST, RING_LIST[_settings.ring]) + rset = RadioSetting("ring", "Ring", rs) + basic.append(rset) + + rs = RadioSettingValueBoolean(not _settings.ste) + rset = RadioSetting("ste", "Squelch Tail Eliminate", rs) + basic.append(rset) + + # + + rs = RadioSettingValueInteger(1, self._upper, _settings.mra) + rset = RadioSetting("mra", "MR A Channel #", rs) + basic.append(rset) + + rs = RadioSettingValueInteger(1, self._upper, _settings.mrb) + rset = RadioSetting("mrb", "MR B Channel #", rs) + basic.append(rset) + + rs = RadioSettingValueList(AB_LIST, AB_LIST[_settings.disp_ab]) + rset = RadioSetting("disp_ab", "Selected Display Line", rs) + basic.append(rset) + + rs = RadioSettingValueList(WX_LIST, WX_LIST[_settings.wx]) + rset = RadioSetting("wx", "NOAA WX Radio", rs) + basic.append(rset) + + def myset_freq(setting, obj, atrb, mult): + """ Callback to set frequency by applying multiplier""" + value = int(float(str(setting.value)) * mult) + setattr(obj, atrb, value) + return + + # FM Broadcast Settings + val = _settings.fmcur + val = val / 10.0 + val_low = 76.0 + if val < val_low or val > 108.0: + val = 90.4 + rx = RadioSettingValueFloat(val_low, 108.0, val, 0.1, 1) + rset = RadioSetting("settings.fmcur", "Broadcast FM Radio (MHz)", rx) + rset.set_apply_callback(myset_freq, _settings, "fmcur", 10) + basic.append(rset) + + rs = RadioSettingValueList(WORKMODE_LIST, + WORKMODE_LIST[_settings.workmode]) + rset = RadioSetting("workmode", "Work Mode", rs) + basic.append(rset) + + rs = RadioSettingValueList(AREA_LIST, AREA_LIST[_settings.area]) + rs.set_mutable(False) + rset = RadioSetting("area", "Area", rs) + basic.append(rset) + + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif setting == "ste": + setattr(obj, setting, not int(element.value)) + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + # This radio has always been post-metadata, so never do + # old-school detection + return False + + +class BFU9Alias(chirp_common.Alias): + VENDOR = "Baofeng" + MODEL = "BF-U9" + + +class AR8Alias(chirp_common.Alias): + VENDOR = "Arcshell" + MODEL = "AR-8" + + +@directory.register +class BaofengBFT8Generic(BFT8Radio): + ALIASES = [BFU9Alias, AR8Alias, ] diff -r b04ba05b7b64 -r ccf8f96034cd tools/cpep8.manifest --- a/tools/cpep8.manifest Wed Apr 28 07:50:34 2021 -0700 +++ b/tools/cpep8.manifest Wed May 19 20:58:53 2021 -0400 @@ -19,6 +19,7 @@ ./chirp/drivers/baofeng_common.py ./chirp/drivers/baofeng_uv3r.py ./chirp/drivers/baofeng_wp970i.py +./chirp/drivers/bf-t8.py ./chirp/drivers/bjuv55.py ./chirp/drivers/btech.py ./chirp/drivers/ft1500m.py