anytone778uv_function.py

1st draft for Function Setup tab - Jim Unroe, 11/23/2020 06:10 pm

Download (47.9 kB)

 
1
# Copyright 2020 Joe Milbourn <joe@milbourn.org.uk>
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
# TODO use the band field from ver_response
17
# TODO handle radio settings
18
#
19
# Supported features
20
# * Read and write memory access for 200 normal memories
21
# * CTCSS and DTCS for transmit and receive
22
# * Scan list
23
# * Tx off
24
# * Duplex (+ve, -ve, odd, and off splits)
25
# * Transmit power
26
# * Channel width (25kHz and 12.5kHz)
27
# * Retevis RT95, CRT Micron UV, and Midland DBR2500 radios
28
# * Full range of frequencies for tx and rx, supported band read from radio
29
#   during download, not verified on upload.  Radio will refuse to TX if out of
30
#   band.
31
#
32
# Unsupported features
33
# * VFO1, VFO2, and TRF memories
34
# * custom CTCSS tones
35
# * Any non-memory radio settings
36
# * Reverse, talkaround, scramble
37
# * busy channel lock out
38
# * probably other things too - like things encoded by the unknown bits in the
39
#   memory struct
40

    
41
from chirp import chirp_common, directory, memmap, errors, util
42
from chirp import bitwise
43
from chirp.settings import RadioSettingGroup, RadioSetting, \
44
    RadioSettingValueBoolean, RadioSettingValueList, \
45
    RadioSettingValueString, RadioSettingValueInteger, \
46
    RadioSettingValueFloat, RadioSettings, InvalidValueError
47

    
48
import struct
49
import time
50
import logging
51
#import re
52
#import math
53

    
54
LOG = logging.getLogger(__name__)
55

    
56
# Gross hack to handle missing future module on un-updatable
57
# platforms like MacOS. Just avoid registering these radio
58
# classes for now.
59
try:
60
    from builtins import bytes
61
    has_future = True
62
except ImportError:
63
    has_future = False
64
    LOG.warning('python-future package is not '
65
                'available; %s requires it' % __name__)
66

    
67

    
68
# Here is where we define the memory map for the radio. Since
69
# We often just know small bits of it, we can use #seekto to skip
70
# around as needed.
71

    
72
MEM_FORMAT = '''
73
#seekto 0x0000;
74
struct {
75
  bbcd freq[4];
76
  bbcd offset[4];
77
  u8 unknown1;
78
  u8 talkaround:1,
79
     scramble:1,
80
     unknown:2,
81
     txpower:2,
82
     duplex:2;
83
  u8 unknown_bits1:4,
84
     channel_width:2,
85
     reverse:1,
86
     tx_off:1;
87
  u8 unknown_bits2:4,
88
     dtcs_decode_en:1,
89
     ctcss_decode_en:1,
90
     dtcs_encode_en:1,
91
     ctcss_encode_en:1;
92
  u8 ctcss_dec_tone;
93
  u8 ctcss_enc_tone;
94
  u8 dtcs_decode_code;
95
  u8 unknown_bits6:6,
96
     dtcs_decode_invert:1,
97
     dtcs_decode_code_highbit:1;
98
  u8 dtcs_encode_code;
99
  u8 unknown_bits7:6,
100
     dtcs_encode_invert:1,
101
     dtcs_encode_code_highbit:1;
102
  u8 unknown_bits4:6,
103
     busy_channel_lockout:2;
104
  u8 unknown6;
105
  u8 unknown_bits5:7,
106
     tone_squelch_en:1;
107
  u8 unknown7;
108
  u8 unknown8;
109
  u8 unknown9;
110
  u8 unknown10;
111
  char name[5];
112
  ul16 customctcss;
113
} memory[200];
114
#seekto 0x1940;
115
struct {
116
  u8 occupied_bitfield[32];
117
  u8 scan_enabled_bitfield[32];
118
} memory_status;
119

    
120
#seekto 0x1980;
121
struct {
122
  char line[7];           // starting display
123
} starting_display;
124

    
125
#seekto 0x3200;
126
struct {
127
  u8 beepVolume;          // 0x3200 beep volume
128
  u8 frequencyStep;       // 0x3201 frequency step
129
  u8 displayMode;         // 0x3202 display mode
130
  u8 unk0x3203;
131
  u8 squelchLevelA;       // 0x3204 squelch level a
132
  u8 squelchLevelB;       // 0x3205 squelch level b
133
  u8 speakerVolume;       // 0x3206 speaker volume
134
  u8 powerOnPasswd;       // 0x3207 power-on password
135
  u8 scanType;            // 0x3208 scan type
136
  u8 scanRecoveryT;       // 0x3209 scan recovery time
137
  u8 autoPowerOn;         // 0x320A auto power on
138
  u8 main;                // 0x320B main
139
  u8 dualWatch;           // 0x320C dual watch (rx way select)
140
  u8 backlightBr;         // 0x320D backlight brightness
141
  u8 timeOutTimer;        // 0x320E time out timer
142
  u8 autoPowerOff;        // 0x320F auto power off
143
  u8 tbstFrequency;       // 0x3210 tbst frequency
144
  u8 screenDir;           // 0x3211 screen direction
145
  u8 micKeyBright;        // 0x3212 hand mic key brightness
146
  u8 speakerSwitch;       // 0x3213 speaker switch
147
  u8 unk0x3214;
148
  u8 unk0x3215;
149
  u8 unk0x3216;
150
  u8 unk0x3217;
151
  u8 steType;             // 0x3218 ste type
152
  u8 steFrequency;        // 0x3219 ste frequency
153
  u8 unk0x321A;
154
  u8 unk_bit7_5:2,        // 0x321B
155
     monKeyFunction:1,    //        mon key function
156
     channelLocked:1,     //        channel locked
157
     saveChParameter:1,   //        save channel parameter
158
     powerOnReset:1,      //        power on reset
159
     trfEnable:1,         //        trf enable
160
     knobMode:1;          //        knob mode
161
} settings;
162

    
163
#seekto 0x3240;
164
struct {
165
  char line[6];           // password
166
} password;
167

    
168
#seekto 0x3260;
169
struct {
170
  u8 mrChannelA;          // 0x3260 mr channel a
171
  u8 vfomrModeA;          // 0x3260 vfo/mr mode a
172
  u8 unknown2;
173
  u8 unknown3;
174
  u8 unknown4;
175
  u8 unknown5;
176
  u8 unknown6;
177
  u8 mrChannelB;          // 0x3267 mr channel b
178
  u8 vfomrModeB;          // 0x3268 vfo/mr mode b
179
  u8 unknown9;
180
  u8 unknowna;
181
  u8 unknownb;
182
  u8 unknownc;
183
  u8 bandlimit;       // d
184
  u8 unknownd;
185
  u8 unknowne;
186
  u8 unknownf;
187
} radio_settings;
188
'''
189

    
190
# Format for the version messages returned by the radio
191
VER_FORMAT = '''
192
u8 hdr;
193
char model[7];
194
u8 bandlimit;
195
char version[6];
196
u8 ack;
197
'''
198

    
199
TXPOWER_LOW = 0x00
200
TXPOWER_MED = 0x01
201
TXPOWER_HIGH = 0x02
202

    
203
DUPLEX_NOSPLIT = 0x00
204
DUPLEX_POSSPLIT = 0x01
205
DUPLEX_NEGSPLIT = 0x02
206
DUPLEX_ODDSPLIT = 0x03
207

    
208
CHANNEL_WIDTH_25kHz = 0x02
209
CHANNEL_WIDTH_20kHz = 0x01
210
CHANNEL_WIDTH_12d5kHz = 0x00
211

    
212
BUSY_CHANNEL_LOCKOUT_OFF = 0x00
213
BUSY_CHANNEL_LOCKOUT_REPEATER = 0x01
214
BUSY_CHANNEL_LOCKOUT_BUSY = 0x02
215

    
216
MEMORY_ADDRESS_RANGE = (0x0000, 0x3290)
217
MEMORY_RW_BLOCK_SIZE = 0x10
218
MEMORY_RW_BLOCK_CMD_SIZE = 0x16
219

    
220
POWER_LEVELS = [chirp_common.PowerLevel('Low', dBm=37),
221
                chirp_common.PowerLevel('Medium', dBm=40),
222
                chirp_common.PowerLevel('High', dBm=44)]
223

    
224
# CTCSS Tone definitions
225
TONE_CUSTOM_CTCSS = 0x33
226
TONE_MAP_VAL_TO_TONE = {0x00: 62.5, 0x01: 67.0, 0x02: 69.3,
227
                        0x03: 71.9, 0x04: 74.4, 0x05: 77.0,
228
                        0x06: 79.7, 0x07: 82.5, 0x08: 85.4,
229
                        0x09: 88.5, 0x0a: 91.5, 0x0b: 94.8,
230
                        0x0c: 97.4, 0x0d: 100.0, 0x0e: 103.5,
231
                        0x0f: 107.2, 0x10: 110.9, 0x11: 114.8,
232
                        0x12: 118.8, 0x13: 123.0, 0x14: 127.3,
233
                        0x15: 131.8, 0x16: 136.5, 0x17: 141.3,
234
                        0x18: 146.2, 0x19: 151.4, 0x1a: 156.7,
235
                        0x1b: 159.8, 0x1c: 162.2, 0x1d: 165.5,
236
                        0x1e: 167.9, 0x1f: 171.3, 0x20: 173.8,
237
                        0x21: 177.3, 0x22: 179.9, 0x23: 183.5,
238
                        0x24: 186.2, 0x25: 189.9, 0x26: 192.8,
239
                        0x27: 196.6, 0x28: 199.5, 0x29: 203.5,
240
                        0x2a: 206.5, 0x2b: 210.7, 0x2c: 218.1,
241
                        0x2d: 225.7, 0x2e: 229.1, 0x2f: 233.6,
242
                        0x30: 241.8, 0x31: 250.3, 0x32: 254.1}
243

    
244
TONE_MAP_TONE_TO_VAL = {TONE_MAP_VAL_TO_TONE[val]: val
245
                        for val in TONE_MAP_VAL_TO_TONE}
246

    
247
TONES_EN_TXTONE = (1 << 3)
248
TONES_EN_RXTONE = (1 << 2)
249
TONES_EN_TXCODE = (1 << 1)
250
TONES_EN_RXCODE = (1 << 0)
251
TONES_EN_NO_TONE = 0
252

    
253
# Radio supports upper case and symbols
254
CHARSET_ASCII_PLUS = chirp_common.CHARSET_UPPER_NUMERIC + '- '
255

    
256
# Band limits as defined by the band byte in ver_response, defined in Hz, for
257
# VHF and UHF, used for RX and TX.
258
BAND_LIMITS = {0x00: [(144000000, 148000000), (430000000, 440000000)],
259
               0x01: [(136000000, 174000000), (400000000, 490000000)],
260
               0x02: [(144000000, 146000000), (430000000, 440000000)]}
261

    
262

    
263
# Get band limits from a band limit value
264
def get_band_limits_Hz(limit_value):
265
    if limit_value not in BAND_LIMITS:
266
        limit_value = 0x01
267
        LOG.warning('Unknown band limit value 0x%02x, default to 0x01')
268
    bandlimitfrequencies = BAND_LIMITS[limit_value]
269
    return bandlimitfrequencies
270

    
271

    
272
# Calculate the checksum used in serial packets
273
def checksum(message_bytes):
274
    mask = 0xFF
275
    checksum = 0
276
    for b in message_bytes:
277
        checksum = (checksum + b) & mask
278
    return checksum
279

    
280

    
281
# Send a command to the radio, return any reply stripping the echo of the
282
# command (tx and rx share a single pin in this radio)
283
def send_serial_command(serial, command, expectedlen=None):
284
    ''' send a command to the radio, and return any response.
285
    set expectedlen to return as soon as that many bytes are read.
286
    '''
287
    serial.write(command)
288
    serial.flush()
289

    
290
    response = b''
291
    tout = time.time() + 0.5
292
    while time.time() < tout:
293
        if serial.inWaiting():
294
            response += serial.read()
295
        # remember everything gets echo'd back
296
        if len(response) - len(command) == expectedlen:
297
            break
298

    
299
    # cut off what got echo'd back, we don't need to see it again
300
    if response.startswith(command):
301
        response = response[len(command):]
302

    
303
    return response
304

    
305

    
306
# strip trailing 0x00 to convert a string returned by bitwise.parse into a
307
# python string
308
def cstring_to_py_string(cstring):
309
    return "".join(c for c in cstring if c != '\x00')
310

    
311

    
312
# Check the radio version reported to see if it's one we support,
313
# returns bool version supported, and the band index
314
def check_ver(ver_response, allowed_types):
315
    ''' Check the returned radio version is one we approve of '''
316

    
317
    LOG.debug('ver_response = ')
318
    LOG.debug(util.hexprint(ver_response))
319

    
320
    resp = bitwise.parse(VER_FORMAT, ver_response)
321
    verok = False
322

    
323
    if resp.hdr == 0x49 and resp.ack == 0x06:
324
        model, version = [cstring_to_py_string(bitwise.get_string(s)).strip()
325
                          for s in (resp.model, resp.version)]
326
        LOG.debug('radio model: \'%s\' version: \'%s\'' %
327
                  (model, version))
328
        LOG.debug('allowed_types = %s' % allowed_types)
329

    
330
        if model in allowed_types:
331
            LOG.debug('model in allowed_types')
332

    
333
            if version in allowed_types[model]:
334
                LOG.debug('version in allowed_types[model]')
335
                verok = True
336
    else:
337
        raise errors.RadioError('Failed to parse version response')
338

    
339
    return verok, int(resp.bandlimit)
340

    
341

    
342
# Put the radio in programming mode, sending the initial command and checking
343
# the response.  raise RadioError if there is no response (500ms timeout), and
344
# if the returned version isn't matched by check_ver
345
def enter_program_mode(radio):
346
    serial = radio.pipe
347
    # place the radio in program mode, and confirm
348
    program_response = send_serial_command(serial, b'PROGRAM')
349

    
350
    if program_response != b'QX\x06':
351
        raise errors.RadioError('No initial response from radio.')
352
    LOG.debug('entered program mode')
353

    
354
    # read the radio ID string, make sure it matches one we know about
355
    ver_response = send_serial_command(serial, b'\x02')
356

    
357
    verok, bandlimit = check_ver(ver_response, radio.ALLOWED_RADIO_TYPES)
358
    if not verok:
359
        exit_program_mode(radio)
360
        raise errors.RadioError(
361
            'Radio version not in allowed list for %s-%s: %s' %
362
            (radio.VENDOR, radio.MODEL, util.hexprint(ver_response)))
363

    
364
    return bandlimit
365

    
366

    
367
# Exit programming mode
368
def exit_program_mode(radio):
369
    send_serial_command(radio.pipe, b'END')
370

    
371

    
372
# Parse a packet from the radio returning the header (R/W, address, data, and
373
# checksum valid
374
def parse_read_response(resp):
375
    addr = resp[:4]
376
    data = bytes(resp[4:-2])
377
    cs = checksum(ord(d) for d in resp[1:-2])
378
    valid = cs == ord(resp[-2])
379
    if not valid:
380
        LOG.error('checksumfail: %02x, expected %02x' % (cs, ord(resp[-2])))
381
        LOG.error('msg data: %s' % util.hexprint(resp))
382
    return addr, data, valid
383

    
384

    
385
# Download data from the radio and populate the memory map
386
def do_download(radio):
387
    '''Download memories from the radio'''
388

    
389
    # Get the serial port connection
390
    serial = radio.pipe
391

    
392
    try:
393
        enter_program_mode(radio)
394

    
395
        memory_data = bytes()
396

    
397
        # status info for the UI
398
        status = chirp_common.Status()
399
        status.cur = 0
400
        status.max = (MEMORY_ADDRESS_RANGE[1] -
401
                      MEMORY_ADDRESS_RANGE[0])/MEMORY_RW_BLOCK_SIZE
402
        status.msg = 'Cloning from radio...'
403
        radio.status_fn(status)
404

    
405
        for addr in range(MEMORY_ADDRESS_RANGE[0],
406
                          MEMORY_ADDRESS_RANGE[1] + MEMORY_RW_BLOCK_SIZE,
407
                          MEMORY_RW_BLOCK_SIZE):
408
            read_command = struct.pack('>BHB', 0x52, addr,
409
                                       MEMORY_RW_BLOCK_SIZE)
410
            read_response = send_serial_command(serial, read_command,
411
                                                MEMORY_RW_BLOCK_CMD_SIZE)
412
            # LOG.debug('read response:\n%s' % util.hexprint(read_response))
413

    
414
            address, data, valid = parse_read_response(read_response)
415
            memory_data += data
416

    
417
            # update UI
418
            status.cur = (addr - MEMORY_ADDRESS_RANGE[0])\
419
                / MEMORY_RW_BLOCK_SIZE
420
            radio.status_fn(status)
421

    
422
        exit_program_mode(radio)
423
    except errors.RadioError as e:
424
        raise e
425
    except Exception as e:
426
        raise errors.RadioError('Failed to download from radio: %s' % e)
427

    
428
    return memmap.MemoryMapBytes(memory_data)
429

    
430

    
431
# Build a write data command to send to the radio
432
def make_write_data_cmd(addr, data, datalen):
433
    cmd = struct.pack('>BHB', 0x57, addr, datalen)
434
    cmd += data
435
    cs = checksum(ord(c) for c in cmd[1:])
436
    cmd += struct.pack('>BB', cs, 0x06)
437
    return cmd
438

    
439

    
440
# Upload a memory map to the radio
441
def do_upload(radio):
442
    try:
443
        bandlimit = enter_program_mode(radio)
444

    
445
        if bandlimit != radio._memobj.radio_settings.bandlimit:
446
            LOG.warning('radio and image bandlimits differ'
447
                        ' some channels many not work'
448
                        ' (img:0x%02x radio:0x%02x)' %
449
                        (int(bandlimit),
450
                         int(radio._memobj.radio_settings.bandlimit)))
451
            LOG.warning('radio bands: %s' % get_band_limits_Hz(
452
                         int(radio._memobj.radio_settings.bandlimit)))
453
            LOG.warning('img bands: %s' % get_band_limits_Hz(bandlimit))
454

    
455
        serial = radio.pipe
456

    
457
        # send the initial message, radio responds with something that looks a
458
        # bit like a bitfield, but I don't know what it is yet.
459
        read_command = struct.pack('>BHB', 0x52, 0x3b10, MEMORY_RW_BLOCK_SIZE)
460
        read_response = send_serial_command(serial, read_command,
461
                                            MEMORY_RW_BLOCK_CMD_SIZE)
462
        address, data, valid = parse_read_response(read_response)
463
        LOG.debug('Got initial response from radio: %s' %
464
                  util.hexprint(read_response))
465

    
466
        bptr = 0
467

    
468
        memory_addrs = range(MEMORY_ADDRESS_RANGE[0],
469
                             MEMORY_ADDRESS_RANGE[1] + MEMORY_RW_BLOCK_SIZE,
470
                             MEMORY_RW_BLOCK_SIZE)
471

    
472
        # status info for the UI
473
        status = chirp_common.Status()
474
        status.cur = 0
475
        status.max = len(memory_addrs)
476
        status.msg = 'Cloning to radio...'
477
        radio.status_fn(status)
478

    
479
        for idx, addr in enumerate(memory_addrs):
480
            write_command = make_write_data_cmd(
481
                addr, radio._mmap[bptr:bptr+MEMORY_RW_BLOCK_SIZE],
482
                MEMORY_RW_BLOCK_SIZE)
483
            # LOG.debug('write data:\n%s' % util.hexprint(write_command))
484
            write_response = send_serial_command(serial, write_command, 0x01)
485
            bptr += MEMORY_RW_BLOCK_SIZE
486

    
487
            if write_response == '\x0a':
488
                # NACK from radio, e.g. checksum wrongn
489
                LOG.debug('Radio returned 0x0a - NACK:')
490
                LOG.debug(' * write cmd:\n%s' % util.hexprint(write_command))
491
                LOG.debug(' * write response:\n%s' %
492
                          util.hexprint(write_response))
493
                exit_program_mode(radio)
494
                raise errors.RadioError('Radio NACK\'d write command')
495

    
496
            # update UI
497
            status.cur = idx
498
            radio.status_fn(status)
499
        exit_program_mode(radio)
500
    except errors.RadioError:
501
        raise
502
    except Exception as e:
503
        raise errors.RadioError('Failed to download from radio: %s' % e)
504

    
505

    
506
# Get the value of @bitfield @number of bits in from 0
507
def get_bitfield(bitfield, number):
508
    ''' Get the value of @bitfield @number of bits in '''
509
    byteidx = number//8
510
    bitidx = number - (byteidx * 8)
511
    return bitfield[byteidx] & (1 << bitidx)
512

    
513

    
514
# Set the @value of @bitfield @number of bits in from 0
515
def set_bitfield(bitfield, number, value):
516
    ''' Set the @value of @bitfield @number of bits in '''
517
    byteidx = number//8
518
    bitidx = number - (byteidx * 8)
519
    if value is True:
520
        bitfield[byteidx] |= (1 << bitidx)
521
    else:
522
        bitfield[byteidx] &= ~(1 << bitidx)
523
    return bitfield
524

    
525

    
526
# Translate the radio's version of a code as stored to a real code
527
def dtcs_code_bits_to_val(highbit, lowbyte):
528
    return chirp_common.ALL_DTCS_CODES[highbit*256 + lowbyte]
529

    
530

    
531
# Translate the radio's version of a tone as stored to a real tone
532
def ctcss_tone_bits_to_val(tone_byte):
533
    # TODO use the custom setting 0x33 and ref the custom ctcss
534
    # field
535
    tone_byte = int(tone_byte)
536
    if tone_byte in TONE_MAP_VAL_TO_TONE:
537
        return TONE_MAP_VAL_TO_TONE[tone_byte]
538
    elif tone_byte == TONE_CUSTOM_CTCSS:
539
        LOG.info('custom ctcss not implemented (yet?).')
540
    else:
541
        raise errors.UnsupportedToneError('unknown ctcss tone value: %02x' %
542
                                          tone_byte)
543

    
544

    
545
# Translate a real tone to the radio's version as stored
546
def ctcss_code_val_to_bits(tone_value):
547
    if tone_value in TONE_MAP_TONE_TO_VAL:
548
        return TONE_MAP_TONE_TO_VAL[tone_value]
549
    else:
550
        raise errors.UnsupportedToneError('Tone %f not supported' % tone_value)
551

    
552

    
553
# Translate a real code to the radio's version as stored
554
def dtcs_code_val_to_bits(code):
555
    val = chirp_common.ALL_DTCS_CODES.index(code)
556
    return (val & 0xFF), ((val >> 8) & 0x01)
557

    
558

    
559
class AnyTone778UVBase(chirp_common.CloneModeRadio,
560
                       chirp_common.ExperimentalRadio):
561
    '''AnyTone 778UV and probably Retivis RT95 and others'''
562
    BAUD_RATE = 9600
563
    NEEDS_COMPAT_SERIAL = False
564

    
565
    @classmethod
566
    def get_prompts(cls):
567
        rp = chirp_common.RadioPrompts()
568

    
569
        rp.experimental = \
570
            ('This is experimental support for the %s %s.  '
571
             'Please send in bug and enhancement requests!' %
572
             (cls.VENDOR, cls.MODEL))
573

    
574
        return rp
575

    
576
    # Return information about this radio's features, including
577
    # how many memories it has, what bands it supports, etc
578
    def get_features(self):
579
        rf = chirp_common.RadioFeatures()
580
        rf.has_bank = False
581
        rf.has_settings = True
582
        rf.can_odd_split = True
583
        rf.has_name = True
584
        rf.has_offset = True
585
        rf.valid_name_length = 5
586
        rf.valid_duplexes = ['', '+', '-', 'split', 'off']
587
        rf.valid_characters = CHARSET_ASCII_PLUS
588

    
589
        rf.has_dtcs = True
590
        rf.has_rx_dtcs = True
591
        rf.has_dtcs_polarity = True
592
        rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES
593
        rf.has_ctone = True
594
        rf.has_cross = True
595
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
596
        rf.valid_cross_modes = ['Tone->Tone',
597
                                'Tone->DTCS',
598
                                'DTCS->Tone',
599
                                'DTCS->DTCS',
600
                                'DTCS->',
601
                                '->DTCS',
602
                                '->Tone']
603

    
604
        rf.memory_bounds = (1, 200)  # This radio supports memories 1-200
605
        try:
606
            rf.valid_bands = get_band_limits_Hz(
607
                int(self._memobj.radio_settings.bandlimit))
608
        except TypeError as e:
609
            # If we're asked without memory loaded, assume the most permissive
610
            rf.valid_bands = get_band_limits_Hz(1)
611
        except Exception as e:
612
            LOG.error('Failed to get band limits for anytone778uv: %s' % e)
613
            rf.valid_bands = get_band_limits_Hz(1)
614
        rf.valid_modes = ['FM', 'NFM']
615
        rf.valid_power_levels = POWER_LEVELS
616
        rf.valid_tuning_steps = [2.5, 5, 6.25, 10, 12.5, 20, 25, 30, 50]
617
        rf.has_tuning_step = False
618
        return rf
619

    
620
    # Do a download of the radio from the serial port
621
    def sync_in(self):
622
        self._mmap = do_download(self)
623
        self.process_mmap()
624

    
625
    # Do an upload of the radio to the serial port
626
    def sync_out(self):
627
        do_upload(self)
628

    
629
    # Convert the raw byte array into a memory object structure
630
    def process_mmap(self):
631
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
632

    
633
    # Return a raw representation of the memory object, which
634
    # is very helpful for development
635
    def get_raw_memory(self, number):
636
        return repr(self._memobj.memory[number - 1])
637

    
638
    # Extract a high-level memory object from the low-level memory map
639
    # This is called to populate a memory in the UI
640
    def get_memory(self, number):
641
        number -= 1
642
        # Get a low-level memory object mapped to the image
643
        _mem = self._memobj.memory[number]
644
        _mem_status = self._memobj.memory_status
645

    
646
        # Create a high-level memory object to return to the UI
647
        mem = chirp_common.Memory()
648
        mem.number = number + 1           # Set the memory number
649

    
650
        # Check if this memory is present in the occupied list
651
        mem.empty = get_bitfield(_mem_status.occupied_bitfield, number) == 0
652

    
653
        if not mem.empty:
654
            # Check if this memory is in the scan enabled list
655
            mem.skip = ''
656
            if get_bitfield(_mem_status.scan_enabled_bitfield, number) == 0:
657
                mem.skip = 'S'
658

    
659
            # set the name
660
            mem.name = str(_mem.name).rstrip()  # Set the alpha tag
661

    
662
            # Convert your low-level frequency and offset to Hertz
663
            mem.freq = int(_mem.freq) * 10
664
            mem.offset = int(_mem.offset) * 10
665

    
666
            # Set the duplex flags
667
            if _mem.duplex == DUPLEX_POSSPLIT:
668
                mem.duplex = '+'
669
            elif _mem.duplex == DUPLEX_NEGSPLIT:
670
                mem.duplex = '-'
671
            elif _mem.duplex == DUPLEX_NOSPLIT:
672
                mem.duplex = ''
673
            elif _mem.duplex == DUPLEX_ODDSPLIT:
674
                mem.duplex = 'split'
675
            else:
676
                LOG.error('%s: get_mem: unhandled duplex: %02x' %
677
                          (mem.name, _mem.duplex))
678

    
679
            # handle tx off
680
            if _mem.tx_off:
681
                mem.duplex = 'off'
682

    
683
            # Set the channel width
684
            if _mem.channel_width == CHANNEL_WIDTH_25kHz:
685
                mem.mode = 'FM'
686
            elif _mem.channel_width == CHANNEL_WIDTH_20kHz:
687
                LOG.info(
688
                    '%s: get_mem: promoting 20kHz channel width to 25kHz' %
689
                    mem.name)
690
                mem.mode = 'FM'
691
            elif _mem.channel_width == CHANNEL_WIDTH_12d5kHz:
692
                mem.mode = 'NFM'
693
            else:
694
                LOG.error('%s: get_mem: unhandled channel width: 0x%02x' %
695
                          (mem.name, _mem.channel_width))
696

    
697
            # set the power level
698
            if _mem.txpower == TXPOWER_LOW:
699
                mem.power = POWER_LEVELS[0]
700
            elif _mem.txpower == TXPOWER_MED:
701
                mem.power = POWER_LEVELS[1]
702
            elif _mem.txpower == TXPOWER_HIGH:
703
                mem.power = POWER_LEVELS[2]
704
            else:
705
                LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
706
                          (mem.name, _mem.txpower))
707

    
708
            # CTCSS Tones
709
            # TODO support custom ctcss tones here
710
            txtone = None
711
            rxtone = None
712
            rxcode = None
713
            txcode = None
714

    
715
            # check if dtcs tx is enabled
716
            if _mem.dtcs_encode_en:
717
                txcode = dtcs_code_bits_to_val(_mem.dtcs_encode_code_highbit,
718
                                               _mem.dtcs_encode_code)
719

    
720
            # check if dtcs rx is enabled
721
            if _mem.dtcs_decode_en:
722
                rxcode = dtcs_code_bits_to_val(_mem.dtcs_decode_code_highbit,
723
                                               _mem.dtcs_decode_code)
724

    
725
            if txcode is not None:
726
                LOG.debug('%s: get_mem dtcs_enc: %d' % (mem.name, txcode))
727
            if rxcode is not None:
728
                LOG.debug('%s: get_mem dtcs_dec: %d' % (mem.name, rxcode))
729

    
730
            # tsql set if radio squelches on tone
731
            tsql = _mem.tone_squelch_en
732

    
733
            # check if ctcss tx is enabled
734
            if _mem.ctcss_encode_en:
735
                txtone = ctcss_tone_bits_to_val(_mem.ctcss_enc_tone)
736

    
737
            # check if ctcss rx is enabled
738
            if _mem.ctcss_decode_en:
739
                rxtone = ctcss_tone_bits_to_val(_mem.ctcss_dec_tone)
740

    
741
            # Define this here to allow a readable if-else tree enabling tone
742
            # options
743
            enabled = 0
744
            enabled |= (txtone is not None) * TONES_EN_TXTONE
745
            enabled |= (rxtone is not None) * TONES_EN_RXTONE
746
            enabled |= (txcode is not None) * TONES_EN_TXCODE
747
            enabled |= (rxcode is not None) * TONES_EN_RXCODE
748

    
749
            # Add some debugging output for the tone bitmap
750
            enstr = []
751
            if enabled & TONES_EN_TXTONE:
752
                enstr += ['TONES_EN_TXTONE']
753
            if enabled & TONES_EN_RXTONE:
754
                enstr += ['TONES_EN_RXTONE']
755
            if enabled & TONES_EN_TXCODE:
756
                enstr += ['TONES_EN_TXCODE']
757
            if enabled & TONES_EN_RXCODE:
758
                enstr += ['TONES_EN_RXCODE']
759
            if enabled == 0:
760
                enstr = ['TONES_EN_NOTONE']
761
            LOG.debug('%s: enabled = %s' % (
762
                mem.name, '|'.join(enstr)))
763

    
764
            mem.tmode = ''
765
            if enabled == TONES_EN_NO_TONE:
766
                mem.tmode = ''
767
            elif enabled == TONES_EN_TXTONE:
768
                mem.tmode = 'Tone'
769
                mem.rtone = txtone
770
            elif enabled == TONES_EN_RXTONE and tsql:
771
                mem.tmode = 'Cross'
772
                mem.cross_mode = '->Tone'
773
                mem.ctone = rxtone
774
            elif enabled == (TONES_EN_TXTONE | TONES_EN_RXTONE) and tsql:
775
                if txtone == rxtone:  # TSQL
776
                    mem.tmode = 'TSQL'
777
                    mem.ctone = txtone
778
                else:  # Tone->Tone
779
                    mem.tmode = 'Cross'
780
                    mem.cross_mode = 'Tone->Tone'
781
                    mem.ctone = rxtone
782
                    mem.rtone = txtone
783
            elif enabled == TONES_EN_TXCODE:
784
                mem.tmode = 'Cross'
785
                mem.cross_mode = 'DTCS->'
786
                mem.dtcs = txcode
787
            elif enabled == TONES_EN_RXCODE and tsql:
788
                mem.tmode = 'Cross'
789
                mem.cross_mode = '->DTCS'
790
                mem.rx_dtcs = rxcode
791
            elif enabled == (TONES_EN_TXCODE | TONES_EN_RXCODE) and tsql:
792
                if rxcode == txcode:
793
                    mem.tmode = 'DTCS'
794
                    mem.rx_dtcs = rxcode
795
                    # #8327 Not sure this is the correct interpretation of
796
                    # DevelopersToneModes, but it seems to make it work round
797
                    # tripping with the anytone software.  DTM implies that we
798
                    # might not need to set mem.dtcs, but if we do it only DTCS
799
                    # rx works (as if we were Cross:None->DTCS).
800
                    mem.dtcs = rxcode
801
                else:
802
                    mem.tmode = 'Cross'
803
                    mem.cross_mode = 'DTCS->DTCS'
804
                    mem.rx_dtcs = rxcode
805
                    mem.dtcs = txcode
806
            elif enabled == (TONES_EN_TXCODE | TONES_EN_RXTONE) and tsql:
807
                mem.tmode = 'Cross'
808
                mem.cross_mode = 'DTCS->Tone'
809
                mem.dtcs = txcode
810
                mem.ctone = rxtone
811
            elif enabled == (TONES_EN_TXTONE | TONES_EN_RXCODE) and tsql:
812
                mem.tmode = 'Cross'
813
                mem.cross_mode = 'Tone->DTCS'
814
                mem.rx_dtcs = rxcode
815
                mem.rtone = txtone
816
            else:
817
                LOG.error('%s: Unhandled tmode enabled = %d.' % (
818
                    mem.name, enabled))
819

    
820
                # Can get here if e.g. TONE_EN_RXCODE is set and tsql isn't
821
                # In that case should we perhaps store the tone and code values
822
                # if they're present and then setup tmode and cross_mode as
823
                # appropriate later?
824

    
825
            # set the dtcs polarity
826
            dtcs_pol_bit_to_str = {0: 'N', 1: 'R'}
827
            mem.dtcs_polarity = '%s%s' %\
828
                (dtcs_pol_bit_to_str[_mem.dtcs_encode_invert == 1],
829
                 dtcs_pol_bit_to_str[_mem.dtcs_decode_invert == 1])
830

    
831
        return mem
832

    
833
    # Store details about a high-level memory to the memory map
834
    # This is called when a user edits a memory in the UI
835
    def set_memory(self, mem):
836
        # Get a low-level memory object mapped to the image
837
        _mem = self._memobj.memory[mem.number - 1]
838
        _mem_status = self._memobj.memory_status
839

    
840
        # set the occupied bitfield
841
        _mem_status.occupied_bitfield = \
842
            set_bitfield(_mem_status.occupied_bitfield, mem.number - 1,
843
                         not mem.empty)
844

    
845
        # set the scan add bitfield
846
        _mem_status.scan_enabled_bitfield = \
847
            set_bitfield(_mem_status.scan_enabled_bitfield, mem.number - 1,
848
                         (not mem.empty) and (mem.skip != 'S'))
849

    
850
        if mem.empty:
851
            # Set the whole memory to 0xff
852
            _mem.set_raw('\xff' * (_mem.size() / 8))
853
        else:
854
            _mem.set_raw('\x00' * (_mem.size() / 8))
855

    
856
            _mem.freq = int(mem.freq / 10)
857
            _mem.offset = int(mem.offset / 10)
858

    
859
            _mem.name = mem.name.ljust(5)[:5]  # Store the alpha tag
860

    
861
            # TODO support busy channel lockout - disabled for now
862
            _mem.busy_channel_lockout = BUSY_CHANNEL_LOCKOUT_OFF
863

    
864
            # Set duplex bitfields
865
            if mem.duplex == '+':
866
                _mem.duplex = DUPLEX_POSSPLIT
867
            elif mem.duplex == '-':
868
                _mem.duplex = DUPLEX_NEGSPLIT
869
            elif mem.duplex == '':
870
                _mem.duplex = DUPLEX_NOSPLIT
871
            elif mem.duplex == 'split':
872
                # TODO: this is an unverified punt!
873
                _mem.duplex = DUPLEX_ODDSPLIT
874
            else:
875
                LOG.error('%s: set_mem: unhandled duplex: %s' %
876
                          (mem.name, mem.duplex))
877

    
878
            # handle tx off
879
            _mem.tx_off = 0
880
            if mem.duplex == 'off':
881
                _mem.tx_off = 1
882

    
883
            # Set the channel width - remember we promote 20kHz channels to FM
884
            # on import
885
            # , so don't handle them here
886
            if mem.mode == 'FM':
887
                _mem.channel_width = CHANNEL_WIDTH_25kHz
888
            elif mem.mode == 'NFM':
889
                _mem.channel_width = CHANNEL_WIDTH_12d5kHz
890
            else:
891
                LOG.error('%s: set_mem: unhandled mode: %s' % (
892
                    mem.name, mem.mode))
893

    
894
            # set the power level
895
            if mem.power == POWER_LEVELS[0]:
896
                _mem.txpower = TXPOWER_LOW
897
            elif mem.power == POWER_LEVELS[1]:
898
                _mem.txpower = TXPOWER_MED
899
            elif mem.power == POWER_LEVELS[2]:
900
                _mem.txpower = TXPOWER_HIGH
901
            else:
902
                LOG.error('%s: set_mem: unhandled power level: %s' %
903
                          (mem.name, mem.power))
904

    
905
            # TODO set the CTCSS values
906
            # TODO support custom ctcss tones here
907
            # Default - tones off, carrier sql
908
            _mem.ctcss_encode_en = 0
909
            _mem.ctcss_decode_en = 0
910
            _mem.tone_squelch_en = 0
911
            _mem.ctcss_enc_tone = 0x00
912
            _mem.ctcss_dec_tone = 0x00
913
            _mem.customctcss = 0x00
914
            _mem.dtcs_encode_en = 0
915
            _mem.dtcs_encode_code_highbit = 0
916
            _mem.dtcs_encode_code = 0
917
            _mem.dtcs_encode_invert = 0
918
            _mem.dtcs_decode_en = 0
919
            _mem.dtcs_decode_code_highbit = 0
920
            _mem.dtcs_decode_code = 0
921
            _mem.dtcs_decode_invert = 0
922

    
923
            dtcs_pol_str_to_bit = {'N': 0, 'R': 1}
924
            _mem.dtcs_encode_invert = dtcs_pol_str_to_bit[mem.dtcs_polarity[0]]
925
            _mem.dtcs_decode_invert = dtcs_pol_str_to_bit[mem.dtcs_polarity[1]]
926

    
927
            if mem.tmode == 'Tone':
928
                _mem.ctcss_encode_en = 1
929
                _mem.ctcss_enc_tone = ctcss_code_val_to_bits(mem.rtone)
930
            elif mem.tmode == 'TSQL':
931
                _mem.ctcss_encode_en = 1
932
                _mem.ctcss_enc_tone = ctcss_code_val_to_bits(mem.ctone)
933
                _mem.ctcss_decode_en = 1
934
                _mem.tone_squelch_en = 1
935
                _mem.ctcss_dec_tone = ctcss_code_val_to_bits(mem.ctone)
936
            elif mem.tmode == 'DTCS':
937
                _mem.dtcs_encode_en = 1
938
                _mem.dtcs_encode_code, _mem.dtcs_encode_code_highbit = \
939
                    dtcs_code_val_to_bits(mem.rx_dtcs)
940
                _mem.dtcs_decode_en = 1
941
                _mem.dtcs_decode_code, _mem.dtcs_decode_code_highbit = \
942
                    dtcs_code_val_to_bits(mem.rx_dtcs)
943
                _mem.tone_squelch_en = 1
944
            elif mem.tmode == 'Cross':
945
                txmode, rxmode = mem.cross_mode.split('->')
946

    
947
                if txmode == 'Tone':
948
                    _mem.ctcss_encode_en = 1
949
                    _mem.ctcss_enc_tone = ctcss_code_val_to_bits(mem.rtone)
950
                elif txmode == '':
951
                    pass
952
                elif txmode == 'DTCS':
953
                    _mem.dtcs_encode_en = 1
954
                    _mem.dtcs_encode_code, _mem.dtcs_encode_code_highbit = \
955
                        dtcs_code_val_to_bits(mem.dtcs)
956
                else:
957
                    LOG.error('%s: unhandled cross TX mode: %s' % (
958
                        mem.name, mem.cross_mode))
959

    
960
                if rxmode == 'Tone':
961
                    _mem.ctcss_decode_en = 1
962
                    _mem.tone_squelch_en = 1
963
                    _mem.ctcss_dec_tone = ctcss_code_val_to_bits(mem.ctone)
964
                elif rxmode == '':
965
                    pass
966
                elif rxmode == 'DTCS':
967
                    _mem.dtcs_decode_en = 1
968
                    _mem.dtcs_decode_code, _mem.dtcs_decode_code_highbit = \
969
                        dtcs_code_val_to_bits(mem.rx_dtcs)
970
                    _mem.tone_squelch_en = 1
971
                else:
972
                    LOG.error('%s: unhandled cross RX mode: %s' % (
973
                        mem.name, mem.cross_mode))
974
            else:
975
                LOG.error('%s: Unhandled tmode/cross %s/%s.' %
976
                          (mem.name, mem.tmode, mem.cross_mode))
977
            LOG.debug('%s: tmode=%s, cross=%s, rtone=%f, ctone=%f' % (
978
                mem.name, mem.tmode, mem.cross_mode, mem.rtone, mem.ctone))
979
            LOG.debug('%s: CENC=%d, CDEC=%d, t(enc)=%02x, t(dec)=%02x' % (
980
                mem.name,
981
                _mem.ctcss_encode_en,
982
                _mem.ctcss_decode_en,
983
                ctcss_code_val_to_bits(mem.rtone),
984
                ctcss_code_val_to_bits(mem.ctone)))
985

    
986
            # set unknown defaults, based on reading memory set by vendor tool
987
            _mem.unknown1 = 0x00
988
            _mem.unknown6 = 0x00
989
            _mem.unknown7 = 0x00
990
            _mem.unknown8 = 0x00
991
            _mem.unknown9 = 0x00
992
            _mem.unknown10 = 0x00
993

    
994

    
995
    def get_settings(self):
996
        """Translate the MEM_FORMAT structs into setstuf in the UI"""
997
        _mem = self._memobj
998
        _settings = self._memobj.settings
999
        _radio_settings = self._memobj.radio_settings
1000
        _password = self._memobj.password
1001
        #_workmode = self._memobj.workmodesettings
1002

    
1003
        function = RadioSettingGroup("function", "Function Setup")
1004
        group = RadioSettings(function)
1005

    
1006
        # MODE SET
1007
        # Channel Locked
1008
        rs = RadioSettingValueBoolean(_settings.channelLocked)
1009
        rset = RadioSetting("settings.channelLocked", "Channel locked", rs)
1010
        function.append(rset)
1011

    
1012
        # Menu 3 - Display Mode
1013
        options = ["Frequency", "Channel", "Name"]
1014
        rs = RadioSettingValueList(options, options[_settings.displayMode])
1015
        rset = RadioSetting("settings.displayMode", "Display Mode", rs)
1016
        function.append(rset)
1017

    
1018
        # VFO/MR A
1019
        options = ["MR", "VFO"]
1020
        rs = RadioSettingValueList(options, options[_radio_settings.vfomrModeA])
1021
        rset = RadioSetting("radio_settings.vfomrModeA", "VFO/MR mode A", rs)
1022
        function.append(rset)
1023

    
1024
        # MR Channel A
1025
        options = ["%s" % x for x in range(1, 201)]
1026
        rs = RadioSettingValueList(options, options[_radio_settings.mrChannelA])
1027
        rset = RadioSetting("radio_settings.mrChannelA", "MR channel A", rs)
1028
        function.append(rset)
1029

    
1030
        # VFO/MR B
1031
        options = ["MR", "VFO"]
1032
        rs = RadioSettingValueList(options, options[_radio_settings.vfomrModeB])
1033
        rset = RadioSetting("radio_settings.vfomrModeB", "VFO/MR mode B", rs)
1034
        function.append(rset)
1035

    
1036
        # MR Channel B
1037
        options = ["%s" % x for x in range(1, 201)]
1038
        rs = RadioSettingValueList(options, options[_radio_settings.mrChannelB])
1039
        rset = RadioSetting("radio_settings.mrChannelB", "MR channel B", rs)
1040
        function.append(rset)
1041

    
1042
        # DISPLAY SET
1043
        # Starting Display
1044
        name = ""
1045
        for i in range(7):  # 0 - 7
1046
            name += chr(self._memobj.starting_display.line[i])
1047
        name = name.rstrip()  # remove trailing spaces
1048

    
1049
        rs = RadioSettingValueString(0, 7, name)
1050
        rset = RadioSetting("starting_display.line", "Starting display", rs)
1051
        function.append(rset)
1052

    
1053
        # Menu 11 - Backlight Brightness
1054
        options = ["%s" % x for x in range(1, 4)]
1055
        rs = RadioSettingValueList(options, options[_settings.backlightBr - 1])
1056
        rset = RadioSetting("settings.backlightBr",
1057
                            "Backlight brightness", rs)
1058
        function.append(rset)
1059

    
1060
        # Menu 15 - Screen Direction
1061
        options = ["Positive", "Inverted"]
1062
        rs = RadioSettingValueList(options, options[_settings.screenDir])
1063
        rset = RadioSetting("settings.screenDir",
1064
                            "Screen direction", rs)
1065
        function.append(rset)
1066

    
1067
        # Hand Mic Key Brightness
1068
        options = ["%s" % x for x in range(1, 32)]
1069
        rs = RadioSettingValueList(options, options[_settings.micKeyBright - 1])
1070
        rset = RadioSetting("settings.micKeyBright",
1071
                            "Hand mic key brightness", rs)
1072
        function.append(rset)
1073

    
1074
        # VOL SET
1075
        # Menu 1 - Beep Volume
1076
        options = ["OFF"] + ["%s" % x for x in range(1, 6)]
1077
        rs = RadioSettingValueList(options, options[_settings.beepVolume])
1078
        rset = RadioSetting("settings.beepVolume",
1079
                            "Beep volume", rs)
1080
        function.append(rset)
1081

    
1082
        # Menu 5 - Volume level Setup
1083
        options = ["%s" % x for x in range(1, 37)]
1084
        rs = RadioSettingValueList(options, options[_settings.speakerVolume - 1])
1085
        rset = RadioSetting("settings.speakerVolume",
1086
                            "Speaker volume", rs)
1087
        function.append(rset)
1088

    
1089
        # Menu 16 - Speaker Switch
1090
        options = ["Host on|Hand mic off", "Host on|Hand mic on", "Host off|Hand mic on"]
1091
        rs = RadioSettingValueList(options, options[_settings.speakerSwitch])
1092
        rset = RadioSetting("settings.speakerSwitch",
1093
                            "Speaker switch", rs)
1094
        function.append(rset)
1095

    
1096
        # STE SET
1097
        # STE Frequency
1098
        options = ["Off", "55.2 Hz", "259.2 Hz"]
1099
        rs = RadioSettingValueList(options, options[_settings.steFrequency])
1100
        rset = RadioSetting("settings.steFrequency",
1101
                            "STE frequency", rs)
1102
        function.append(rset)
1103

    
1104
        # STE Type
1105
        options = ["Off", "Silent", "120 degrees", "180 degrees", "240 degrees"]
1106
        rs = RadioSettingValueList(options, options[_settings.steType])
1107
        rset = RadioSetting("settings.steType",
1108
                            "STE type", rs)
1109
        function.append(rset)
1110

    
1111
        # ON/OFF SET
1112
        # Power-on Password
1113
        rs = RadioSettingValueBoolean(_settings.powerOnPasswd)
1114
        rset = RadioSetting("settings.powerOnPasswd", "Power-on Password", rs)
1115
        function.append(rset)
1116

    
1117
        # Password
1118
        name = ""
1119
        for i in range(6):  # 0 - 6
1120
            name += chr(self._memobj.password.line[i])
1121
        name = name.rstrip()  # remove trailing spaces
1122

    
1123
        rs = RadioSettingValueString(0, 6, name)
1124
        rs.set_mutable(False)
1125
        rset = RadioSetting("password.line", "Password", rs)
1126
        function.append(rset)
1127

    
1128
        # Menu 9 - Auto Power On
1129
        rs = RadioSettingValueBoolean(_settings.autoPowerOn)
1130
        rset = RadioSetting("settings.autoPowerOn", "Auto power on", rs)
1131
        function.append(rset)
1132

    
1133
        # Menu 13 - Auto Power Off
1134
        options = ["Off", "30 minutes", "60 minutes", "120 minutes"]
1135
        rs = RadioSettingValueList(options, options[_settings.autoPowerOff])
1136
        rset = RadioSetting("settings.autoPowerOff",
1137
                            "Auto power off", rs)
1138
        function.append(rset)
1139

    
1140
        # Power On Reset Enable
1141
        rs = RadioSettingValueBoolean(_settings.powerOnReset)
1142
        rset = RadioSetting("settings.powerOnReset", "Power on reset", rs)
1143
        function.append(rset)
1144

    
1145
        # FUNCTION SET
1146
        # Menu 4 - Squelch Level A
1147
        options = ["OFF"] + ["%s" % x for x in range(1, 10)]
1148
        rs = RadioSettingValueList(options, options[_settings.squelchLevelA])
1149
        rset = RadioSetting("settings.squelchLevelA",
1150
                            "Squelch level A", rs)
1151
        function.append(rset)
1152

    
1153
        # Squelch Level B
1154
        options = ["OFF"] + ["%s" % x for x in range(1, 10)]
1155
        rs = RadioSettingValueList(options, options[_settings.squelchLevelB])
1156
        rset = RadioSetting("settings.squelchLevelB",
1157
                            "Squelch level B", rs)
1158
        function.append(rset)
1159

    
1160
        # Menu 7 - Scan Type
1161
        options = ["Time operated (TO)", "Carrier operated (CO)", "Search (SE)"]
1162
        rs = RadioSettingValueList(options, options[_settings.scanType])
1163
        rset = RadioSetting("settings.scanType",
1164
                            "Scan mode", rs)
1165
        function.append(rset)
1166

    
1167
        # Menu 8 - Scan Recovery Time
1168
        options = ["%s seconds" % x for x in range(5, 20, 5)]
1169
        rs = RadioSettingValueList(options, options[_settings.scanRecoveryT])
1170
        rset = RadioSetting("settings.scanRecoveryT",
1171
                            "Scan recovery time", rs)
1172
        function.append(rset)
1173

    
1174
        # Main
1175
        options = ["A", "B"]
1176
        rs = RadioSettingValueList(options, options[_settings.main])
1177
        rset = RadioSetting("settings.main",
1178
                            "Main", rs)
1179
        function.append(rset)
1180

    
1181
        # Menu 10 - Dual Watch (RX Way Select)
1182
        rs = RadioSettingValueBoolean(_settings.dualWatch)
1183
        rset = RadioSetting("settings.dualWatch", "Dual watch", rs)
1184
        function.append(rset)
1185

    
1186
        # Menu 12 - Time Out Timer
1187
        options = ["OFF"] + ["%s minutes" % x for x in range(1, 31)]
1188
        rs = RadioSettingValueList(options, options[_settings.timeOutTimer])
1189
        rset = RadioSetting("settings.timeOutTimer",
1190
                            "Time out timer", rs)
1191
        function.append(rset)
1192

    
1193
        # TBST Frequency
1194
        options = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"]
1195
        rs = RadioSettingValueList(options, options[_settings.tbstFrequency])
1196
        rset = RadioSetting("settings.tbstFrequency",
1197
                            "TBST frequency", rs)
1198
        function.append(rset)
1199

    
1200
        # Save Channel Perameter
1201
        rs = RadioSettingValueBoolean(_settings.saveChParameter)
1202
        rset = RadioSetting("settings.saveChParameter", "Save channel parameter", rs)
1203
        function.append(rset)
1204

    
1205
        # MON Key Function
1206
        options = ["Squelch off momentary", "Squelch off"]
1207
        rs = RadioSettingValueList(options, options[_settings.monKeyFunction])
1208
        rset = RadioSetting("settings.monKeyFunction",
1209
                            "MON key function", rs)
1210
        function.append(rset)
1211

    
1212
        # Frequency Step
1213
        options = ["2.5 KHz", "5 KHz", "6.25 KHz", "10 KHz", "12.5 KHz",
1214
                   "20 KHz", "25 KHz", "30 KHz", "50 KHz"]
1215
        rs = RadioSettingValueList(options, options[_settings.frequencyStep])
1216
        rset = RadioSetting("settings.frequencyStep",
1217
                            "Frequency step", rs)
1218
        function.append(rset)
1219

    
1220
        # Knob Mode
1221
        options = ["Volume", "Channel"]
1222
        rs = RadioSettingValueList(options, options[_settings.knobMode])
1223
        rset = RadioSetting("settings.knobMode",
1224
                            "Knob mode", rs)
1225
        function.append(rset)
1226

    
1227
        # TRF Enable
1228
        rs = RadioSettingValueBoolean(_settings.trfEnable)
1229
        rset = RadioSetting("settings.trfEnable", "TRF enable", rs)
1230
        function.append(rset)
1231

    
1232
        return group
1233

    
1234
    def set_settings(self, settings):
1235
        _settings = self._memobj.settings
1236
        _mem = self._memobj
1237
        for element in settings:
1238
            if not isinstance(element, RadioSetting):
1239
                self.set_settings(element)
1240
                continue
1241
            else:
1242
                try:
1243
                    name = element.get_name()
1244
                    if "." in name:
1245
                        bits = name.split(".")
1246
                        obj = self._memobj
1247
                        for bit in bits[:-1]:
1248
                            if "/" in bit:
1249
                                bit, index = bit.split("/", 1)
1250
                                index = int(index)
1251
                                obj = getattr(obj, bit)[index]
1252
                            else:
1253
                                obj = getattr(obj, bit)
1254
                        setting = bits[-1]
1255
                    else:
1256
                        obj = _settings
1257
                        setting = element.get_name()
1258

    
1259
                    if element.has_apply_callback():
1260
                        LOG.debug("Using apply callback")
1261
                        element.run_apply_callback()
1262
                    elif setting == "backlightBr":
1263
                        setattr(obj, setting, int(element.value) + 1)
1264
                    elif setting == "micKeyBright":
1265
                        setattr(obj, setting, int(element.value) + 1)
1266
                    elif setting == "speakerVolume":
1267
                        setattr(obj, setting, int(element.value) + 1)
1268
                    elif element.value.get_mutable():
1269
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1270
                        setattr(obj, setting, element.value)
1271
                except Exception, e:
1272
                    LOG.debug(element.get_name())
1273
                    raise
1274

    
1275
if has_future:
1276
    @directory.register
1277
    class AnyTone778UV(AnyTone778UVBase):
1278
        VENDOR = "AnyTone"
1279
        MODEL = "778UV"
1280
        # Allowed radio types is a dict keyed by model of a list of version
1281
        # strings
1282
        ALLOWED_RADIO_TYPES = {'AT778UV': ['V100', 'V200']}
1283

    
1284
    @directory.register
1285
    class RetevisRT95(AnyTone778UVBase):
1286
        VENDOR = "Retevis"
1287
        MODEL = "RT95"
1288
        # Allowed radio types is a dict keyed by model of a list of version
1289
        # strings
1290
        ALLOWED_RADIO_TYPES = {'RT95': ['V100']}
1291

    
1292
    @directory.register
1293
    class CRTMicronUV(AnyTone778UVBase):
1294
        VENDOR = "CRT"
1295
        MODEL = "Micron UV"
1296
        # Allowed radio types is a dict keyed by model of a list of version
1297
        # strings
1298
        ALLOWED_RADIO_TYPES = {'MICRON': ['V100']}
1299

    
1300
    @directory.register
1301
    class MidlandDBR2500(AnyTone778UVBase):
1302
        VENDOR = "Midland"
1303
        MODEL = "DBR2500"
1304
        # Allowed radio types is a dict keyed by model of a list of version
1305
        # strings
1306
        ALLOWED_RADIO_TYPES = {'DBR2500': ['V100']}
1307

    
1308
    @directory.register
1309
    class YedroYCM04vus(AnyTone778UVBase):
1310
        VENDOR = "Yedro"
1311
        MODEL = "YC-M04VUS"
1312
        # Allowed radio types is a dict keyed by model of a list of version
1313
        # strings
1314
        ALLOWED_RADIO_TYPES = {'YCM04UV': ['V100']}