Project

General

Profile

Bug #10528 » bf_t8_10528_fix.py

Jim Unroe, 04/18/2023 06:31 PM

 
1
# Copyright 2022 Jim Unroe <rock.unroe@gmail.com>
2
#
3
# This program is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation, either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

    
16
import time
17
import os
18
import struct
19
import logging
20

    
21
from chirp import (
22
    bitwise,
23
    chirp_common,
24
    directory,
25
    errors,
26
    memmap,
27
    util,
28
)
29
from chirp.settings import (
30
    RadioSetting,
31
    RadioSettingGroup,
32
    RadioSettings,
33
    RadioSettingValueBoolean,
34
    RadioSettingValueFloat,
35
    RadioSettingValueInteger,
36
    RadioSettingValueList,
37
    RadioSettingValueString,
38
)
39

    
40
LOG = logging.getLogger(__name__)
41

    
42
MEM_FORMAT = """
43
#seekto 0x0000;
44
struct {
45
  lbcd rxfreq[4];       // RX Frequency
46
  lbcd txfreq[4];       // TX Frequency
47
  u8 rx_tmode;          // RX Tone Mode
48
  u8 rx_tone;           // PL/DPL Decode
49
  u8 tx_tmode;          // TX Tone Mode
50
  u8 tx_tone;           // PL/DPL Encode
51
  u8 unknown1:3,        //
52
     skip:1,            // Scan Add: 1 = Skip, 0 = Scan
53
     unknown2:2,
54
     isnarrow:1,        // W/N: 1 = Narrow, 0 = Wide
55
     lowpower:1;        // TX Power: 1 = Low, 0 = High
56
  u8 unknown3[3];       //
57
} memory[%d];
58

    
59
#seekto 0x0630;
60
struct {
61
  u8 squelch;           // SQL
62
  u8 vox;               // Vox Lv
63
  u8 tot;               // TOT
64
  u8 unk1:3,            //
65
     ste:1,             // Tail Clear
66
     bcl:1,             // BCL
67
     save:1,            // Save
68
     tdr:1,             // TDR
69
     beep:1;            // Beep
70
  u8 voice;             // Voice
71
  u8 abr;               // Back Light
72
  u8 ring;              // Ring
73
  u8 mdf;               // Display Type
74
  u8 mra;               // MR Channel A
75
  u8 mrb;               // MR Channel B
76
  u8 disp_ab;           // Display A/B Selected
77
  u16 fmcur;            // Broadcast FM station
78
  u8 workmode;          // Work Mode
79
  u8 wx;                // NOAA WX ch#
80
  u8 area;              // Area Selected
81
} settings;
82

    
83
#seekto 0x0D00;
84
struct {
85
  char name[6];
86
  u8 unknown1[2];
87
} names[%d];
88
"""
89

    
90
CMD_ACK = b"\x06"
91

    
92
TONES = chirp_common.TONES
93
TMODES = ["", "Tone", "DTCS", "DTCS"]
94

    
95
AB_LIST = ["A", "B"]
96
ABR_LIST = ["OFF", "ON", "Key"]
97
AREA_LIST = ["China", "Japan", "Korea", "Malaysia", "American",
98
             "Australia", "Iran", "Taiwan", "Europe", "Russia"]
99
MDF_LIST = ["Frequency", "Channel #", "Name"]
100
RING_LIST = ["OFF"] + ["%s" % x for x in range(1, 11)]
101
TOT_LIST = ["OFF"] + ["%s seconds" % x for x in range(30, 210, 30)]
102
TOT2_LIST = TOT_LIST[1:]
103
VOICE_LIST = ["Off", "Chinese", "English"]
104
VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 6)]
105
WORKMODE_LIST = ["General", "PMR"]
106
WX_LIST = ["CH01 - 162.550",
107
           "CH02 - 162.400",
108
           "CH03 - 162.475",
109
           "CH04 - 162.425",
110
           "CH05 - 162.450",
111
           "CH06 - 162.500",
112
           "CH07 - 162.525",
113
           "CH08 - 161.650",
114
           "CH09 - 161.775",
115
           "CH10 - 161.750",
116
           "CH11 - 162.000"
117
           ]
118

    
119
SETTING_LISTS = {
120
    "ab": AB_LIST,
121
    "abr": ABR_LIST,
122
    "area": AREA_LIST,
123
    "mdf": MDF_LIST,
124
    "ring": RING_LIST,
125
    "tot": TOT_LIST,
126
    "tot": TOT2_LIST,
127
    "voice": VOICE_LIST,
128
    "vox": VOX_LIST,
129
    "workmode": WORKMODE_LIST,
130
    "wx": WX_LIST,
131
    }
132

    
133
FRS_FREQS1 = [462562500, 462587500, 462612500, 462637500, 462662500,
134
              462687500, 462712500]
135
FRS_FREQS2 = [467562500, 467587500, 467612500, 467637500, 467662500,
136
              467687500, 467712500]
137
FRS_FREQS3 = [462550000, 462575000, 462600000, 462625000, 462650000,
138
              462675000, 462700000, 462725000]
139
FRS_FREQS = FRS_FREQS1 + FRS_FREQS2 + FRS_FREQS3
140

    
141
GMRS_FREQS = FRS_FREQS + FRS_FREQS3
142

    
143
MURS_FREQS = [151820000, 151880000, 151940000, 154570000, 154600000]
144

    
145
PMR_FREQS1 = [446006250, 446018750, 446031250, 446043750, 446056250,
146
              446068750, 446081250, 446093750]
147
PMR_FREQS2 = [446106250, 446118750, 446131250, 446143750, 446156250,
148
              446168750, 446181250, 446193750]
149
PMR_FREQS = PMR_FREQS1 + PMR_FREQS2
150

    
151
VOICE_CHOICES = ["Off", "On"]
152
VOICE_VALUES = [0x00, 0x02]
153

    
154

    
155
def _enter_programming_mode(radio):
156
    serial = radio.pipe
157

    
158
    exito = False
159
    for i in range(0, 5):
160
        serial.write(radio._magic)
161
        ack = serial.read(1)
162

    
163
        try:
164
            if ack == CMD_ACK:
165
                exito = True
166
                break
167
        except:
168
            LOG.debug("Attempt #%s, failed, trying again" % i)
169
            pass
170

    
171
    # check if we had EXITO
172
    if exito is False:
173
        _exit_programming_mode(radio)
174
        msg = "The radio did not accept program mode after five tries.\n"
175
        msg += "Check you interface cable and power cycle your radio."
176
        raise errors.RadioError(msg)
177

    
178
    try:
179
        serial.write(b"\x02")
180
        ident = serial.read(len(radio._fingerprint))
181
    except:
182
        _exit_programming_mode(radio)
183
        raise errors.RadioError("Error communicating with radio")
184

    
185
    if not ident == radio._fingerprint:
186
        _exit_programming_mode(radio)
187
        LOG.debug(util.hexprint(ident))
188
        raise errors.RadioError("Radio returned unknown identification string")
189

    
190
    try:
191
        serial.write(CMD_ACK)
192
        ack = serial.read(1)
193
    except:
194
        _exit_programming_mode(radio)
195
        raise errors.RadioError("Error communicating with radio")
196

    
197
    if ack != CMD_ACK:
198
        _exit_programming_mode(radio)
199
        raise errors.RadioError("Radio refused to enter programming mode")
200

    
201

    
202
def _exit_programming_mode(radio):
203
    serial = radio.pipe
204
    try:
205
        serial.write(b"E")
206
    except:
207
        raise errors.RadioError("Radio refused to exit programming mode")
208

    
209

    
210
def _read_block(radio, block_addr, block_size):
211
    serial = radio.pipe
212

    
213
    cmd = struct.pack(">cHb", b'R', block_addr, block_size)
214
    expectedresponse = b"W" + cmd[1:]
215
    LOG.debug("Reading block %04x..." % (block_addr))
216

    
217
    try:
218
        serial.write(cmd)
219
        response = serial.read(4 + block_size)
220
        if response[:4] != expectedresponse:
221
            raise Exception("Error reading block %04x." % (block_addr))
222

    
223
        block_data = response[4:]
224

    
225
        serial.write(CMD_ACK)
226
        ack = serial.read(1)
227
    except:
228
        _exit_programming_mode(radio)
229
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
230

    
231
    if ack != CMD_ACK:
232
        _exit_programming_mode(radio)
233
        raise Exception("No ACK reading block %04x." % (block_addr))
234

    
235
    return block_data
236

    
237

    
238
def _write_block(radio, block_addr, block_size):
239
    serial = radio.pipe
240

    
241
    cmd = struct.pack(">cHb", b'W', block_addr, block_size)
242
    data = radio.get_mmap()[block_addr:block_addr + block_size]
243

    
244
    LOG.debug("Writing Data:")
245
    LOG.debug(util.hexprint(cmd + data))
246

    
247
    try:
248
        serial.write(cmd + data)
249
        if serial.read(1) != CMD_ACK:
250
            raise Exception("No ACK")
251
    except:
252
        _exit_programming_mode(radio)
253
        raise errors.RadioError("Failed to send block "
254
                                "to radio at %04x" % block_addr)
255

    
256

    
257
def do_download(radio):
258
    LOG.debug("download")
259
    _enter_programming_mode(radio)
260

    
261
    data = b""
262

    
263
    status = chirp_common.Status()
264
    status.msg = "Cloning from radio"
265

    
266
    status.cur = 0
267
    status.max = radio._memsize
268

    
269
    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
270
        status.cur = addr + radio.BLOCK_SIZE
271
        radio.status_fn(status)
272

    
273
        block = _read_block(radio, addr, radio.BLOCK_SIZE)
274
        data += block
275

    
276
        LOG.debug("Address: %04x" % addr)
277
        LOG.debug(util.hexprint(block))
278

    
279
    _exit_programming_mode(radio)
280

    
281
    return memmap.MemoryMapBytes(data)
282

    
283

    
284
def do_upload(radio):
285
    status = chirp_common.Status()
286
    status.msg = "Uploading to radio"
287

    
288
    _enter_programming_mode(radio)
289

    
290
    status.cur = 0
291
    status.max = radio._memsize
292

    
293
    for start_addr, end_addr in radio._ranges:
294
        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
295
            status.cur = addr + radio.BLOCK_SIZE_UP
296
            radio.status_fn(status)
297
            _write_block(radio, addr, radio.BLOCK_SIZE_UP)
298

    
299
    _exit_programming_mode(radio)
300

    
301

    
302
class BFT8Radio(chirp_common.CloneModeRadio):
303
    """Baofeng BF-T8"""
304
    VENDOR = "Baofeng"
305
    MODEL = "BF-T8"
306
    BAUD_RATE = 9600
307
    NEEDS_COMPAT_SERIAL = False
308
    BLOCK_SIZE = BLOCK_SIZE_UP = 0x10
309
    ODD_SPLIT = True
310
    HAS_NAMES = False
311
    NAME_LENGTH = 0
312
    VALID_CHARS = ""
313
    CH_OFFSET = False
314
    SKIP_VALUES = []
315
    DTCS_CODES = sorted(chirp_common.DTCS_CODES)
316
    DUPLEXES = ["", "-", "+", "split", "off"]
317

    
318
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
319
                    chirp_common.PowerLevel("Low", watts=0.50)]
320
    VALID_BANDS = [(400000000, 470000000)]
321

    
322
    _magic = b"\x02" + b"PROGRAM"
323
    _fingerprint = b"\x2E" + b"BF-T6" + b"\x2E"
324
    _upper = 99
325
    _mem_params = (_upper,  # number of channels
326
                   _upper   # number of names
327
                   )
328
    _frs = _gmrs = _murs = _pmr = False
329

    
330
    _ranges = [
331
               (0x0000, 0x0B60),
332
              ]
333
    _memsize = 0x0B60
334

    
335
    def get_features(self):
336
        rf = chirp_common.RadioFeatures()
337
        rf.has_settings = True
338
        rf.has_bank = False
339
        rf.has_ctone = True
340
        rf.has_cross = True
341
        rf.has_rx_dtcs = True
342
        rf.has_tuning_step = False
343
        rf.has_name = self.HAS_NAMES
344
        rf.can_odd_split = self.ODD_SPLIT
345
        rf.valid_name_length = self.NAME_LENGTH
346
        rf.valid_characters = self.VALID_CHARS
347
        rf.valid_skips = self.SKIP_VALUES
348
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
349
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
350
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
351
        rf.valid_dtcs_codes = self.DTCS_CODES
352
        rf.valid_power_levels = self.POWER_LEVELS
353
        rf.valid_duplexes = self.DUPLEXES
354
        rf.valid_modes = ["FM", "NFM"]  # 25 kHz, 12.5 KHz.
355
        rf.memory_bounds = (1, self._upper)
356
        rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.]
357
        rf.valid_bands = self.VALID_BANDS
358

    
359
        return rf
360

    
361
    def process_mmap(self):
362
        self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap)
363

    
364
    def sync_in(self):
365
        """Download from radio"""
366
        try:
367
            data = do_download(self)
368
        except errors.RadioError:
369
            # Pass through any real errors we raise
370
            raise
371
        except:
372
            # If anything unexpected happens, make sure we raise
373
            # a RadioError and log the problem
374
            LOG.exception('Unexpected error during download')
375
            raise errors.RadioError('Unexpected error communicating '
376
                                    'with the radio')
377
        self._mmap = data
378
        self.process_mmap()
379

    
380
    def sync_out(self):
381
        """Upload to radio"""
382
        try:
383
            do_upload(self)
384
        except:
385
            # If anything unexpected happens, make sure we raise
386
            # a RadioError and log the problem
387
            LOG.exception('Unexpected error during upload')
388
            raise errors.RadioError('Unexpected error communicating '
389
                                    'with the radio')
390

    
391
    def get_raw_memory(self, number):
392
        return repr(self._memobj.memory[number - 1])
393

    
394
    def _get_tone(self, mem, _mem):
395
        rx_tone = tx_tone = None
396

    
397
        tx_tmode = TMODES[_mem.tx_tmode]
398
        rx_tmode = TMODES[_mem.rx_tmode]
399

    
400
        if tx_tmode == "Tone":
401
            tx_tone = TONES[_mem.tx_tone]
402
        elif tx_tmode == "DTCS":
403
            tx_tone = self.DTCS_CODES[_mem.tx_tone]
404

    
405
        if rx_tmode == "Tone":
406
            rx_tone = TONES[_mem.rx_tone]
407
        elif rx_tmode == "DTCS":
408
            rx_tone = self.DTCS_CODES[_mem.rx_tone]
409

    
410
        tx_pol = _mem.tx_tmode == 0x03 and "R" or "N"
411
        rx_pol = _mem.rx_tmode == 0x03 and "R" or "N"
412

    
413
        chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
414
                                       (rx_tmode, rx_tone, rx_pol))
415

    
416
    def _is_txinh(self, _mem):
417
        raw_tx = ""
418
        for i in range(0, 4):
419
            raw_tx += _mem.txfreq[i].get_raw()
420
        return raw_tx == "\xFF\xFF\xFF\xFF"
421

    
422
    def _get_mem(self, number):
423
        return self._memobj.memory[number - 1]
424

    
425
    def _get_nam(self, number):
426
        return self._memobj.names[number - 1]
427

    
428
    def get_memory(self, number):
429
        _mem = self._get_mem(number)
430
        if self.HAS_NAMES:
431
            _nam = self._get_nam(number)
432

    
433
        mem = chirp_common.Memory()
434

    
435
        mem.number = number
436
        mem.freq = int(_mem.rxfreq) * 10
437

    
438
        # We'll consider any blank (i.e. 0MHz frequency) to be empty
439
        if mem.freq == 0:
440
            mem.empty = True
441
            return mem
442

    
443
        if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF":
444
            mem.freq = 0
445
            mem.empty = True
446
            return mem
447

    
448
        if _mem.get_raw() == ("\xFF" * 16):
449
            LOG.debug("Initializing empty memory")
450
            _mem.set_raw("\x00" * 13 + "\xFF" * 3)
451

    
452
        if self._is_txinh(_mem):
453
            mem.duplex = "off"
454
            mem.offset = 0
455
        elif int(_mem.rxfreq) == int(_mem.txfreq):
456
            mem.duplex = ""
457
            mem.offset = 0
458
        else:
459
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
460
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
461

    
462
        # wide/narrow
463
        mem.mode = _mem.isnarrow and "NFM" or "FM"
464

    
465
        mem.skip = _mem.skip and "S" or ""
466

    
467
        if self.HAS_NAMES:
468
            for char in _nam.name:
469
                if str(char) == "\xFF":
470
                    char = " "  # The OEM software may have 0xFF mid-name
471
                mem.name += str(char)
472
            mem.name = mem.name.rstrip()
473

    
474
        # tone data
475
        self._get_tone(mem, _mem)
476

    
477
        # tx power
478
        levels = self.POWER_LEVELS
479
        try:
480
            mem.power = levels[_mem.lowpower]
481
        except IndexError:
482
            LOG.error("Radio reported invalid power level %s (in %s)" %
483
                      (_mem.lowpower, levels))
484
            mem.power = levels[0]
485

    
486
        immutable = []
487

    
488
        if self._frs:
489
            if mem.freq in FRS_FREQS:
490
                if mem.number >= 1 and mem.number <= 22:
491
                    FRS_FREQ = FRS_FREQS[mem.number - 1]
492
                    mem.freq = FRS_FREQ
493
                mem.duplex == ''
494
                mem.offset = 0
495
                mem.mode = "NFM"
496
                if mem.number >= 8 and mem.number <= 14:
497
                    mem.power = self.POWER_LEVELS[1]
498
                    immutable = ["empty", "freq", "duplex", "offset", "mode",
499
                                 "power"]
500
                else:
501
                    immutable = ["empty", "freq", "duplex", "offset", "mode"]
502
        elif self._murs:
503
            if mem.freq in MURS_FREQS:
504
                if mem.number >= 1 and mem.number <= 5:
505
                    MURS_FREQ = MURS_FREQS[mem.number - 1]
506
                    mem.freq = MURS_FREQ
507
                mem.duplex = ''
508
                mem.offset = 0
509
                if mem.number <= 3:
510
                    mem.mode = "NFM"
511
                    immutable = ["empty", "freq", "duplex", "offset", "mode"]
512
                else:
513
                    immutable = ["empty", "freq", "duplex", "offset"]
514
        elif self._pmr:
515
            if mem.freq in PMR_FREQS:
516
                if mem.number >= 1 and mem.number <= 16:
517
                    PMR_FREQ = PMR_FREQS[mem.number - 1]
518
                    mem.freq = PMR_FREQ
519
                mem.duplex = ''
520
                mem.offset = 0
521
                mem.mode = "NFM"
522
                mem.power = self.POWER_LEVELS[1]
523
                immutable = ["empty", "freq", "duplex", "offset", "mode",
524
                             "power"]
525
        elif self._gmrs:
526
            if mem.freq in GMRS_FREQS:
527
                if mem.number >= 1 and mem.number <= 30:
528
                    GMRS_FREQ = GMRS_FREQS[mem.number - 1]
529
                    mem.freq = GMRS_FREQ
530
                    immutable = ["empty", "freq"]
531
                if mem.number >= 1 and mem.number <= 7:
532
                    mem.duplex = ''
533
                    mem.offset = 0
534
                    immutable += ["duplex", "offset"]
535
                elif mem.number >= 8 and mem.number <= 14:
536
                    mem.duplex = ''
537
                    mem.offset = 0
538
                    mem.mode = "NFM"
539
                    mem.power = self.POWER_LEVELS[1]
540
                    immutable += ["duplex", "offset", "mode", "power"]
541
                elif mem.number >= 15 and mem.number <= 22:
542
                    mem.duplex = ''
543
                    mem.offset = 0
544
                    immutable += ["duplex", "offset"]
545
                elif mem.number >= 23 and mem.number <= 30:
546
                    mem.duplex = '+'
547
                    mem.offset = 5000000
548
                    immutable += ["duplex", "offset"]
549
                elif mem.freq in FRS_FREQS1:
550
                    mem.duplex = ''
551
                    mem.offset = 0
552
                    immutable += ["duplex", "offset"]
553
                elif mem.freq in FRS_FREQS2:
554
                    mem.duplex = ''
555
                    mem.offset = 0
556
                    mem.mode = "NFM"
557
                    mem.power = self.POWER_LEVELS[1]
558
                    immutable += ["duplex", "offset", "mode", "power"]
559
                elif mem.freq in FRS_FREQS3:
560
                    if mem.duplex == '':
561
                        mem.offset = 0
562
                    if mem.duplex == '+':
563
                        mem.offset = 5000000
564
            else:
565
                if mem.freq not in GMRS_FREQS:
566
                    mem.duplex = 'off'
567
                    mem.offset = 0
568
                    immutable = ["duplex", "offset"]
569

    
570
        mem.immutable = immutable
571

    
572
        return mem
573

    
574
    def _set_tone(self, mem, _mem):
575
        ((txmode, txtone, txpol),
576
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
577

    
578
        _mem.tx_tmode = TMODES.index(txmode)
579
        _mem.rx_tmode = TMODES.index(rxmode)
580
        if txmode == "Tone":
581
            _mem.tx_tone = TONES.index(txtone)
582
        elif txmode == "DTCS":
583
            _mem.tx_tmode = txpol == "R" and 0x03 or 0x02
584
            _mem.tx_tone = self.DTCS_CODES.index(txtone)
585
        if rxmode == "Tone":
586
            _mem.rx_tone = TONES.index(rxtone)
587
        elif rxmode == "DTCS":
588
            _mem.rx_tmode = rxpol == "R" and 0x03 or 0x02
589
            _mem.rx_tone = self.DTCS_CODES.index(rxtone)
590

    
591
    def _set_mem(self, number):
592
        return self._memobj.memory[number - 1]
593

    
594
    def _set_nam(self, number):
595
        return self._memobj.names[number - 1]
596

    
597
    def set_memory(self, mem):
598
        _mem = self._set_mem(mem.number)
599
        if self.HAS_NAMES:
600
            _nam = self._set_nam(mem.number)
601

    
602
        # if empty memory
603
        if mem.empty:
604
            _mem.set_raw("\xFF" * (_mem.size() // 8))
605

    
606
            if self.HAS_NAMES:
607
                for i in range(0, self.NAME_LENGTH):
608
                    _nam.name[i].set_raw("\xFF")
609

    
610
            return mem
611

    
612
        _mem.set_raw("\x00" * 13 + "\xFF" * 3)
613

    
614
        # frequency
615
        _mem.rxfreq = mem.freq / 10
616

    
617
        if mem.duplex == "off":
618
            for i in range(0, 4):
619
                _mem.txfreq[i].set_raw("\xFF")
620
        elif mem.duplex == "split":
621
            _mem.txfreq = mem.offset / 10
622
        elif mem.duplex == "+":
623
            _mem.txfreq = (mem.freq + mem.offset) / 10
624
        elif mem.duplex == "-":
625
            _mem.txfreq = (mem.freq - mem.offset) / 10
626
        else:
627
            _mem.txfreq = mem.freq / 10
628

    
629
        # wide/narrow
630
        _mem.isnarrow = mem.mode == "NFM"
631

    
632
        _mem.skip = mem.skip == "S"
633

    
634
        if self.HAS_NAMES:
635
            for i in range(self.NAME_LENGTH):
636
                try:
637
                    _nam.name[i] = mem.name[i]
638
                except IndexError:
639
                    _nam.name[i] = "\xFF"
640

    
641
        # tone data
642
        self._set_tone(mem, _mem)
643

    
644
        # tx power
645
        if str(mem.power) == "High":
646
            _mem.lowpower = 0
647
        elif str(mem.power) == "Low":
648
            _mem.lowpower = 1
649
        else:
650
            _mem.lowpower = 0
651

    
652
        return mem
653

    
654
    def get_settings(self):
655
        _settings = self._memobj.settings
656
        basic = RadioSettingGroup("basic", "Basic Settings")
657
        top = RadioSettings(basic)
658

    
659
        # Menu 03
660
        rs = RadioSettingValueInteger(0, 9, _settings.squelch)
661
        rset = RadioSetting("squelch", "Squelch Level", rs)
662
        basic.append(rset)
663

    
664
        model_list = ["RB27B", "RB27V", "RB627B"]
665
        if self.MODEL in model_list:
666
            # Menu 09 (RB27x/RB627x)
667
            rs = RadioSettingValueList(TOT2_LIST, TOT2_LIST[_settings.tot])
668
        else:
669
            # Menu 11 / 09 (RB27)
670
            rs = RadioSettingValueList(TOT_LIST, TOT_LIST[_settings.tot])
671
        rset = RadioSetting("tot", "Time-out timer", rs)
672
        basic.append(rset)
673

    
674
        # Menu 06
675
        rs = RadioSettingValueList(VOX_LIST, VOX_LIST[_settings.vox])
676
        rset = RadioSetting("vox", "VOX Level", rs)
677
        basic.append(rset)
678

    
679
        # Menu 15 (BF-T8) / Menu 14 (FRS-A1)
680
        if self.MODEL == "FRS-A1":
681
            # Menu 14 (FRS-A1)
682
            def apply_voice_listvalue(setting, obj):
683
                LOG.debug("Setting value: " + str(setting.value) +
684
                          " from list")
685
                val = str(setting.value)
686
                index = VOICE_CHOICES.index(val)
687
                val = VOICE_VALUES[index]
688
                obj.set_value(val)
689

    
690
            if _settings.voice in VOICE_VALUES:
691
                idx = VOICE_VALUES.index(_settings.voice)
692
            else:
693
                idx = VOICE_VALUES.index(0x00)
694
            rs = RadioSettingValueList(VOICE_CHOICES, VOICE_CHOICES[idx])
695
            rset = RadioSetting("voice", "Voice", rs)
696
            rset.set_apply_callback(apply_voice_listvalue, _settings.voice)
697
            basic.append(rset)
698
        else:
699
            # Menu 15 (BF-T8)
700
            rs = RadioSettingValueList(VOICE_LIST, VOICE_LIST[_settings.voice])
701
            rset = RadioSetting("voice", "Voice", rs)
702
            basic.append(rset)
703

    
704
        # Menu 12
705
        rs = RadioSettingValueBoolean(_settings.bcl)
706
        rset = RadioSetting("bcl", "Busy Channel Lockout", rs)
707
        basic.append(rset)
708

    
709
        # Menu 10 / 08 (RB27/RB627)
710
        rs = RadioSettingValueBoolean(_settings.save)
711
        rset = RadioSetting("save", "Battery Saver", rs)
712
        basic.append(rset)
713

    
714
        # Menu 08 / 07 (RB-27/RB627)
715
        rs = RadioSettingValueBoolean(_settings.tdr)
716
        rset = RadioSetting("tdr", "Dual Watch", rs)
717
        basic.append(rset)
718

    
719
        # Menu 05
720
        rs = RadioSettingValueBoolean(_settings.beep)
721
        rset = RadioSetting("beep", "Beep", rs)
722
        basic.append(rset)
723

    
724
        # Menu 04
725
        rs = RadioSettingValueList(ABR_LIST, ABR_LIST[_settings.abr])
726
        rset = RadioSetting("abr", "Back Light", rs)
727
        basic.append(rset)
728

    
729
        # Menu 13 / 11 (RB-27/RB627)
730
        rs = RadioSettingValueList(RING_LIST, RING_LIST[_settings.ring])
731
        rset = RadioSetting("ring", "Ring", rs)
732
        basic.append(rset)
733

    
734
        rs = RadioSettingValueBoolean(not _settings.ste)
735
        rset = RadioSetting("ste", "Squelch Tail Eliminate", rs)
736
        basic.append(rset)
737

    
738
        # Menu 15 (FRS-A1)
739
        if self.MODEL == "FRS-A1":
740
            rs = RadioSettingValueList(MDF_LIST, MDF_LIST[_settings.mdf])
741
            rset = RadioSetting("mdf", "Display Type", rs)
742
            basic.append(rset)
743

    
744
        if self.CH_OFFSET:
745
            rs = RadioSettingValueInteger(1, self._upper, _settings.mra + 1)
746
        else:
747
            rs = RadioSettingValueInteger(1, self._upper, _settings.mra)
748
        rset = RadioSetting("mra", "MR A Channel #", rs)
749
        basic.append(rset)
750

    
751
        if self.CH_OFFSET:
752
            rs = RadioSettingValueInteger(1, self._upper, _settings.mrb + 1)
753
        else:
754
            rs = RadioSettingValueInteger(1, self._upper, _settings.mrb)
755
        rset = RadioSetting("mrb", "MR B Channel #", rs)
756
        basic.append(rset)
757

    
758
        rs = RadioSettingValueList(AB_LIST, AB_LIST[_settings.disp_ab])
759
        rset = RadioSetting("disp_ab", "Selected Display Line", rs)
760
        basic.append(rset)
761

    
762
        if not self.MODEL.startswith("RB627"):
763
            if self.MODEL == "FRS-A1":
764
                del WX_LIST[7:]
765
            rs = RadioSettingValueList(WX_LIST, WX_LIST[_settings.wx])
766
            rset = RadioSetting("wx", "NOAA WX Radio", rs)
767
            basic.append(rset)
768

    
769
        def myset_freq(setting, obj, atrb, mult):
770
            """ Callback to set frequency by applying multiplier"""
771
            value = int(float(str(setting.value)) * mult)
772
            setattr(obj, atrb, value)
773
            return
774

    
775
        # FM Broadcast Settings
776
        val = _settings.fmcur
777
        val = val / 10.0
778
        val_low = 76.0
779
        if self.MODEL == "FRS-A1":
780
            val_low = 87.0
781
        if val < val_low or val > 108.0:
782
            val = 90.4
783
        rx = RadioSettingValueFloat(val_low, 108.0, val, 0.1, 1)
784
        rset = RadioSetting("settings.fmcur", "Broadcast FM Radio (MHz)", rx)
785
        rset.set_apply_callback(myset_freq, _settings, "fmcur", 10)
786
        basic.append(rset)
787

    
788
        model_list = ["BF-T8", "BF-U9", "AR-8"]
789
        if self.MODEL in model_list:
790
            rs = RadioSettingValueList(WORKMODE_LIST,
791
                                       WORKMODE_LIST[_settings.workmode])
792
            rset = RadioSetting("workmode", "Work Mode", rs)
793
            basic.append(rset)
794

    
795
            rs = RadioSettingValueList(AREA_LIST, AREA_LIST[_settings.area])
796
            rs.set_mutable(False)
797
            rset = RadioSetting("area", "Area", rs)
798
            basic.append(rset)
799

    
800
        return top
801

    
802
    def set_settings(self, settings):
803
        for element in settings:
804
            if not isinstance(element, RadioSetting):
805
                self.set_settings(element)
806
                continue
807
            else:
808
                try:
809
                    if "." in element.get_name():
810
                        bits = element.get_name().split(".")
811
                        obj = self._memobj
812
                        for bit in bits[:-1]:
813
                            obj = getattr(obj, bit)
814
                        setting = bits[-1]
815
                    else:
816
                        obj = self._memobj.settings
817
                        setting = element.get_name()
818

    
819
                    if element.has_apply_callback():
820
                        LOG.debug("Using apply callback")
821
                        element.run_apply_callback()
822
                    elif setting == "mra" and self.CH_OFFSET:
823
                        setattr(obj, setting, int(element.value) - 1)
824
                    elif setting == "mrb" and self.CH_OFFSET:
825
                        setattr(obj, setting, int(element.value) - 1)
826
                    elif setting == "ste":
827
                        setattr(obj, setting, not int(element.value))
828
                    elif element.value.get_mutable():
829
                        LOG.debug("Setting %s = %s" % (setting, element.value))
830
                        setattr(obj, setting, element.value)
831
                except Exception as e:
832
                    LOG.debug(element.get_name())
833
                    raise
834

    
835
    @classmethod
836
    def match_model(cls, filedata, filename):
837
        # This radio has always been post-metadata, so never do
838
        # old-school detection
839
        return False
840

    
841

    
842
class BFU9Alias(chirp_common.Alias):
843
    VENDOR = "Baofeng"
844
    MODEL = "BF-U9"
845

    
846

    
847
class AR8Alias(chirp_common.Alias):
848
    VENDOR = "Arcshell"
849
    MODEL = "AR-8"
850

    
851

    
852
@directory.register
853
class BaofengBFT8Generic(BFT8Radio):
854
    ALIASES = [BFU9Alias, AR8Alias, ]
855

    
856

    
857
@directory.register
858
class RetevisRT16(BFT8Radio):
859
    VENDOR = "Retevis"
860
    MODEL = "RT16"
861

    
862
    _upper = 22
863
    _frs = True
864

    
865

    
866
@directory.register
867
class RetevisRB27B(BFT8Radio):
868
    VENDOR = "Retevis"
869
    MODEL = "RB27B"
870
    DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
871
    HAS_NAMES = True
872
    NAME_LENGTH = 6
873
    VALID_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-"
874
    CH_OFFSET = True
875
    SKIP_VALUES = ["", "S"]
876
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
877
                    chirp_common.PowerLevel("Low", watts=0.50)]
878
    VALID_BANDS = [(400000000, 520000000)]
879

    
880
    _upper = 22
881
    _frs = True
882
    _gmrs = _murs = _pmr = False
883

    
884
    _ranges = [
885
               (0x0000, 0x0640),
886
               (0x0D00, 0x1040),
887
              ]
888
    _memsize = 0x1040
889

    
890

    
891
@directory.register
892
class RetevisRB27(RetevisRB27B):
893
    VENDOR = "Retevis"
894
    MODEL = "RB27"
895
    DUPLEXES = ["", "+", "off"]
896
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
897
                    chirp_common.PowerLevel("Low", watts=0.50)]
898
    VALID_BANDS = [(136000000, 174000000),
899
                   (400000000, 520000000)]
900
    ODD_SPLIT = False
901

    
902
    _upper = 99
903
    _gmrs = True
904
    _frs = _murs = _pmr = False
905

    
906
    def validate_memory(self, mem):
907
        msgs = super().validate_memory(mem)
908

    
909
        _msg_duplex = 'Duplex must be "off" for this frequency'
910
        _msg_offset = 'Only simplex or +5MHz offset allowed on GMRS'
911

    
912
        if mem.freq not in GMRS_FREQS:
913
            if mem.duplex != "off":
914
                msgs.append(chirp_common.ValidationWarning(_msg_duplex))
915
        if mem.freq in FRS_FREQS3:
916
            if mem.duplex and mem.offset != 5000000:
917
                msgs.append(chirp_common.ValidationWarning(_msg_offset))
918
            if mem.duplex and mem.duplex != "+":
919
                msgs.append(chirp_common.ValidationWarning(_msg_offset))
920

    
921
        return msgs
922

    
923
    def check_set_memory_immutable_policy(self, existing, new):
924
        existing.immutable = []
925
        super().check_set_memory_immutable_policy(existing, new)
926

    
927

    
928
@directory.register
929
class RetevisRB27V(RetevisRB27B):
930
    VENDOR = "Retevis"
931
    MODEL = "RB27V"
932
    VALID_BANDS = [(136000000, 174000000)]
933

    
934
    _upper = 5
935
    _murs = True
936
    _frs = _gmrs = _pmr = False
937

    
938

    
939
@directory.register
940
class RetevisRB627B(RetevisRB27B):
941
    VENDOR = "Retevis"
942
    MODEL = "RB627B"
943
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=0.50),
944
                    chirp_common.PowerLevel("Low", watts=0.50)]
945

    
946
    _upper = 16
947
    _pmr = True
948
    _frs = _gmrs = _murs = False
949

    
950

    
951
@directory.register
952
class FRSA1Radio(BFT8Radio):
953
    """BTECH FRS-A1"""
954
    VENDOR = "BTECH"
955
    MODEL = "FRS-A1"
956
    ODD_SPLIT = False
957
    HAS_NAMES = True
958
    NAME_LENGTH = 6
959
    SKIP_VALUES = ["", "S"]
960
    DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
961
    DUPLEXES = ['', '-', '+', 'off']
962

    
963
    _fingerprint = b"BF-T8A" + b"\x2E"
964
    _upper = 22
965
    _frs = _upper == 22
966

    
967
    _ranges = [
968
               (0x0000, 0x0640),
969
               (0x0D00, 0x0DC0),
970
              ]
971
    _memsize = 0x0DC0
972

    
973
    def get_features(self):
974
        rf = BFT8Radio.get_features(self)
975
        rf.valid_name_length = 6
976
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-"
977
        return rf
(3-3/3)