Project

General

Profile

Bug #4069 » leixen_dualbank.py

Alessandro Dogliotti, 12/10/2019 02:30 PM

 
1
# Copyright 2014 Tom Hayward <tom@tomh.us>
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 os
18
import logging
19

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

    
28
LOG = logging.getLogger(__name__)
29

    
30
MEM_FORMAT = """
31
#seekto 0x0184;
32
struct {
33
  u8 unknown:4,
34
     sql:4;              // squelch level
35
  u8 unknown0x0185;
36
  u8 obeep:1,            // open beep
37
     dw_off:1,           // dual watch (inverted)
38
     kbeep:1,            // key beep
39
     rbeep:1,            // roger beep
40
     unknown:2,
41
     ctdcsb:1,           // ct/dcs busy lock
42
     unknown:1;
43
  u8 alarm:1,            // alarm key
44
     unknown1:1,
45
     aliasen_off:1,      // alias enable (inverted)
46
     save:1,             // battery save
47
     unknown2:1,
48
     keypad_enable:1,    // microphone keypad enable
49
     mrcha:1,            // mr/cha
50
     vfomr:1;            // vfo/mr
51
  u8 keylock_off:1,      // key lock (inverted)
52
     txstop_off:1,       // tx stop (inverted)
53
     scanm:1,            // scan key/mode
54
     vir:1,              // vox inhibit on receive
55
     keylockm:2,         // key lock mode
56
     lamp:2;             // backlight
57
  u8 opendis:2,          // open display
58
     fmen_off:1,         // fm enable (inverted)
59
     unknown1:1,
60
     fmscan_off:1,       // fm scan (inverted)
61
     fmdw:1,             // fm dual watch
62
     unknown2:2;
63
  u8 step:4,             // step
64
     vol:4;              // volume
65
  u8 apo:4,              // auto power off
66
     tot:4;              // time out timer
67
  u8 unknown0x018C;
68
  u8 voxdt:4,            // vox delay time
69
     voxgain:4;          // vox gain
70
  u8 unknown0x018E;
71
  u8 unknown0x018F;
72
  u8 unknown:3,
73
     lptime:5;           // long press time
74
  u8 keyp2long:4,        // p2 key long press
75
     keyp2short:4;       // p2 key short press
76
  u8 keyp1long:4,        // p1 key long press
77
     keyp1short:4;       // p1 key short press
78
  u8 keyp3long:4,        // p3 key long press
79
     keyp3short:4;       // p3 key short press
80
  u8 unknown0x0194;
81
  u8 menuen:1,           // menu enable
82
     absel:1,            // a/b select
83
     unknown:2
84
     keymshort:4;        // m key short press
85
  u8 unknown:4,
86
     dtmfst:1,           // dtmf sidetone
87
     ackdecode:1,        // ack decode
88
     monitor:2;          // monitor
89
  u8 unknown1:3,
90
     reset:1,            // reset enable
91
     unknown2:1,
92
     keypadmic_off:1,    // keypad mic (inverted)
93
     unknown3:2;
94
  u8 unknown0x0198;
95
  u8 unknown1:3,
96
     dtmftime:5;         // dtmf digit time
97
  u8 unknown1:3,
98
     dtmfspace:5;        // dtmf digit space time
99
  u8 unknown1:2,
100
     dtmfdelay:6;        // dtmf first digit delay
101
  u8 unknown1:1,
102
     dtmfpretime:7;      // dtmf pretime
103
  u8 unknown1:2,
104
     dtmfdelay2:6;       // dtmf * and # digit delay
105
  u8 unknown1:3,
106
     smfont_off:1,       // small font (inverted)
107
     unknown:4;
108
} settings;
109

    
110
#seekto 0x01cd;
111
struct {
112
  u8 rssi136;            // squelch base level (vhf)
113
  u8 unknown0x01ce;
114
  u8 rssi400;            // squelch base level (uhf)
115
} service;
116

    
117
#seekto 0x0900;
118
struct {
119
  char user1[7];         // user message 1
120
  char unknown0x0907;
121
  char unknown0x0908[8];
122
  char unknown0x0910[8];
123
  char system[7];        // system message
124
  char unknown0x091F;
125
  char user2[7];         // user message 2
126
  char unknown0x0927;
127
} messages;
128

    
129
struct channel {
130
  bbcd rx_freq[4];
131
  bbcd tx_freq[4];
132
  u8 rx_tone;
133
  u8 rx_tmode_extra:6,
134
     rx_tmode:2;
135
  u8 tx_tone;
136
  u8 tx_tmode_extra:6,
137
     tx_tmode:2;
138
  u8 unknown5;
139
  u8 pttidoff:1,
140
     dtmfoff:1,
141
     %(unknownormode)s,
142
     tailcut:1,
143
     aliasop:1,
144
     talkaroundoff:1,
145
     voxoff:1,
146
     skip:1;
147
  u8 %(modeorpower)s,
148
     reverseoff:1,
149
     blckoff:1,
150
     unknown7:1,
151
     apro:3;
152
  u8 unknown8;
153
};
154

    
155
struct name {
156
    char name[7];
157
    u8 pad;
158
};
159

    
160
#seekto 0x%(chanstart)x;
161
struct channel default[%(defaults)i];
162
struct channel memory[199];
163

    
164
#seekto 0x%(namestart)x;
165
struct name defaultname[%(defaults)i];
166
struct name name[199];
167
"""
168

    
169

    
170
APO_LIST = ["OFF", "10M", "20M", "30M", "40M", "50M", "60M", "90M",
171
            "2H", "4H", "6H", "8H", "10H", "12H", "14H", "16H"]
172
SQL_LIST = ["%s" % x for x in range(0, 10)]
173
SCANM_LIST = ["CO", "TO"]
174
TOT_LIST = ["OFF"] + ["%s seconds" % x for x in range(10, 130, 10)]
175
_STEP_LIST = [2.5, 5., 6.25, 10., 12.50, 25.]
176
STEP_LIST = ["{}KHz".format(x) for x in _STEP_LIST]
177
MONITOR_LIST = ["CTC/DCS", "DTMF", "CTC/DCS and DTMF", "CTC/DCS or DTMF"]
178
VFOMR_LIST = ["MR", "VFO"]
179
MRCHA_LIST = ["MR CHA", "Freq. MR"]
180
VOL_LIST = ["OFF"] + ["%s" % x for x in range(1, 16)]
181
OPENDIS_LIST = ["All", "Lease Time", "User-defined", "Leixen"]
182
LAMP_LIST = ["OFF", "KEY", "CONT"]
183
KEYLOCKM_LIST = ["K+S", "PTT", "KEY", "ALL"]
184
ABSEL_LIST = ["B Channel",  "A Channel"]
185
VOXGAIN_LIST = ["%s" % x for x in range(1, 9)]
186
VOXDT_LIST = ["%s seconds" % x for x in range(1, 5)]
187
DTMFTIME_LIST = ["%i milliseconds" % x for x in range(50, 210, 10)]
188
DTMFDELAY_LIST = ["%i milliseconds" % x for x in range(0, 550, 50)]
189
DTMFPRETIME_LIST = ["%i milliseconds" % x for x in range(100, 1100, 100)]
190
DTMFDELAY2_LIST = ["%i milliseconds" % x for x in range(0, 450, 50)]
191

    
192
LPTIME_LIST = ["%i milliseconds" % x for x in range(500, 2600, 100)]
193
PFKEYLONG_LIST = ["OFF",
194
                  "FM",
195
                  "Monitor Momentary",
196
                  "Monitor Lock",
197
                  "SQ Off Momentary",
198
                  "Mute",
199
                  "SCAN",
200
                  "TX Power",
201
                  "EMG",
202
                  "VFO/MR",
203
                  "DTMF",
204
                  "CALL",
205
                  "Transmit 1750Hz",
206
                  "A/B",
207
                  "Talk Around",
208
                  "Reverse"
209
                  ]
210

    
211
PFKEYSHORT_LIST = ["OFF",
212
                   "FM",
213
                   "BandChange",
214
                   "Time",
215
                   "Monitor Lock",
216
                   "Mute",
217
                   "SCAN",
218
                   "TX Power",
219
                   "EMG",
220
                   "VFO/MR",
221
                   "DTMF",
222
                   "CALL",
223
                   "Transmit 1750Hz",
224
                   "A/B",
225
                   "Talk Around",
226
                   "Reverse"
227
                   ]
228

    
229
MODES = ["NFM", "FM"]
230
WTFTONES = map(float, xrange(56, 64))
231
TONES = WTFTONES + chirp_common.TONES
232
DTCS_CODES = [17, 50, 645] + chirp_common.DTCS_CODES
233
DTCS_CODES.sort()
234
TMODES = ["", "Tone", "DTCS", "DTCS"]
235

    
236

    
237
def _image_ident_from_data(data):
238
    return data[0x168:0x178]
239

    
240

    
241
def _image_ident_from_image(radio):
242
    return _image_ident_from_data(radio.get_mmap())
243

    
244

    
245
def checksum(frame):
246
    x = 0
247
    for b in frame:
248
        x ^= ord(b)
249
    return chr(x)
250

    
251

    
252
def make_frame(cmd, addr, data=""):
253
    payload = struct.pack(">H", addr) + data
254
    header = struct.pack(">BB", ord(cmd), len(payload))
255
    frame = header + payload
256
    return frame + checksum(frame)
257

    
258

    
259
def send(radio, frame):
260
    # LOG.debug("%04i P>R: %s" %
261
    #           (len(frame),
262
    #            util.hexprint(frame).replace("\n", "\n          ")))
263
    try:
264
        radio.pipe.write(frame)
265
    except Exception, e:
266
        raise errors.RadioError("Failed to communicate with radio: %s" % e)
267

    
268

    
269
def recv(radio, readdata=True):
270
    hdr = radio.pipe.read(4)
271
    # LOG.debug("%04i P<R: %s" %
272
    #           (len(hdr), util.hexprint(hdr).replace("\n", "\n          ")))
273
    if hdr == "\x09\x00\x09":
274
        raise errors.RadioError("Radio rejected command.")
275
    cmd, length, addr = struct.unpack(">BBH", hdr)
276
    length -= 2
277
    if readdata:
278
        data = radio.pipe.read(length)
279
        # LOG.debug("     P<R: %s" %
280
        #           util.hexprint(hdr + data).replace("\n", "\n          "))
281
        if len(data) != length:
282
            raise errors.RadioError("Radio sent %i bytes (expected %i)" % (
283
                len(data), length))
284
        chk = radio.pipe.read(1)
285
    else:
286
        data = ""
287
    return addr, data
288

    
289

    
290
def do_ident(radio):
291
    send(radio, "\x02\x06LEIXEN\x17")
292
    ident = radio.pipe.read(9)
293
    LOG.debug("     P<R: %s" %
294
              util.hexprint(ident).replace("\n", "\n          "))
295
    if ident != "\x06\x06leixen\x13":
296
        raise errors.RadioError("Radio refused program mode")
297
    radio.pipe.write("\x06\x00\x06")
298
    ack = radio.pipe.read(3)
299
    if ack != "\x06\x00\x06":
300
        raise errors.RadioError("Radio did not ack.")
301

    
302

    
303
def do_download(radio):
304
    do_ident(radio)
305

    
306
    data = ""
307
    data += "\xFF" * (0 - len(data))
308
    for addr in range(0, radio._memsize, 0x10):
309
        send(radio, make_frame("R", addr, chr(0x10)))
310
        _addr, _data = recv(radio)
311
        if _addr != addr:
312
            raise errors.RadioError("Radio sent unexpected address")
313
        data += _data
314

    
315
        status = chirp_common.Status()
316
        status.cur = addr
317
        status.max = radio._memsize
318
        status.msg = "Cloning from radio"
319
        radio.status_fn(status)
320

    
321
    finish(radio)
322

    
323
    return memmap.MemoryMap(data)
324

    
325

    
326
def do_upload(radio):
327
    _ranges = [(0x0d00, 0x2000)]
328

    
329
    image_ident = _image_ident_from_image(radio)
330
    if image_ident.startswith(radio._file_ident) and \
331
       radio._model_ident in image_ident:
332
        _ranges = radio._ranges
333

    
334
    do_ident(radio)
335

    
336
    for start, end in _ranges:
337
        LOG.debug('Uploading range 0x%04X - 0x%04X' % (start, end))
338
        for addr in range(start, end, 0x10):
339
            frame = make_frame("W", addr, radio._mmap[addr:addr + 0x10])
340
            send(radio, frame)
341
            # LOG.debug("     P<R: %s" %
342
            #           util.hexprint(frame).replace("\n", "\n          "))
343
            radio.pipe.write("\x06\x00\x06")
344
            ack = radio.pipe.read(3)
345
            if ack != "\x06\x00\x06":
346
                raise errors.RadioError("Radio refused block at %04x" % addr)
347

    
348
            status = chirp_common.Status()
349
            status.cur = addr
350
            status.max = radio._memsize
351
            status.msg = "Cloning to radio"
352
            radio.status_fn(status)
353

    
354
    finish(radio)
355

    
356

    
357
def finish(radio):
358
    send(radio, "\x64\x01\x6F\x0A")
359
    ack = radio.pipe.read(8)
360

    
361

    
362
# Declaring Aliases
363
class LT898UV(chirp_common.Alias):
364
    VENDOR = "LUITON"
365
    MODEL = "LT-898UV"
366

    
367

    
368
@directory.register
369
class LeixenVV898Radio(chirp_common.CloneModeRadio):
370

    
371
    """Leixen VV-898"""
372
    VENDOR = "Leixen"
373
    MODEL = "VV-898"
374
    ALIASES = [LT898UV, ]
375
    BAUD_RATE = 9600
376

    
377
    _file_ident = "Leixen"
378
    _model_ident = 'LX-\x89\x85\x63'
379

    
380
    _memsize = 0x2000
381
    _ranges = [
382
        (0x0000, 0x013f),
383
        (0x0148, 0x0167),
384
        (0x0184, 0x018f),
385
        (0x0190, 0x01cf),
386
        (0x0900, 0x090f),
387
        (0x0920, 0x0927),
388
        (0x0d00, 0x2000),
389
    ]
390

    
391
    _mem_formatter = {'unknownormode': 'unknown6:1',
392
                      'modeorpower': 'mode:1, power:1',
393
                      'chanstart': 0x0D00,
394
                      'namestart': 0x19B0,
395
                      'defaults': 3}
396
    _power_levels = [chirp_common.PowerLevel("Low", watts=4),
397
                     chirp_common.PowerLevel("High", watts=10)]
398

    
399
    def get_features(self):
400
        rf = chirp_common.RadioFeatures()
401
        rf.has_settings = True
402
        rf.has_cross = True
403
        rf.has_bank = False
404
        rf.has_tuning_step = False
405
        rf.can_odd_split = True
406
        rf.has_rx_dtcs = True
407
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
408
        rf.valid_modes = MODES
409
        rf.valid_cross_modes = [
410
            "Tone->Tone",
411
            "DTCS->",
412
            "->DTCS",
413
            "Tone->DTCS",
414
            "DTCS->Tone",
415
            "->Tone",
416
            "DTCS->DTCS"]
417
        rf.valid_characters = chirp_common.CHARSET_ASCII
418
        rf.valid_name_length = 7
419
        rf.valid_power_levels = self._power_levels
420
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
421
        rf.valid_skips = ["", "S"]
422
        rf.valid_tuning_steps = _STEP_LIST
423
        rf.valid_bands = [(136000000, 174000000),
424
                          (400000000, 470000000)]
425
        rf.memory_bounds = (1, 199)
426
        return rf
427

    
428
    def sync_in(self):
429
        try:
430
            self._mmap = do_download(self)
431
        except Exception, e:
432
            finish(self)
433
            raise errors.RadioError("Failed to download from radio: %s" % e)
434
        self.process_mmap()
435

    
436
    def process_mmap(self):
437
        self._memobj = bitwise.parse(
438
            MEM_FORMAT % self._mem_formatter, self._mmap)
439

    
440
    def sync_out(self):
441
        try:
442
            do_upload(self)
443
        except errors.RadioError:
444
            finish(self)
445
            raise
446
        except Exception, e:
447
            raise errors.RadioError("Failed to upload to radio: %s" % e)
448

    
449
    def get_raw_memory(self, number):
450
        name, mem = self._get_memobjs(number)
451
        return repr(name) + repr(mem)
452

    
453
    def _get_tone(self, mem, _mem):
454
        rx_tone = tx_tone = None
455

    
456
        tx_tmode = TMODES[_mem.tx_tmode]
457
        rx_tmode = TMODES[_mem.rx_tmode]
458

    
459
        if tx_tmode == "Tone":
460
            tx_tone = TONES[_mem.tx_tone - 1]
461
        elif tx_tmode == "DTCS":
462
            tx_tone = DTCS_CODES[_mem.tx_tone - 1]
463

    
464
        if rx_tmode == "Tone":
465
            rx_tone = TONES[_mem.rx_tone - 1]
466
        elif rx_tmode == "DTCS":
467
            rx_tone = DTCS_CODES[_mem.rx_tone - 1]
468

    
469
        tx_pol = _mem.tx_tmode == 0x03 and "R" or "N"
470
        rx_pol = _mem.rx_tmode == 0x03 and "R" or "N"
471

    
472
        chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
473
                                       (rx_tmode, rx_tone, rx_pol))
474

    
475
    def _is_txinh(self, _mem):
476
        raw_tx = ""
477
        for i in range(0, 4):
478
            raw_tx += _mem.tx_freq[i].get_raw()
479
        return raw_tx == "\xFF\xFF\xFF\xFF"
480

    
481
    def _get_memobjs(self, number):
482
        _mem = self._memobj.memory[number - 1]
483
        _name = self._memobj.name[number - 1]
484
        return _mem, _name
485

    
486
    def get_memory(self, number):
487
        _mem, _name = self._get_memobjs(number)
488

    
489
        mem = chirp_common.Memory()
490
        mem.number = number
491

    
492
        if _mem.get_raw()[:4] == "\xFF\xFF\xFF\xFF":
493
            mem.empty = True
494
            return mem
495

    
496
        mem.freq = int(_mem.rx_freq) * 10
497

    
498
        if self._is_txinh(_mem):
499
            mem.duplex = "off"
500
            mem.offset = 0
501
        elif int(_mem.rx_freq) == int(_mem.tx_freq):
502
            mem.duplex = ""
503
            mem.offset = 0
504
        elif abs(int(_mem.rx_freq) * 10 - int(_mem.tx_freq) * 10) > 70000000:
505
            mem.duplex = "split"
506
            mem.offset = int(_mem.tx_freq) * 10
507
        else:
508
            mem.duplex = int(_mem.rx_freq) > int(_mem.tx_freq) and "-" or "+"
509
            mem.offset = abs(int(_mem.rx_freq) - int(_mem.tx_freq)) * 10
510

    
511
        mem.name = str(_name.name).rstrip()
512

    
513
        self._get_tone(mem, _mem)
514
        mem.mode = MODES[_mem.mode]
515
        powerindex = _mem.power if _mem.power < len(self._power_levels) else -1
516
        mem.power = self._power_levels[powerindex]
517
        mem.skip = _mem.skip and "S" or ""
518

    
519
        mem.extra = RadioSettingGroup("Extra", "extra")
520

    
521
        opts = ["On", "Off"]
522
        rs = RadioSetting("blckoff", "Busy Channel Lockout",
523
                          RadioSettingValueList(
524
                              opts, opts[_mem.blckoff]))
525
        mem.extra.append(rs)
526
        opts = ["Off", "On"]
527
        rs = RadioSetting("tailcut", "Squelch Tail Elimination",
528
                          RadioSettingValueList(
529
                              opts, opts[_mem.tailcut]))
530
        mem.extra.append(rs)
531
        apro = _mem.apro if _mem.apro < 0x5 else 0
532
        opts = ["Off", "Compander", "Scrambler", "TX Scrambler",
533
                "RX Scrambler"]
534
        rs = RadioSetting("apro", "Audio Processing",
535
                          RadioSettingValueList(
536
                              opts, opts[apro]))
537
        mem.extra.append(rs)
538
        opts = ["On", "Off"]
539
        rs = RadioSetting("voxoff", "VOX",
540
                          RadioSettingValueList(
541
                              opts, opts[_mem.voxoff]))
542
        mem.extra.append(rs)
543
        opts = ["On", "Off"]
544
        rs = RadioSetting("pttidoff", "PTT ID",
545
                          RadioSettingValueList(
546
                              opts, opts[_mem.pttidoff]))
547
        mem.extra.append(rs)
548
        opts = ["On", "Off"]
549
        rs = RadioSetting("dtmfoff", "DTMF",
550
                          RadioSettingValueList(
551
                              opts, opts[_mem.dtmfoff]))
552
        mem.extra.append(rs)
553
        opts = ["Name", "Frequency"]
554
        aliasop = RadioSetting("aliasop", "Display",
555
                               RadioSettingValueList(
556
                                   opts, opts[_mem.aliasop]))
557
        mem.extra.append(aliasop)
558
        opts = ["On", "Off"]
559
        rs = RadioSetting("reverseoff", "Reverse Frequency",
560
                          RadioSettingValueList(
561
                              opts, opts[_mem.reverseoff]))
562
        mem.extra.append(rs)
563
        opts = ["On", "Off"]
564
        rs = RadioSetting("talkaroundoff", "Talk Around",
565
                          RadioSettingValueList(
566
                              opts, opts[_mem.talkaroundoff]))
567
        mem.extra.append(rs)
568

    
569
        return mem
570

    
571
    def _set_tone(self, mem, _mem):
572
        ((txmode, txtone, txpol),
573
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
574

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

    
588
    def set_memory(self, mem):
589
        _mem, _name = self._get_memobjs(mem.number)
590

    
591
        if mem.empty:
592
            _mem.set_raw("\xFF" * 16)
593
            return
594
        elif _mem.get_raw() == ("\xFF" * 16):
595
            _mem.set_raw("\xFF" * 8 + "\xFF\x00\xFF\x00\xFF\xFE\xF0\xFC")
596

    
597
        _mem.rx_freq = mem.freq / 10
598

    
599
        if mem.duplex == "off":
600
            for i in range(0, 4):
601
                _mem.tx_freq[i].set_raw("\xFF")
602
        elif mem.duplex == "split":
603
            _mem.tx_freq = mem.offset / 10
604
        elif mem.duplex == "+":
605
            _mem.tx_freq = (mem.freq + mem.offset) / 10
606
        elif mem.duplex == "-":
607
            _mem.tx_freq = (mem.freq - mem.offset) / 10
608
        else:
609
            _mem.tx_freq = mem.freq / 10
610

    
611
        self._set_tone(mem, _mem)
612

    
613
        _mem.power = mem.power and self._power_levels.index(mem.power) or 0
614
        _mem.mode = MODES.index(mem.mode)
615
        _mem.skip = mem.skip == "S"
616
        _name.name = mem.name.ljust(7)
617

    
618
        # autoset display to name if filled, else show frequency
619
        if mem.extra:
620
            # mem.extra only seems to be populated when called from edit panel
621
            aliasop = mem.extra["aliasop"]
622
        else:
623
            aliasop = None
624
        if mem.name:
625
            _mem.aliasop = False
626
            if aliasop and not aliasop.changed():
627
                aliasop.value = "Name"
628
        else:
629
            _mem.aliasop = True
630
            if aliasop and not aliasop.changed():
631
                aliasop.value = "Frequency"
632

    
633
        for setting in mem.extra:
634
            setattr(_mem, setting.get_name(), setting.value)
635

    
636
    def _get_settings(self):
637
        _settings = self._memobj.settings
638
        _service = self._memobj.service
639
        _msg = self._memobj.messages
640
        cfg_grp = RadioSettingGroup("cfg_grp", "Basic Settings")
641
        adv_grp = RadioSettingGroup("adv_grp", "Advanced Settings")
642
        key_grp = RadioSettingGroup("key_grp", "Key Assignment")
643
        group = RadioSettings(cfg_grp, adv_grp, key_grp)
644

    
645
        #
646
        # Basic Settings
647
        #
648
        rs = RadioSetting("apo", "Auto Power Off",
649
                          RadioSettingValueList(
650
                              APO_LIST, APO_LIST[_settings.apo]))
651
        cfg_grp.append(rs)
652
        rs = RadioSetting("sql", "Squelch Level",
653
                          RadioSettingValueList(
654
                              SQL_LIST, SQL_LIST[_settings.sql]))
655
        cfg_grp.append(rs)
656
        rs = RadioSetting("scanm", "Scan Mode",
657
                          RadioSettingValueList(
658
                              SCANM_LIST, SCANM_LIST[_settings.scanm]))
659
        cfg_grp.append(rs)
660
        rs = RadioSetting("tot", "Time Out Timer",
661
                          RadioSettingValueList(
662
                              TOT_LIST, TOT_LIST[_settings.tot]))
663
        cfg_grp.append(rs)
664
        rs = RadioSetting("step", "Step",
665
                          RadioSettingValueList(
666
                              STEP_LIST, STEP_LIST[_settings.step]))
667
        cfg_grp.append(rs)
668
        rs = RadioSetting("monitor", "Monitor",
669
                          RadioSettingValueList(
670
                              MONITOR_LIST, MONITOR_LIST[_settings.monitor]))
671
        cfg_grp.append(rs)
672
        rs = RadioSetting("vfomr", "VFO/MR",
673
                          RadioSettingValueList(
674
                              VFOMR_LIST, VFOMR_LIST[_settings.vfomr]))
675
        cfg_grp.append(rs)
676
        rs = RadioSetting("mrcha", "MR/CHA",
677
                          RadioSettingValueList(
678
                              MRCHA_LIST, MRCHA_LIST[_settings.mrcha]))
679
        cfg_grp.append(rs)
680
        rs = RadioSetting("vol", "Volume",
681
                          RadioSettingValueList(
682
                              VOL_LIST, VOL_LIST[_settings.vol]))
683
        cfg_grp.append(rs)
684
        rs = RadioSetting("opendis", "Open Display",
685
                          RadioSettingValueList(
686
                              OPENDIS_LIST, OPENDIS_LIST[_settings.opendis]))
687
        cfg_grp.append(rs)
688

    
689
        def _filter(name):
690
            filtered = ""
691
            for char in str(name):
692
                if char in chirp_common.CHARSET_ASCII:
693
                    filtered += char
694
                else:
695
                    filtered += " "
696
            LOG.debug("Filtered: %s" % filtered)
697
            return filtered
698

    
699
        rs = RadioSetting("messages.user1", "User-defined Message 1",
700
                          RadioSettingValueString(0, 7, _filter(_msg.user1)))
701
        cfg_grp.append(rs)
702
        rs = RadioSetting("messages.user2", "User-defined Message 2",
703
                          RadioSettingValueString(0, 7, _filter(_msg.user2)))
704
        cfg_grp.append(rs)
705

    
706
        val = RadioSettingValueString(0, 7, _filter(_msg.system))
707
        val.set_mutable(False)
708
        rs = RadioSetting("messages.system", "System Message", val)
709
        cfg_grp.append(rs)
710

    
711
        rs = RadioSetting("lamp", "Backlight",
712
                          RadioSettingValueList(
713
                              LAMP_LIST, LAMP_LIST[_settings.lamp]))
714
        cfg_grp.append(rs)
715
        rs = RadioSetting("keylockm", "Key Lock Mode",
716
                          RadioSettingValueList(
717
                              KEYLOCKM_LIST,
718
                              KEYLOCKM_LIST[_settings.keylockm]))
719
        cfg_grp.append(rs)
720
        rs = RadioSetting("absel", "A/B Select",
721
                          RadioSettingValueList(ABSEL_LIST,
722
                                                ABSEL_LIST[_settings.absel]))
723
        cfg_grp.append(rs)
724

    
725
        rs = RadioSetting("obeep", "Open Beep",
726
                          RadioSettingValueBoolean(_settings.obeep))
727
        cfg_grp.append(rs)
728
        rs = RadioSetting("rbeep", "Roger Beep",
729
                          RadioSettingValueBoolean(_settings.rbeep))
730
        cfg_grp.append(rs)
731
        rs = RadioSetting("keylock_off", "Key Lock",
732
                          RadioSettingValueBoolean(not _settings.keylock_off))
733
        cfg_grp.append(rs)
734
        rs = RadioSetting("ctdcsb", "CT/DCS Busy Lock",
735
                          RadioSettingValueBoolean(_settings.ctdcsb))
736
        cfg_grp.append(rs)
737
        rs = RadioSetting("alarm", "Alarm Key",
738
                          RadioSettingValueBoolean(_settings.alarm))
739
        cfg_grp.append(rs)
740
        rs = RadioSetting("save", "Battery Save",
741
                          RadioSettingValueBoolean(_settings.save))
742
        cfg_grp.append(rs)
743
        rs = RadioSetting("kbeep", "Key Beep",
744
                          RadioSettingValueBoolean(_settings.kbeep))
745
        cfg_grp.append(rs)
746
        rs = RadioSetting("reset", "Reset Enable",
747
                          RadioSettingValueBoolean(_settings.reset))
748
        cfg_grp.append(rs)
749
        rs = RadioSetting("smfont_off", "Small Font",
750
                          RadioSettingValueBoolean(not _settings.smfont_off))
751
        cfg_grp.append(rs)
752
        rs = RadioSetting("aliasen_off", "Alias Enable",
753
                          RadioSettingValueBoolean(not _settings.aliasen_off))
754
        cfg_grp.append(rs)
755
        rs = RadioSetting("txstop_off", "TX Stop",
756
                          RadioSettingValueBoolean(not _settings.txstop_off))
757
        cfg_grp.append(rs)
758
        rs = RadioSetting("dw_off", "Dual Watch",
759
                          RadioSettingValueBoolean(not _settings.dw_off))
760
        cfg_grp.append(rs)
761
        rs = RadioSetting("fmen_off", "FM Enable",
762
                          RadioSettingValueBoolean(not _settings.fmen_off))
763
        cfg_grp.append(rs)
764
        rs = RadioSetting("fmdw", "FM Dual Watch",
765
                          RadioSettingValueBoolean(_settings.fmdw))
766
        cfg_grp.append(rs)
767
        rs = RadioSetting("fmscan_off", "FM Scan",
768
                          RadioSettingValueBoolean(
769
                              not _settings.fmscan_off))
770
        cfg_grp.append(rs)
771
        rs = RadioSetting("keypadmic_off", "Keypad MIC",
772
                          RadioSettingValueBoolean(
773
                              not _settings.keypadmic_off))
774
        cfg_grp.append(rs)
775
        rs = RadioSetting("voxgain", "VOX Gain",
776
                          RadioSettingValueList(
777
                              VOXGAIN_LIST, VOXGAIN_LIST[_settings.voxgain]))
778
        cfg_grp.append(rs)
779
        rs = RadioSetting("voxdt", "VOX Delay Time",
780
                          RadioSettingValueList(
781
                              VOXDT_LIST, VOXDT_LIST[_settings.voxdt]))
782
        cfg_grp.append(rs)
783
        rs = RadioSetting("vir", "VOX Inhibit on Receive",
784
                          RadioSettingValueBoolean(_settings.vir))
785
        cfg_grp.append(rs)
786

    
787
        #
788
        # Advanced Settings
789
        #
790
        val = (_settings.dtmftime) - 5
791
        rs = RadioSetting("dtmftime", "DTMF Digit Time",
792
                          RadioSettingValueList(
793
                              DTMFTIME_LIST, DTMFTIME_LIST[val]))
794
        adv_grp.append(rs)
795
        val = (_settings.dtmfspace) - 5
796
        rs = RadioSetting("dtmfspace", "DTMF Digit Space Time",
797
                          RadioSettingValueList(
798
                              DTMFTIME_LIST, DTMFTIME_LIST[val]))
799
        adv_grp.append(rs)
800
        val = (_settings.dtmfdelay) / 5
801
        rs = RadioSetting("dtmfdelay", "DTMF 1st Digit Delay",
802
                          RadioSettingValueList(
803
                              DTMFDELAY_LIST, DTMFDELAY_LIST[val]))
804
        adv_grp.append(rs)
805
        val = (_settings.dtmfpretime) / 10 - 1
806
        rs = RadioSetting("dtmfpretime", "DTMF Pretime",
807
                          RadioSettingValueList(
808
                              DTMFPRETIME_LIST, DTMFPRETIME_LIST[val]))
809
        adv_grp.append(rs)
810
        val = (_settings.dtmfdelay2) / 5
811
        rs = RadioSetting("dtmfdelay2", "DTMF * and # Digit Delay",
812
                          RadioSettingValueList(
813
                              DTMFDELAY2_LIST, DTMFDELAY2_LIST[val]))
814
        adv_grp.append(rs)
815
        rs = RadioSetting("ackdecode", "ACK Decode",
816
                          RadioSettingValueBoolean(_settings.ackdecode))
817
        adv_grp.append(rs)
818
        rs = RadioSetting("dtmfst", "DTMF Sidetone",
819
                          RadioSettingValueBoolean(_settings.dtmfst))
820
        adv_grp.append(rs)
821

    
822
        rs = RadioSetting("service.rssi400", "Squelch Base Level (UHF)",
823
                          RadioSettingValueInteger(0, 255, _service.rssi400))
824
        adv_grp.append(rs)
825
        rs = RadioSetting("service.rssi136", "Squelch Base Level (VHF)",
826
                          RadioSettingValueInteger(0, 255, _service.rssi136))
827
        adv_grp.append(rs)
828

    
829
        #
830
        # Key Settings
831
        #
832
        val = (_settings.lptime) - 5
833
        rs = RadioSetting("lptime", "Long Press Time",
834
                          RadioSettingValueList(
835
                              LPTIME_LIST, LPTIME_LIST[val]))
836
        key_grp.append(rs)
837
        rs = RadioSetting("keyp1long", "P1 Long Key",
838
                          RadioSettingValueList(
839
                              PFKEYLONG_LIST,
840
                              PFKEYLONG_LIST[_settings.keyp1long]))
841
        key_grp.append(rs)
842
        rs = RadioSetting("keyp1short", "P1 Short Key",
843
                          RadioSettingValueList(
844
                              PFKEYSHORT_LIST,
845
                              PFKEYSHORT_LIST[_settings.keyp1short]))
846
        key_grp.append(rs)
847
        rs = RadioSetting("keyp2long", "P2 Long Key",
848
                          RadioSettingValueList(
849
                              PFKEYLONG_LIST,
850
                              PFKEYLONG_LIST[_settings.keyp2long]))
851
        key_grp.append(rs)
852
        rs = RadioSetting("keyp2short", "P2 Short Key",
853
                          RadioSettingValueList(
854
                              PFKEYSHORT_LIST,
855
                              PFKEYSHORT_LIST[_settings.keyp2short]))
856
        key_grp.append(rs)
857
        rs = RadioSetting("keyp3long", "P3 Long Key",
858
                          RadioSettingValueList(
859
                              PFKEYLONG_LIST,
860
                              PFKEYLONG_LIST[_settings.keyp3long]))
861
        key_grp.append(rs)
862
        rs = RadioSetting("keyp3short", "P3 Short Key",
863
                          RadioSettingValueList(
864
                              PFKEYSHORT_LIST,
865
                              PFKEYSHORT_LIST[_settings.keyp3short]))
866
        key_grp.append(rs)
867

    
868
        val = RadioSettingValueList(PFKEYSHORT_LIST,
869
                                    PFKEYSHORT_LIST[_settings.keymshort])
870
        val.set_mutable(_settings.menuen == 0)
871
        rs = RadioSetting("keymshort", "M Short Key", val)
872
        key_grp.append(rs)
873
        val = RadioSettingValueBoolean(_settings.menuen)
874
        rs = RadioSetting("menuen", "Menu Enable", val)
875
        key_grp.append(rs)
876

    
877
        return group
878

    
879
    def get_settings(self):
880
        try:
881
            return self._get_settings()
882
        except:
883
            import traceback
884
            LOG.error("Failed to parse settings: %s", traceback.format_exc())
885
            return None
886

    
887
    def set_settings(self, settings):
888
        _settings = self._memobj.settings
889
        for element in settings:
890
            if not isinstance(element, RadioSetting):
891
                self.set_settings(element)
892
                continue
893
            else:
894
                try:
895
                    name = element.get_name()
896
                    if "." in name:
897
                        bits = name.split(".")
898
                        obj = self._memobj
899
                        for bit in bits[:-1]:
900
                            if "/" in bit:
901
                                bit, index = bit.split("/", 1)
902
                                index = int(index)
903
                                obj = getattr(obj, bit)[index]
904
                            else:
905
                                obj = getattr(obj, bit)
906
                        setting = bits[-1]
907
                    else:
908
                        obj = _settings
909
                        setting = element.get_name()
910

    
911
                    if element.has_apply_callback():
912
                        LOG.debug("Using apply callback")
913
                        element.run_apply_callback()
914
                    elif setting == "keylock_off":
915
                        setattr(obj, setting, not int(element.value))
916
                    elif setting == "smfont_off":
917
                        setattr(obj, setting, not int(element.value))
918
                    elif setting == "aliasen_off":
919
                        setattr(obj, setting, not int(element.value))
920
                    elif setting == "txstop_off":
921
                        setattr(obj, setting, not int(element.value))
922
                    elif setting == "dw_off":
923
                        setattr(obj, setting, not int(element.value))
924
                    elif setting == "fmen_off":
925
                        setattr(obj, setting, not int(element.value))
926
                    elif setting == "fmscan_off":
927
                        setattr(obj, setting, not int(element.value))
928
                    elif setting == "keypadmic_off":
929
                        setattr(obj, setting, not int(element.value))
930
                    elif setting == "dtmftime":
931
                        setattr(obj, setting, int(element.value) + 5)
932
                    elif setting == "dtmfspace":
933
                        setattr(obj, setting, int(element.value) + 5)
934
                    elif setting == "dtmfdelay":
935
                        setattr(obj, setting, int(element.value) * 5)
936
                    elif setting == "dtmfpretime":
937
                        setattr(obj, setting, (int(element.value) + 1) * 10)
938
                    elif setting == "dtmfdelay2":
939
                        setattr(obj, setting, int(element.value) * 5)
940
                    elif setting == "lptime":
941
                        setattr(obj, setting, int(element.value) + 5)
942
                    else:
943
                        LOG.debug("Setting %s = %s" % (setting, element.value))
944
                        setattr(obj, setting, element.value)
945
                except Exception, e:
946
                    LOG.debug(element.get_name())
947
                    raise
948

    
949
    @classmethod
950
    def match_model(cls, filedata, filename):
951
        if filedata[0x168:0x170].startswith(cls._file_ident) and \
952
           filedata[0x170:0x178].startswith(cls._model_ident):
953
            return True
954
        else:
955
            return False
956

    
957

    
958
@directory.register
959
class JetstreamJT270MRadio(LeixenVV898Radio):
960

    
961
    """Jetstream JT270M"""
962
    VENDOR = "Jetstream"
963
    MODEL = "JT270M"
964

    
965
    _file_ident = "JET"
966
    _model_ident = 'LX-\x89\x85\x53'
967

    
968

    
969
class VV898E(chirp_common.Alias):
970

    
971
    '''Leixen has called this radio both 898E and S historically, ident is
972
    identical'''
973
    VENDOR = "Leixen"
974
    MODEL = "VV-898E"
975

    
976

    
977
@directory.register
978
class LeixenVV898SRadio(LeixenVV898Radio):
979

    
980
    """Leixen VV-898S, also VV-898E which is identical"""
981
    VENDOR = "Leixen"
982
    MODEL = "VV-898S"
983
    ALIASES = [VV898E, ]
984

    
985
    _model_ident = 'LX-\x89\x85\x75'
986
    _mem_formatter = {'unknownormode': 'mode:1',
987
                      'modeorpower': 'power:2',
988
                      'chanstart': 0x0D00,
989
                      'namestart': 0x19B0,
990
                      'defaults': 3}
991
    _power_levels = [chirp_common.PowerLevel("Low", watts=5),
992
                     chirp_common.PowerLevel("Med", watts=10),
993
                     chirp_common.PowerLevel("High", watts=25)]
994

    
995

    
996
@directory.register
997
class LeixenVV898SDualBankRadio(LeixenVV898SRadio):
998

    
999
    """Jetstream JT270MH"""
1000
    VENDOR = "Leixen"
1001
    MODEL = "VV898SDualBank"
1002

    
1003
    _file_ident = "Leixen"
1004
    _model_ident = 'LX-\x89\x85\x85'
1005
    _ranges = [
1006
        (0x0000, 0x013f),
1007
        (0x0148, 0x0167),
1008
        (0x0184, 0x018f),
1009
        (0x0190, 0x01cf),
1010
        (0x0900, 0x090f),
1011
        (0x0920, 0x0927),
1012
        (0x0c00, 0x2000),
1013
    ]
1014
    _mem_formatter = {'unknownormode': 'mode:1',
1015
                      'modeorpower': 'power:2',
1016
                      'chanstart': 0x0C00,
1017
                      'namestart': 0x1900,
1018
                      'defaults': 6}
1019

    
1020
    def get_features(self):
1021
        rf = super(LeixenVV898SDualBankRadio, self).get_features()
1022
        rf.has_sub_devices = self.VARIANT == ''
1023
        rf.memory_bounds = (1, 99)
1024
        return rf
1025

    
1026
    def get_sub_devices(self):
1027
        return [LeixenVV898SDualBankRadioA(self._mmap),
1028
                LeixenVV898SDualBankRadioB(self._mmap)]
1029

    
1030
    def _get_memobjs(self, number):
1031
        number = number * 2 - self._offset
1032
        _mem = self._memobj.memory[number]
1033
        _name = self._memobj.name[number]
1034
        return _mem, _name
1035

    
1036

    
1037
class LeixenVV898SDualBankRadioA(LeixenVV898SDualBankRadio):
1038
    VARIANT = 'A Band'
1039
    _offset = 1
1040

    
1041

    
1042
class LeixenVV898SDualBankRadioB(LeixenVV898SDualBankRadio):
1043
    VARIANT = 'B Band'
1044
    _offset = 2
1045

    
1046

    
1047
@directory.register
1048
class JetstreamJT270MH(LeixenVV898SDualBankRadio):
1049

    
1050
    """Jetstream JT270MH, same ident as dual bank Leixens"""
1051
    VENDOR = "Jetstream"
1052
    MODEL = "JT270MH"
1053
    _file_ident = "JET"
(17-17/21)