Project

General

Profile

Feature #10512 » iradio_uv_5118plus(am for airband).py

set AM for 108-130 MHz - Jim Unroe, 04/12/2023 12:47 PM

 
1
# Copyright 2023 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 struct
17
import logging
18

    
19
from chirp import chirp_common, directory, memmap
20
from chirp import bitwise, errors, util
21
from chirp.settings import RadioSetting, RadioSettingGroup, \
22
    RadioSettingValueInteger, RadioSettingValueList, \
23
    RadioSettingValueBoolean, RadioSettingValueString, \
24
    RadioSettingValueFloat, RadioSettings
25

    
26
LOG = logging.getLogger(__name__)
27

    
28
MEM_FORMAT = """
29
struct memory {
30
  ul32 rxfreq;      // RX Frequency          00-03
31
  ul16 rx_tone;     // PL/DPL Decode         04-05
32
  ul32 txfreq;      // TX Frequency          06-09
33
  ul16 tx_tone;     // PL/DPL Encode         0a-0b
34
  ul24 mutecode;    // Mute Code             0c-0e
35
  u8 unknown_0:2,   //                       0f
36
     mutetype:2,    // Mute Type
37
     unknown_1:4;   //
38
  u8 isnarrow:1,    // Bandwidth             00
39
     lowpower:1,    // Power
40
     scan:1,        // Scan Add
41
     bcl:2,         // Busy Lock
42
     is_airband:1,  // Air Band (AM)
43
     unknown_3:1,   //
44
     unknown_4:1;   //
45
  u8 unknown_5;     //                       01
46
  u8 unused_0:4,    //                       02
47
     scno:4;        // SC No.
48
  u8 unknown_6[3];  //                       03-05
49
  char name[10];    //                       06-0f
50
};
51

    
52
#seekto 0x1000;
53
struct memory channels[999];
54

    
55
#seekto 0x0000;
56
struct {
57
  char startuplabel[32];  // Startup Label         0000-001f
58
  char personalid[16];    // Personal ID           0020-002f
59
  u8 displaylogo:1,       // Display Startup Logo  0030
60
     displayvoltage:1,    // Display Voltage
61
     displaylabel:1,      // Display Startup Label
62
     tailtone:1,          // Tail Tone
63
     startupringtone:1,   // Startup Ringtone
64
     voiceprompt:1,       // Voice Prompt
65
     keybeep:1,           // Key Beep
66
     unknown_0:1;
67
  u8 txpriority:1,        // TX Priority           0031
68
     rogerbeep:2,         // Roger Beep
69
     savemode:1,          // Save Mode
70
     frequencystep:4;     // Frequency Step
71
  u8 squelch:4,           // Squelch               0032
72
     talkaround:2,        // Talkaround
73
     noaaalarm:1,         // NOAA Alarm
74
     dualdisplay:1;       // Dual Display
75
  u8 displaytimer;        // Display Timer         0033
76
  u8 locktimer;           // Lock Timer            0034
77
  u8 timeouttimer;        // Timeout Timer         0035
78
  u8 voxlevel:4,          // VOX Level             0036
79
     voxdelay:4;          // Delay
80
  ul16 tonefrequency;     // Tone Frequency        0037-0038
81
  ul16 fmfrequency;       // FM Frequency          0039-003a
82
  u8 fmstandby:1,         // FM Standby            003b
83
     dualstandby:1,       // Dual Standby
84
     standbyarea:1,       // Standby Area
85
     scandirection:1,     // Scan Direction
86
     unknown_2:2,
87
     workmode:1,          // Work Mode
88
     unknown_3:1;
89
  ul16 areaach;           // Area A CH             003c-003d
90
  ul16 areabch;           // Area B CH             003e-003f
91
  u8 unused_0:4,          //                       0040
92
     key1long:4;          // Key 1 Long
93
  u8 unused_1:4,          //                       0041
94
     key1short:4;         // Key 1 Short
95
  u8 unused_2:4,          //                       0042
96
     key2long:4;          // Key 2 Long
97
  u8 unused_3:4,          //                       0043
98
     key2short:4;         // Key 2 Short
99
  u8 unknown_4:4,         //                       0044
100
     vox:1,               // VOX
101
     unknown_5:3;
102
  u8 xposition;           // X position (0-159)    0045
103
  u8 yposition;           // Y position (0-110)    0046
104
  ul16 bordercolor;       // Border  Color         0047-0048
105
  u8 unknown_6[9];        // 0x00                  0049-0051
106
  u8 unknown_7[2];        // 0xFF                  0052-0053
107
  u8 range174_240;        // 174-240 MHz           0054
108
  u8 range240_320;        // 240-320 MHz           0055
109
  u8 range320_400;        // 320-400 MHz           0056
110
  u8 range480_560;        // 480-560 MHz           0057
111
  u8 unused_4[7];         // 0xFF                  0058-005e
112
  u8 unknown_8;           // 0x00                  005f
113
  u8 unused_5[12];        // 0xFF                  0060-006b
114
  u8 unknown_9[4];        // 0x00                  006c-006f
115
  ul16 quickch2;          // Quick CH 2            0070-0071
116
  ul16 quickch1;          // Quick CH 1            0072-0073
117
  ul16 quickch4;          // Quick CH 4            0074-0075
118
  ul16 quickch3;          // Quick CH 3            0076-0077
119
} settings;
120

    
121
#seekto 0x8D20;
122
struct {
123
  u8 senddelay;           // Send Delay            8d20
124
  u8 sendinterval;        // Send Interval         8d21
125
  u8 unused_0:6,          //                       8d22
126
     sendmode:2;          // Send Mode
127
  u8 unused_2:4,          //                       8d23
128
     sendselect:4;        // Send Select
129
  u8 unused_3:7,          //                       8d24
130
     recvdisplay:1;       // Recv Display
131
  u8 encodegain;          // Encode Gain           8d25
132
  u8 decodeth;            // Decode TH             8d26
133
} dtmf;
134

    
135
#seekto 0x8D30;
136
struct {
137
  char code[14];          // DTMF code
138
  u8 unused_ff;
139
  u8 code_len;            // DTMF code length
140
} dtmfcode[16];
141

    
142
#seekto 0x8E30;
143
struct {
144
  char kill[14];          // Remotely Kill         8e30-8e3d
145
  u8 unknown_0;           //                       8e3e
146
  u8 kill_len;            // Remotely Kill Length  83ef
147
  char stun[14];          // Remotely Stun         8e40-834d
148
  u8 unknown_1;           //                       8e4e
149
  u8 stun_len;            // Remotely Stun Length  8e4f
150
  char wakeup[14];        // Wake Up               8e50-8e5d
151
  u8 unknown_2;           //                       8e5e
152
  u8 wakeup_len;          // Wake Up Length        8e5f
153
} dtmf2;
154

    
155
"""
156

    
157
CMD_ACK = b"\x06"
158

    
159
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
160

    
161
_STEP_LIST = [0.25, 1.25, 2.5, 5., 6.25, 10., 12.5, 25., 50., 100., 500.,
162
              1000., 5000.]
163

    
164
LIST_AB = ["A", "B"]
165
LIST_BCL = ["Off", "Carrier", "CTC/DCS"]
166
LIST_DELAY = ["%s ms" % x for x in range(0, 2100, 100)]
167
LIST_DIRECTION = ["Up", "Down"]
168
LIST_FREQSTEP = ["0.25K", "1.25K", "2.5K", "5K", "6.25K", "10K", "12.5K",
169
                 "20K", "25K", "50K", "100K", "500K", "1M", "5M"]
170
LIST_INTERVAL = ["%s ms" % x for x in range(30, 210, 10)]
171
LIST_MUTETYPE = ["Off", "-", "23b", "24b"]
172
LIST_ROGER = ["Off", "Roger 1", "Roger 2", "Send ID"]
173
LIST_SENDM = ["Off", "TX Start", "TX End", "Start and End"]
174
LIST_SENDS = ["DTMF %s" % x for x in range(1, 17)]
175
LIST_SKEY = ["None", "Monitor", "Frequency Detect", "Talkaround",
176
             "Quick CH", "Local Alarm", "Remote Alarm", "Weather CH",
177
             "Send Tone", "Roger Beep"]
178
LIST_REPEATER = ["Off", "Talkaround", "Frequency Reversal"]
179
LIST_TIMER = ["Off", "5 seconds", "10 seconds"] + [
180
              "%s seconds" % x for x in range(15, 615, 15)]
181
LIST_TXPRI = ["Edit", "Busy"]
182
LIST_WORKMODE = ["Frequency", "Channel"]
183

    
184
TXALLOW_CHOICES = ["RX Only", "TX/RX"]
185
TXALLOW_VALUES = [0xFF, 0x00]
186

    
187
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
188
    "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_"
189
DTMF_CHARS = list("0123456789ABCD*#")
190

    
191

    
192
def _checksum(data):
193
    cs = 0
194
    for byte in data:
195
        cs += byte
196
    return cs % 256
197

    
198

    
199
def _enter_programming_mode(radio):
200
    serial = radio.pipe
201

    
202
    exito = False
203
    for i in range(0, 5):
204
        serial.write(radio.magic)
205
        ack = serial.read(1)
206

    
207
        try:
208
            if ack == CMD_ACK:
209
                exito = True
210
                break
211
        except errors.RadioError:
212
            LOG.debug("Attempt #%s, failed, trying again" % i)
213
            pass
214

    
215
    # check if we had EXITO
216
    if exito is False:
217
        msg = "The radio did not accept program mode after five tries.\n"
218
        msg += "Check you interface cable and power cycle your radio."
219
        raise errors.RadioError(msg)
220

    
221

    
222
def _exit_programming_mode(radio):
223
    serial = radio.pipe
224
    try:
225
        serial.write(b"58" + b"\x05\xEE\x60")
226
    except errors.RadioError:
227
        raise errors.RadioError("Radio refused to exit programming mode")
228

    
229

    
230
def _read_block(radio, block_addr, block_size):
231
    serial = radio.pipe
232

    
233
    cmd = struct.pack(">BH", ord(b'R'), block_addr + radio.READ_OFFSET)
234

    
235
    ccs = bytes([_checksum(cmd)])
236

    
237
    expectedresponse = b"R" + cmd[1:]
238

    
239
    cmd = cmd + ccs
240

    
241
    LOG.debug("Reading block %04x..." % block_addr)
242

    
243
    try:
244
        serial.write(cmd)
245
        response = serial.read(3 + block_size + 1)
246

    
247
        cs = _checksum(response[:-1])
248

    
249
        if response[:3] != expectedresponse:
250
            raise Exception("Error reading block %04x." % block_addr)
251

    
252
        chunk = response[3:]
253

    
254
        if chunk[-1] != cs:
255
            raise Exception("Block failed checksum!")
256

    
257
        block_data = chunk[:-1]
258
    except errors.RadioError:
259
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
260

    
261
    return block_data
262

    
263

    
264
def _write_block(radio, block_addr, block_size):
265
    serial = radio.pipe
266

    
267
    # map the upload address to the mmap start and end addresses
268
    start_addr = block_addr * block_size
269
    end_addr = start_addr + block_size
270

    
271
    data = radio.get_mmap()[start_addr:end_addr]
272

    
273
    cmd = struct.pack(">BH", ord(b'I'), block_addr)
274

    
275
    cs = bytes([_checksum(cmd + data)])
276
    data += cs
277

    
278
    LOG.debug("Writing Data:")
279
    LOG.debug(util.hexprint(cmd + data))
280

    
281
    try:
282
        serial.write(cmd + data)
283
        if serial.read(1) != CMD_ACK:
284
            raise Exception("No ACK")
285
    except errors.RadioError:
286
        raise errors.RadioError("Failed to send block "
287
                                "to radio at %04x" % block_addr)
288

    
289

    
290
def do_download(radio):
291
    LOG.debug("download")
292
    _enter_programming_mode(radio)
293

    
294
    data = b""
295

    
296
    status = chirp_common.Status()
297
    status.msg = "Cloning from radio"
298

    
299
    status.cur = 0
300
    status.max = radio.END_ADDR
301

    
302
    for addr in range(radio.START_ADDR, radio.END_ADDR, 1):
303
        status.cur = addr
304
        radio.status_fn(status)
305

    
306
        block = _read_block(radio, addr, radio.BLOCK_SIZE)
307
        data += block
308

    
309
        LOG.debug("Address: %04x" % addr)
310
        LOG.debug(util.hexprint(block))
311

    
312
    _exit_programming_mode(radio)
313

    
314
    return memmap.MemoryMapBytes(data)
315

    
316

    
317
def do_upload(radio):
318
    status = chirp_common.Status()
319
    status.msg = "Uploading to radio"
320

    
321
    _enter_programming_mode(radio)
322

    
323
    status.cur = 0
324
    status.max = radio._memsize
325

    
326
    # The OEM software reads the 1st block from the radio before comencing
327
    # with the upload. That behavior will be mirrored here.
328
    _read_block(radio, radio.START_ADDR, radio.BLOCK_SIZE)
329

    
330
    for start_addr, end_addr in radio._ranges:
331
        for addr in range(start_addr, end_addr, 1):
332
            status.cur = addr * radio.BLOCK_SIZE
333
            radio.status_fn(status)
334
            _write_block(radio, addr, radio.BLOCK_SIZE)
335

    
336
    _exit_programming_mode(radio)
337

    
338

    
339
def _split(rf, f1, f2):
340
    """Returns False if the two freqs are in the same band (no split)
341
    or True otherwise"""
342

    
343
    # determine if the two freqs are in the same band
344
    for low, high in rf.valid_bands:
345
        if f1 >= low and f1 <= high and \
346
                f2 >= low and f2 <= high:
347
            # if the two freqs are on the same Band this is not a split
348
            return False
349

    
350
    # if you get here is because the freq pairs are split
351
    return True
352

    
353

    
354
class IradioUV5118plus(chirp_common.CloneModeRadio):
355
    """IRADIO UV5118plus"""
356
    VENDOR = "Iradio"
357
    MODEL = "UV-5118plus"
358
    NAME_LENGTH = 10
359
    BAUD_RATE = 115200
360
    NEEDS_COMPAT_SERIAL = False
361

    
362
    BLOCK_SIZE = 0x80
363
    magic = b"58" + b"\x05\x10\x82"
364

    
365
    VALID_BANDS = [(108000000, 136000000),  # RX only (Air Band)
366
                   (136000000, 174000000),  # TX/RX (VHF)
367
                   (174000000, 240000000),  # TX/RX
368
                   (240000000, 320000000),  # TX/RX
369
                   (320000000, 400000000),  # TX/RX
370
                   (400000000, 480000000),  # TX/RX (UHF)
371
                   (480000000, 560000000)]  # TX/RX
372

    
373
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
374
                    chirp_common.PowerLevel("Low", watts=0.50)]
375

    
376
    # Radio's write address starts at 0x0000
377
    # Radio's write address ends at 0x0140
378
    START_ADDR = 0
379
    END_ADDR = 0x0140
380
    # Radio's read address starts at 0x7820
381
    # Radio's read address ends at 0x795F
382
    READ_OFFSET = 0x7820
383

    
384
    _ranges = [
385
               (0x0000, 0x0140),
386
              ]
387
    _memsize = 0xA000  # 0x0140 * 0x80
388

    
389
    _upper = 999
390

    
391
    def get_features(self):
392
        rf = chirp_common.RadioFeatures()
393
        rf.has_settings = True
394
        rf.has_bank = False
395
        rf.has_ctone = True
396
        rf.has_cross = True
397
        rf.has_rx_dtcs = True
398
        rf.has_tuning_step = False
399
        rf.can_odd_split = True
400
        rf.has_name = True
401
        rf.valid_name_length = self.NAME_LENGTH
402
        rf.valid_characters = chirp_common.CHARSET_ASCII
403
        rf.valid_skips = ["", "S"]
404
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
405
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
406
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
407
        rf.valid_power_levels = self.POWER_LEVELS
408
        rf.valid_duplexes = ["", "-", "+", "split"]
409
        rf.valid_modes = ["FM", "NFM"]  # 25 KHz, 12.5 KHz.
410
        rf.valid_dtcs_codes = DTCS_CODES
411
        rf.memory_bounds = (1, self._upper)
412
        rf.valid_tuning_steps = _STEP_LIST
413
        rf.valid_bands = self.VALID_BANDS
414

    
415
        return rf
416

    
417
    def process_mmap(self):
418
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
419

    
420
    def sync_in(self):
421
        """Download from radio"""
422
        try:
423
            data = do_download(self)
424
        except errors.RadioError:
425
            # Pass through any real errors we raise
426
            raise
427
        except Exception:
428
            # If anything unexpected happens, make sure we raise
429
            # a RadioError and log the problem
430
            LOG.exception('Unexpected error during download')
431
            raise errors.RadioError('Unexpected error communicating '
432
                                    'with the radio')
433
        self._mmap = data
434
        self.process_mmap()
435

    
436
    def sync_out(self):
437
        """Upload to radio"""
438
        try:
439
            do_upload(self)
440
        except Exception:
441
            # If anything unexpected happens, make sure we raise
442
            # a RadioError and log the problem
443
            LOG.exception('Unexpected error during upload')
444
            raise errors.RadioError('Unexpected error communicating '
445
                                    'with the radio')
446

    
447
    def get_raw_memory(self, number):
448
        return repr(self._memobj.memory[number - 1])
449

    
450
    @staticmethod
451
    def _decode_tone(toneval):
452
        # DCS examples:
453
        # D023N - 1013 - 0001 0000 0001 0011
454
        #                   ^-DCS
455
        # D023I - 2013 - 0010 0000 0001 0100
456
        #                  ^--DCS inverted
457
        # D754I - 21EC - 0010 0001 1110 1100
458
        #    code in octal-------^^^^^^^^^^^
459

    
460
        if toneval == 0x3000:
461
            return '', None, None
462
        elif toneval & 0x1000:
463
            # DTCS N
464
            code = int('%o' % (toneval & 0x1FF))
465
            return 'DTCS', code, 'N'
466
        elif toneval & 0x2000:
467
            # DTCS R
468
            code = int('%o' % (toneval & 0x1FF))
469
            return 'DTCS', code, 'R'
470
        else:
471
            return 'Tone', toneval / 10.0, None
472

    
473
    @staticmethod
474
    def _encode_tone(mode, val, pol):
475
        if not mode:
476
            return 0x3000
477
        elif mode == 'Tone':
478
            return int(val * 10)
479
        elif mode == 'DTCS':
480
            code = int('%i' % val, 8)
481
            if pol == 'N':
482
                code |= 0x1800
483
            if pol == 'R':
484
                code |= 0x2800
485
            return code
486
        else:
487
            raise errors.RadioError('Unsupported tone mode %r' % mode)
488

    
489
    def get_memory(self, number):
490
        mem = chirp_common.Memory()
491
        _mem = self._memobj.channels[number - 1]
492
        mem.number = number
493

    
494
        mem.freq = int(_mem.rxfreq) * 10
495

    
496
        # We'll consider any blank (i.e. 0MHz frequency) to be empty
497
        if mem.freq == 0:
498
            mem.empty = True
499
            return mem
500

    
501
        if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF":
502
            mem.freq = 0
503
            mem.empty = True
504
            return mem
505

    
506
        # Freq and offset
507
        mem.freq = int(_mem.rxfreq) * 10
508
        # TX freq set
509
        offset = (int(_mem.txfreq) * 10) - mem.freq
510
        if offset != 0:
511
            if _split(self.get_features(), mem.freq, int(
512
                      _mem.txfreq) * 10):
513
                mem.duplex = "split"
514
                mem.offset = int(_mem.txfreq) * 10
515
            elif offset < 0:
516
                mem.offset = abs(offset)
517
                mem.duplex = "-"
518
            elif offset > 0:
519
                mem.offset = offset
520
                mem.duplex = "+"
521
        else:
522
            mem.offset = 0
523

    
524
        mem.name = str(_mem.name).rstrip('\xFF ')
525

    
526
        mem.mode = _mem.isnarrow and "NFM" or "FM"
527

    
528
        if mem.freq < 136000000:
529
            _mem.is_airband = True
530
        else:
531
            _mem.is_airband = False
532
            
533
        chirp_common.split_tone_decode(mem,
534
                                       self._decode_tone(_mem.tx_tone),
535
                                       self._decode_tone(_mem.rx_tone))
536

    
537
        mem.power = self.POWER_LEVELS[_mem.lowpower]
538

    
539
        if not _mem.scan:
540
            mem.skip = "S"
541

    
542
        mem.extra = RadioSettingGroup("Extra", "extra")
543

    
544
        rs = RadioSettingValueList(LIST_BCL, LIST_BCL[_mem.bcl])
545
        rset = RadioSetting("bcl", "Busy Channel Lockout", rs)
546
        mem.extra.append(rset)
547

    
548
        rs = RadioSettingValueList(LIST_MUTETYPE, LIST_MUTETYPE[_mem.mutetype])
549
        rset = RadioSetting("mutetype", "Mute Type", rs)
550
        mem.extra.append(rset)
551

    
552
        rs = RadioSettingValueInteger(0, 16777215, _mem.mutecode)
553
        rset = RadioSetting("mutecode", "Mute Code (0-16777215)", rs)
554
        mem.extra.append(rset)
555

    
556
        rs = RadioSettingValueInteger(0, 8, _mem.scno)
557
        rset = RadioSetting("scno", "SC No. (0-8)", rs)
558
        mem.extra.append(rset)
559

    
560
        return mem
561

    
562
    def set_memory(self, mem):
563
        LOG.debug("Setting %i(%s)" % (mem.number, mem.extd_number))
564
        _mem = self._memobj.channels[mem.number - 1]
565

    
566
        # if empty memmory
567
        if mem.empty:
568
            _mem.set_raw("\xFF" * 22 + "\20" * 10)
569
            return
570

    
571
        _mem.set_raw("\xFF" * 4 + "\x00\x30" + "\xFF" * 4 + "\x00\x30" +
572
                     "\x00" * 10 + "\x20" * 10)
573

    
574
        _mem.rxfreq = mem.freq / 10
575

    
576
        if mem.duplex == "split":
577
            _mem.txfreq = mem.offset / 10
578
        elif mem.duplex == "+":
579
            _mem.txfreq = (mem.freq + mem.offset) / 10
580
        elif mem.duplex == "-":
581
            _mem.txfreq = (mem.freq - mem.offset) / 10
582
        else:
583
            _mem.txfreq = mem.freq / 10
584

    
585
        _mem.name = mem.name.rstrip(' ').ljust(10, '\xFF')
586

    
587
        _mem.scan = mem.skip != "S"
588
        _mem.isnarrow = mem.mode == "NFM"
589

    
590
        # dtcs_pol = ["N", "N"]
591

    
592
        txtone, rxtone = chirp_common.split_tone_encode(mem)
593
        _mem.tx_tone = self._encode_tone(*txtone)
594
        _mem.rx_tone = self._encode_tone(*rxtone)
595

    
596
        _mem.lowpower = mem.power == self.POWER_LEVELS[1]
597

    
598
        for setting in mem.extra:
599
            setattr(_mem, setting.get_name(), setting.value)
600

    
601
    def get_settings(self):
602
        _dtmf = self._memobj.dtmf
603
        _dtmf2 = self._memobj.dtmf2
604
        _settings = self._memobj.settings
605
        basic = RadioSettingGroup("basic", "Basic Settings")
606
        dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
607
        startup = RadioSettingGroup("startup", "Startup Settings")
608
        txallow = RadioSettingGroup("txallow", "TX Allow Settings")
609
        top = RadioSettings(basic, dtmf, startup, txallow)
610

    
611
        # Basic Settings
612

    
613
        # Menu 21 - Personal ID
614
        _codeobj = _settings.personalid
615
        _code = str(_codeobj).rstrip('\x20')
616
        rs = RadioSettingValueString(0, 16, _code, True)
617
        rset = RadioSetting("personalid", "Personal ID", rs)
618
        basic.append(rset)
619

    
620
        rs = RadioSettingValueList(LIST_WORKMODE,
621
                                   LIST_WORKMODE[_settings.workmode])
622
        rset = RadioSetting("workmode", "Work Mode", rs)
623
        basic.append(rset)
624

    
625
        # Menu 05 - Voice Prompt
626
        rs = RadioSettingValueBoolean(_settings.voiceprompt)
627
        rset = RadioSetting("voiceprompt", "Voice Prompt", rs)
628
        basic.append(rset)
629

    
630
        # Menu 06 - Key Beep
631
        rs = RadioSettingValueBoolean(_settings.keybeep)
632
        rset = RadioSetting("keybeep", "Key Beep", rs)
633
        basic.append(rset)
634

    
635
        # Menu 07 - Roger Beep
636
        rs = RadioSettingValueList(LIST_ROGER,
637
                                   LIST_ROGER[_settings.rogerbeep])
638
        rset = RadioSetting("rogerbeep", "Roger Beep", rs)
639
        basic.append(rset)
640

    
641
        # Menu 09 - TX Priority
642
        rs = RadioSettingValueList(LIST_TXPRI,
643
                                   LIST_TXPRI[_settings.txpriority])
644
        rset = RadioSetting("txpriority", "TX Priority", rs)
645
        basic.append(rset)
646

    
647
        # Menu 10 - Save Mode
648
        rs = RadioSettingValueBoolean(_settings.savemode)
649
        rset = RadioSetting("savemode", "Save Mode", rs)
650
        basic.append(rset)
651

    
652
        # Menu 11 - Freq Step
653
        val = min(_settings.frequencystep, 0x0D)
654
        rs = RadioSettingValueList(LIST_FREQSTEP, LIST_FREQSTEP[val])
655
        rset = RadioSetting("frequencystep", "Frequency Step", rs)
656
        basic.append(rset)
657

    
658
        # Menu 12 - SQ Level
659
        val = min(_settings.squelch, 0x09)
660
        rs = RadioSettingValueInteger(0, 9, val)
661
        rset = RadioSetting("squelch", "Squelch Level (0-9)", rs)
662
        basic.append(rset)
663

    
664
        # Menu 13 - LED Timer
665
        val = min(_settings.displaytimer, 0x2A)
666
        rs = RadioSettingValueList(LIST_TIMER, LIST_TIMER[val])
667
        rset = RadioSetting("displaytimer", "Display Timer", rs)
668
        basic.append(rset)
669

    
670
        # Menu 14 - Lcok Timer
671
        val = min(_settings.locktimer, 0x2A)
672
        rs = RadioSettingValueList(LIST_TIMER, LIST_TIMER[val])
673
        rset = RadioSetting("locktimer", "Lock Timer", rs)
674
        basic.append(rset)
675

    
676
        # Menu 15 - TOT
677
        val = min(_settings.timeouttimer, 0x2A)
678
        rs = RadioSettingValueList(LIST_TIMER, LIST_TIMER[val])
679
        rset = RadioSetting("timeouttimer", "Timeout Timer", rs)
680
        basic.append(rset)
681

    
682
        rs = RadioSettingValueBoolean(_settings.vox)
683
        rset = RadioSetting("vox", "VOX", rs)
684
        basic.append(rset)
685

    
686
        # Menu 16 - VOX Level
687
        val = min(_settings.voxlevel, 0x09)
688
        rs = RadioSettingValueInteger(0, 9, val)
689
        rset = RadioSetting("voxlevel", "VOX Level (0-9)", rs)
690
        basic.append(rset)
691

    
692
        # Menu 17 - VOX Delay
693
        val = min(_settings.voxdelay, 0x09)
694
        rs = RadioSettingValueInteger(0, 9, val)
695
        rset = RadioSetting("voxdelay", "VOX Delay (0-9)", rs)
696
        basic.append(rset)
697

    
698
        # Menu 18 - NOAA Monitor
699
        rs = RadioSettingValueBoolean(_settings.noaaalarm)
700
        rset = RadioSetting("noaaalarm", "NOAA Alarm", rs)
701
        basic.append(rset)
702

    
703
        # Menu 19 - FM Standby
704
        rs = RadioSettingValueBoolean(_settings.fmstandby)
705
        rset = RadioSetting("fmstandby", "FM Standby", rs)
706
        basic.append(rset)
707

    
708
        def myset_freq(setting, obj, atrb, mult):
709
            """ Callback to set frequency by applying multiplier"""
710
            value = int(float(str(setting.value)) * mult)
711
            setattr(obj, atrb, value)
712
            return
713

    
714
        # FM Broadcast Settings
715
        val = _settings.fmfrequency
716
        val = val / 10.0
717
        if val < 64.0 or val > 108.0:
718
            val = 90.4
719
        rx = RadioSettingValueFloat(64.0, 108.0, val, 0.1, 1)
720
        rset = RadioSetting("fmfrequency", "Broadcast FM Freq (MHz)", rx)
721
        rset.set_apply_callback(myset_freq, _settings, "fmfrequency", 10)
722
        basic.append(rset)
723

    
724
        # Menu 20 - Tail Tone
725
        rs = RadioSettingValueBoolean(_settings.tailtone)
726
        rset = RadioSetting("tailtone", "Tail Tone", rs)
727
        basic.append(rset)
728

    
729
        # Menu 21 - Scan DIR
730
        rs = RadioSettingValueList(LIST_DIRECTION,
731
                                   LIST_DIRECTION[_settings.scandirection])
732
        rset = RadioSetting("scandirection", "Scan Direction", rs)
733
        basic.append(rset)
734

    
735
        # Menu 08 - Dual Display
736
        rs = RadioSettingValueBoolean(_settings.dualdisplay)
737
        rset = RadioSetting("dualdisplay", "Dual Display", rs)
738
        basic.append(rset)
739

    
740
        # Menu 23 - Repeater Mode
741
        val = min(_settings.talkaround, 0x02)
742
        rs = RadioSettingValueList(LIST_REPEATER, LIST_REPEATER[val])
743
        rset = RadioSetting("talkaround", "Talkaround", rs)
744
        basic.append(rset)
745

    
746
        # Menu 37 - K1 Short
747
        val = min(_settings.key1short, 0x09)
748
        rs = RadioSettingValueList(LIST_SKEY, LIST_SKEY[val])
749
        rset = RadioSetting("key1short", "Key 1 Short", rs)
750
        basic.append(rset)
751

    
752
        # Menu 36 - K1 Long
753
        val = min(_settings.key1long, 0x09)
754
        rs = RadioSettingValueList(LIST_SKEY, LIST_SKEY[val])
755
        rset = RadioSetting("key1long", "Key 1 Long", rs)
756
        basic.append(rset)
757

    
758
        # Menu 39 - K2 Short
759
        val = min(_settings.key2short, 0x09)
760
        rs = RadioSettingValueList(LIST_SKEY, LIST_SKEY[val])
761
        rset = RadioSetting("key2short", "Key 2 Short", rs)
762
        basic.append(rset)
763

    
764
        # Menu 38 - K2 Long
765
        val = min(_settings.key2long, 0x09)
766
        rs = RadioSettingValueList(LIST_SKEY, LIST_SKEY[val])
767
        rset = RadioSetting("key2long", "Key 2 Long", rs)
768
        basic.append(rset)
769

    
770
        rs = RadioSettingValueInteger(0, 20000, _settings.tonefrequency)
771
        rset = RadioSetting("tonefrequency", "Tone Frequency (0-2000)", rs)
772
        basic.append(rset)
773

    
774
        rs = RadioSettingValueBoolean(_settings.dualstandby)
775
        rset = RadioSetting("dualstandby", "Dual Standby", rs)
776
        basic.append(rset)
777

    
778
        rs = RadioSettingValueList(LIST_AB,
779
                                   LIST_AB[_settings.standbyarea])
780
        rset = RadioSetting("standbyarea", "Standby Area", rs)
781
        basic.append(rset)
782

    
783
        rs = RadioSettingValueInteger(1, 999, _settings.areaach + 1)
784
        rset = RadioSetting("areaach", "Area A CH (1-999)", rs)
785
        basic.append(rset)
786

    
787
        rs = RadioSettingValueInteger(1, 999, _settings.areabch + 1)
788
        rset = RadioSetting("areabch", "Area B CH (1-999)", rs)
789
        basic.append(rset)
790

    
791
        rs = RadioSettingValueInteger(1, 999, _settings.quickch1 + 1)
792
        rset = RadioSetting("quickch1", "Quick CH 1 (1-999)", rs)
793
        basic.append(rset)
794

    
795
        rs = RadioSettingValueInteger(1, 999, _settings.quickch2 + 1)
796
        rset = RadioSetting("quickch2", "Quick CH 2 (1-999)", rs)
797
        basic.append(rset)
798

    
799
        rs = RadioSettingValueInteger(1, 999, _settings.quickch3 + 1)
800
        rset = RadioSetting("quickch3", "Quick CH 3 (1-999)", rs)
801
        basic.append(rset)
802

    
803
        rs = RadioSettingValueInteger(1, 999, _settings.quickch4 + 1)
804
        rset = RadioSetting("quickch4", "Quick CH 4 (1-999)", rs)
805
        basic.append(rset)
806

    
807
        rs = RadioSettingValueInteger(0, 65535, _settings.bordercolor)
808
        rset = RadioSetting("bordercolor", "Border Color (0-65535)", rs)
809
        basic.append(rset)
810

    
811
        # DTMF Settings
812

    
813
        # Menu 40 - DTMF Delay
814
        val = min(_dtmf.senddelay, 0x14)
815
        rs = RadioSettingValueList(LIST_DELAY, LIST_DELAY[val])
816
        rset = RadioSetting("dtmf.senddelay", "Send Delay", rs)
817
        dtmf.append(rset)
818

    
819
        # Menu 41 - DTMF Interval
820
        val = min(_dtmf.sendinterval, 0x11)
821
        rs = RadioSettingValueList(LIST_INTERVAL, LIST_INTERVAL[val])
822
        rset = RadioSetting("dtmf.sendinterval", "Send Interval", rs)
823
        dtmf.append(rset)
824

    
825
        # Menu 42 - DTMF Mode
826
        rs = RadioSettingValueList(LIST_SENDM,
827
                                   LIST_SENDM[_dtmf.sendmode])
828
        rset = RadioSetting("dtmf.sendmode", "Send Mode", rs)
829
        dtmf.append(rset)
830

    
831
        # Menu 43 - DTMF Select
832
        rs = RadioSettingValueList(LIST_SENDS,
833
                                   LIST_SENDS[_dtmf.sendselect])
834
        rset = RadioSetting("dtmf.sendselect", "Send Select", rs)
835
        dtmf.append(rset)
836

    
837
        # Menu 44 - DTMF Display
838
        rs = RadioSettingValueBoolean(_dtmf.recvdisplay)
839
        rset = RadioSetting("dtmf.recvdisplay", "Receive Display", rs)
840
        dtmf.append(rset)
841

    
842
        val = min(_dtmf.encodegain, 0x7F)
843
        rs = RadioSettingValueInteger(0, 127, val)
844
        rset = RadioSetting("dtmf.encodegain", "Encode Gain (0-127)", rs)
845
        dtmf.append(rset)
846

    
847
        val = min(_dtmf.decodeth, 0x3F)
848
        rs = RadioSettingValueInteger(0, 63, val)
849
        rset = RadioSetting("dtmf.decodeth", "Decode TH (0-63)", rs)
850
        dtmf.append(rset)
851

    
852
        for i in range(0, 16):
853
            _codeobj = self._memobj.dtmfcode[i].code
854
            _code = str(_codeobj).rstrip('\xFF')
855
            rs = RadioSettingValueString(0, 14, _code, False)
856
            rs.set_charset(DTMF_CHARS)
857
            rset = RadioSetting("dtmfcode/%i.code" % i,
858
                                "Code %i" % (i + 1), rs)
859

    
860
            def apply_code(setting, obj, length):
861
                code = ""
862
                for char in str(setting.value):
863
                    if char in DTMF_CHARS:
864
                        code += char
865
                    else:
866
                        code += ""
867
                obj.code_len = len(str(code))
868
                obj.code = code.ljust(length, chr(255))
869
            rset.set_apply_callback(apply_code, self._memobj.dtmfcode[i], 14)
870
            dtmf.append(rset)
871

    
872
        _codeobj = _dtmf2.kill
873
        _code = str(_codeobj).rstrip('\xFF')
874
        rs = RadioSettingValueString(0, 14, _code, False)
875
        rs.set_charset(DTMF_CHARS)
876
        rset = RadioSetting("dtmf2.kill",
877
                            "Remotely Kill", rs)
878

    
879
        def apply_code(setting, obj, length):
880
            code = ""
881
            for char in str(setting.value):
882
                if char in DTMF_CHARS:
883
                    code += char
884
                else:
885
                    code += ""
886
            obj.kill_len = len(str(code))
887
            obj.kill = code.ljust(length, chr(255))
888
        rset.set_apply_callback(apply_code, _dtmf2, 14)
889
        dtmf.append(rset)
890

    
891
        _codeobj = _dtmf2.stun
892
        _code = str(_codeobj).rstrip('\xFF')
893
        rs = RadioSettingValueString(0, 14, _code, False)
894
        rs.set_charset(DTMF_CHARS)
895
        rset = RadioSetting("dtmf2.stun",
896
                            "Remotely Stun", rs)
897

    
898
        def apply_code(setting, obj, length):
899
            code = ""
900
            for char in str(setting.value):
901
                if char in DTMF_CHARS:
902
                    code += char
903
                else:
904
                    code += ""
905
            obj.stun_len = len(str(code))
906
            obj.stun = code.ljust(length, chr(255))
907
        rset.set_apply_callback(apply_code, _dtmf2, 14)
908
        dtmf.append(rset)
909

    
910
        _codeobj = _dtmf2.wakeup
911
        _code = str(_codeobj).rstrip('\xFF')
912
        rs = RadioSettingValueString(0, 14, _code, False)
913
        rs.set_charset(DTMF_CHARS)
914
        rset = RadioSetting("dtmf2.wakeup",
915
                            "Wake Up", rs)
916

    
917
        def apply_code(setting, obj, length):
918
            code = ""
919
            for char in str(setting.value):
920
                if char in DTMF_CHARS:
921
                    code += char
922
                else:
923
                    code += ""
924
            obj.wakeup_len = len(str(code))
925
            obj.wakeup = code.ljust(length, chr(255))
926
        rset.set_apply_callback(apply_code, _dtmf2, 14)
927
        dtmf.append(rset)
928

    
929
        # Startup Settings
930

    
931
        _codeobj = _settings.startuplabel
932
        _code = str(_codeobj).rstrip('\x20')
933
        rs = RadioSettingValueString(0, 32, _code, True)
934
        rset = RadioSetting("startuplabel", "Startup Label", rs)
935
        startup.append(rset)
936

    
937
        # Menu 04 - Prompt Text
938
        rs = RadioSettingValueBoolean(_settings.displaylabel)
939
        rset = RadioSetting("displaylabel", "Display Startup Label", rs)
940
        startup.append(rset)
941

    
942
        # Menu 02 - Voltage
943
        rs = RadioSettingValueBoolean(_settings.displayvoltage)
944
        rset = RadioSetting("displayvoltage", "Display Voltage", rs)
945
        startup.append(rset)
946

    
947
        # Menu 01 - Startup Logo
948
        rs = RadioSettingValueBoolean(_settings.displaylogo)
949
        rset = RadioSetting("displaylogo", "Display Startup Logo", rs)
950
        startup.append(rset)
951

    
952
        # Menu 03 - Ringtone
953
        rs = RadioSettingValueBoolean(_settings.startupringtone)
954
        rset = RadioSetting("startupringtone", "Startup Ringtone", rs)
955
        startup.append(rset)
956

    
957
        val = min(_settings.xposition, 0x9F)
958
        rs = RadioSettingValueInteger(0, 159, val)
959
        rset = RadioSetting("xposition", "X Position (0-159)", rs)
960
        startup.append(rset)
961

    
962
        val = min(_settings.yposition, 0x6E)
963
        rs = RadioSettingValueInteger(16, 110, val)
964
        rset = RadioSetting("yposition", "Y Position (16-110)", rs)
965
        startup.append(rset)
966

    
967
        # TX Allow Settings
968

    
969
        def apply_txallow_listvalue(setting, obj):
970
            LOG.debug("Setting value: " + str(
971
                      setting.value) + " from list")
972
            val = str(setting.value)
973
            index = TXALLOW_CHOICES.index(val)
974
            val = TXALLOW_VALUES[index]
975
            obj.set_value(val)
976

    
977
        if _settings.range174_240 in TXALLOW_VALUES:
978
            idx = TXALLOW_VALUES.index(_settings.range174_240)
979
        else:
980
            idx = TXALLOW_VALUES.index(0xFF)
981
        rs = RadioSettingValueList(TXALLOW_CHOICES, TXALLOW_CHOICES[idx])
982
        rset = RadioSetting("range174_240", "174-240 MHz", rs)
983
        rset.set_apply_callback(apply_txallow_listvalue,
984
                                _settings.range174_240)
985
        txallow.append(rset)
986

    
987
        if _settings.range240_320 in TXALLOW_VALUES:
988
            idx = TXALLOW_VALUES.index(_settings.range240_320)
989
        else:
990
            idx = TXALLOW_VALUES.index(0xFF)
991
        rs = RadioSettingValueList(TXALLOW_CHOICES, TXALLOW_CHOICES[idx])
992
        rset = RadioSetting("range240_320", "240-320 MHz", rs)
993
        rset.set_apply_callback(apply_txallow_listvalue,
994
                                _settings.range240_320)
995
        txallow.append(rset)
996

    
997
        if _settings.range320_400 in TXALLOW_VALUES:
998
            idx = TXALLOW_VALUES.index(_settings.range320_400)
999
        else:
1000
            idx = TXALLOW_VALUES.index(0xFF)
1001
        rs = RadioSettingValueList(TXALLOW_CHOICES, TXALLOW_CHOICES[idx])
1002
        rset = RadioSetting("range320_400", "320-400 MHz", rs)
1003
        rset.set_apply_callback(apply_txallow_listvalue,
1004
                                _settings.range320_400)
1005
        txallow.append(rset)
1006

    
1007
        if _settings.range480_560 in TXALLOW_VALUES:
1008
            idx = TXALLOW_VALUES.index(_settings.range480_560)
1009
        else:
1010
            idx = TXALLOW_VALUES.index(0xFF)
1011
        rs = RadioSettingValueList(TXALLOW_CHOICES, TXALLOW_CHOICES[idx])
1012
        rset = RadioSetting("range480_560", "480-560 MHz", rs)
1013
        rset.set_apply_callback(apply_txallow_listvalue,
1014
                                _settings.range480_560)
1015
        txallow.append(rset)
1016

    
1017
        return top
1018

    
1019
    def set_settings(self, settings):
1020
        for element in settings:
1021
            if not isinstance(element, RadioSetting):
1022
                self.set_settings(element)
1023
                continue
1024
            else:
1025
                try:
1026
                    name = element.get_name()
1027
                    if "." in name:
1028
                        bits = name.split(".")
1029
                        obj = self._memobj
1030
                        for bit in bits[:-1]:
1031
                            if "/" in bit:
1032
                                bit, index = bit.split("/", 1)
1033
                                index = int(index)
1034
                                obj = getattr(obj, bit)[index]
1035
                            else:
1036
                                obj = getattr(obj, bit)
1037
                        setting = bits[-1]
1038
                    else:
1039
                        obj = self._memobj.settings
1040
                        setting = element.get_name()
1041

    
1042
                    if element.has_apply_callback():
1043
                        LOG.debug("Using apply callback")
1044
                        element.run_apply_callback()
1045
                    elif setting in ["areaach",
1046
                                     "areabch",
1047
                                     "quickch1",
1048
                                     "quickch2",
1049
                                     "quickch3",
1050
                                     "quickch4"
1051
                                     ]:
1052
                        setattr(obj, setting, int(element.value) - 1)
1053
                    elif element.value.get_mutable():
1054
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1055
                        setattr(obj, setting, element.value)
1056
                except Exception as e:
1057
                    LOG.debug(element.get_name(), e)
1058
                    raise
1059

    
1060
    @classmethod
1061
    def match_model(cls, filedata, filename):
1062
        # This radio has always been post-metadata, so never do
1063
        # old-school detection
1064
        return False
1065

    
1066

    
1067
@directory.register
1068
class RuyageUV58PlusRadio(IradioUV5118plus):
1069
    """Ruyage UV58Plus"""
1070
    VENDOR = "Ruyage"
1071
    MODEL = "UV58Plus"
(1-1/2)