Project

General

Profile

Bug #4069 » leixen.py

463f30b7 - Dan Smith, 02/05/2024 06:39 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 logging
18

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

    
26
LOG = logging.getLogger(__name__)
27

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

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

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

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

    
152
struct name {
153
    char name[7];
154
    u8 pad;
155
};
156

    
157
#seekto 0x%(chanstart)x;
158
struct channel default[%(defaults)i];
159
struct channel memory[199];
160

    
161
#seekto 0x%(namestart)x;
162
struct name defaultname[%(defaults)i];
163
struct name name[199];
164
"""
165

    
166

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

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

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

    
226
MODES = ["NFM", "FM"]
227
WTFTONES = tuple(float(x) for x in range(56, 64))
228
TONES = tuple(sorted(WTFTONES + chirp_common.TONES))
229
DTCS_CODES = tuple(sorted((17, 50, 645) + chirp_common.DTCS_CODES))
230
TMODES = ["", "Tone", "DTCS", "DTCS"]
231

    
232

    
233
def _image_ident_from_data(data):
234
    return data[0x168:0x178]
235

    
236

    
237
def _image_ident_from_image(radio):
238
    return _image_ident_from_data(radio.get_mmap())
239

    
240

    
241
def checksum(frame):
242
    x = 0
243
    for b in frame:
244
        x ^= b
245
    return x & 0xFF
246

    
247

    
248
def make_frame(cmd, addr, data=b""):
249
    payload = struct.pack(">H", addr) + data
250
    header = struct.pack(">cB", cmd, len(payload))
251
    frame = header + payload
252
    return frame + bytes([checksum(frame)])
253

    
254

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

    
264

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

    
285

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

    
298

    
299
def do_download(radio):
300
    # Ident should have already been done by the detect_from_serial()
301

    
302
    data = b""
303
    data += b"\xFF" * (0 - len(data))
304
    for addr in range(0, radio._memsize, 0x10):
305
        send(radio, make_frame(b"R", addr, b'\x10'))
306
        _addr, _data = recv(radio)
307
        if _addr != addr:
308
            raise errors.RadioError("Radio sent unexpected address")
309
        data += _data
310

    
311
        status = chirp_common.Status()
312
        status.cur = addr
313
        status.max = radio._memsize
314
        status.msg = "Cloning from radio"
315
        radio.status_fn(status)
316

    
317
    finish(radio)
318

    
319
    return memmap.MemoryMapBytes(data)
320

    
321

    
322
def do_upload(radio):
323
    _ranges = [(0x0d00, 0x2000)]
324

    
325
    image_ident = _image_ident_from_image(radio)
326
    if image_ident.startswith(radio._file_ident) and \
327
       radio._model_ident in image_ident:
328
        _ranges = radio._ranges
329

    
330
    do_ident(radio)
331

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

    
344
            status = chirp_common.Status()
345
            status.cur = addr
346
            status.max = radio._memsize
347
            status.msg = "Cloning to radio"
348
            radio.status_fn(status)
349

    
350
    finish(radio)
351

    
352

    
353
def finish(radio):
354
    send(radio, b"\x64\x01\x6F\x0A")
355
    ack = radio.pipe.read(8)
356

    
357

    
358
@directory.register
359
class LeixenVV898Radio(chirp_common.CloneModeRadio):
360

    
361
    """Leixen VV-898"""
362
    VENDOR = "Leixen"
363
    MODEL = "VV-898"
364
    BAUD_RATE = 9600
365
    NEEDS_COMPAT_SERIAL = False
366

    
367
    _file_ident = b"Leixen"
368
    _model_ident = b'LX-\x89\x85\x63'
369

    
370
    _memsize = 0x2000
371
    _ranges = [
372
        (0x0000, 0x013f),
373
        (0x0148, 0x0167),
374
        (0x0184, 0x018f),
375
        (0x0190, 0x01cf),
376
        (0x0900, 0x090f),
377
        (0x0920, 0x0927),
378
        (0x0d00, 0x2000),
379
    ]
380

    
381
    _mem_formatter = {'unknownormode': 'unknown6:1',
382
                      'modeorpower': 'mode:1, power:1',
383
                      'chanstart': 0x0D00,
384
                      'namestart': 0x19B0,
385
                      'defaults': 3}
386
    _power_levels = [chirp_common.PowerLevel("Low", watts=4),
387
                     chirp_common.PowerLevel("High", watts=10)]
388

    
389
    @classmethod
390
    def detect_from_serial(cls, pipe):
391
        radio = cls(pipe)
392
        do_ident(radio)
393
        send(radio, make_frame(b"R", 0x0168, b'\x10'))
394
        _addr, _data = recv(radio)
395
        ident = _data[8:14]
396
        LOG.debug('Got ident from radio:\n%s' % util.hexprint(ident))
397
        for rclass in [cls] + (cls.DETECTED_MODELS or []):
398
            if ident == rclass._model_ident:
399
                return rclass
400
        # Reset the radio if we didn't find a match
401
        finish(radio)
402
        raise errors.RadioError('Unable to detect a supported model')
403

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

    
435
    def sync_in(self):
436
        try:
437
            self._mmap = do_download(self)
438
        except Exception as e:
439
            finish(self)
440
            raise errors.RadioError("Failed to download from radio: %s" % e)
441
        self.process_mmap()
442

    
443
    def process_mmap(self):
444
        self._memobj = bitwise.parse(
445
            MEM_FORMAT % self._mem_formatter, self._mmap)
446

    
447
    def sync_out(self):
448
        try:
449
            do_upload(self)
450
        except errors.RadioError:
451
            finish(self)
452
            raise
453
        except Exception as e:
454
            raise errors.RadioError("Failed to upload to radio: %s" % e)
455

    
456
    def get_raw_memory(self, number):
457
        name, mem = self._get_memobjs(number)
458
        return repr(name) + repr(mem)
459

    
460
    def _get_tone(self, mem, _mem):
461
        rx_tone = tx_tone = None
462

    
463
        tx_tmode = TMODES[_mem.tx_tmode]
464
        rx_tmode = TMODES[_mem.rx_tmode]
465

    
466
        if tx_tmode == "Tone":
467
            tx_tone = TONES[_mem.tx_tone - 1]
468
        elif tx_tmode == "DTCS":
469
            tx_tone = DTCS_CODES[_mem.tx_tone - 1]
470

    
471
        if rx_tmode == "Tone":
472
            rx_tone = TONES[_mem.rx_tone - 1]
473
        elif rx_tmode == "DTCS":
474
            rx_tone = DTCS_CODES[_mem.rx_tone - 1]
475

    
476
        tx_pol = _mem.tx_tmode == 0x03 and "R" or "N"
477
        rx_pol = _mem.rx_tmode == 0x03 and "R" or "N"
478

    
479
        chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
480
                                       (rx_tmode, rx_tone, rx_pol))
481

    
482
    def _is_txinh(self, _mem):
483
        raw_tx = b""
484
        for i in range(0, 4):
485
            raw_tx += _mem.tx_freq[i].get_raw()
486
        return raw_tx == b"\xFF\xFF\xFF\xFF"
487

    
488
    def _get_memobjs(self, number):
489
        _mem = self._memobj.memory[number - 1]
490
        _name = self._memobj.name[number - 1]
491
        return _mem, _name
492

    
493
    def get_memory(self, number):
494
        _mem, _name = self._get_memobjs(number)
495

    
496
        mem = chirp_common.Memory()
497
        mem.number = number
498

    
499
        if _mem.get_raw()[:4] == b"\xFF\xFF\xFF\xFF":
500
            mem.empty = True
501
            return mem
502

    
503
        mem.freq = int(_mem.rx_freq) * 10
504

    
505
        if self._is_txinh(_mem):
506
            mem.duplex = "off"
507
            mem.offset = 0
508
        elif int(_mem.rx_freq) == int(_mem.tx_freq):
509
            mem.duplex = ""
510
            mem.offset = 0
511
        elif abs(int(_mem.rx_freq) * 10 - int(_mem.tx_freq) * 10) > 70000000:
512
            mem.duplex = "split"
513
            mem.offset = int(_mem.tx_freq) * 10
514
        else:
515
            mem.duplex = int(_mem.rx_freq) > int(_mem.tx_freq) and "-" or "+"
516
            mem.offset = abs(int(_mem.rx_freq) - int(_mem.tx_freq)) * 10
517

    
518
        mem.name = str(_name.name).rstrip()
519

    
520
        self._get_tone(mem, _mem)
521
        mem.mode = MODES[_mem.mode]
522
        powerindex = _mem.power if _mem.power < len(self._power_levels) else -1
523
        mem.power = self._power_levels[powerindex]
524
        mem.skip = _mem.skip and "S" or ""
525

    
526
        mem.extra = RadioSettingGroup("Extra", "extra")
527

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

    
576
        return mem
577

    
578
    def _set_tone(self, mem, _mem):
579
        ((txmode, txtone, txpol),
580
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
581

    
582
        _mem.tx_tmode = TMODES.index(txmode)
583
        _mem.rx_tmode = TMODES.index(rxmode)
584
        if txmode == "Tone":
585
            _mem.tx_tone = TONES.index(txtone) + 1
586
        elif txmode == "DTCS":
587
            _mem.tx_tmode = txpol == "R" and 0x03 or 0x02
588
            _mem.tx_tone = DTCS_CODES.index(txtone) + 1
589
        if rxmode == "Tone":
590
            _mem.rx_tone = TONES.index(rxtone) + 1
591
        elif rxmode == "DTCS":
592
            _mem.rx_tmode = rxpol == "R" and 0x03 or 0x02
593
            _mem.rx_tone = DTCS_CODES.index(rxtone) + 1
594

    
595
    def set_memory(self, mem):
596
        _mem, _name = self._get_memobjs(mem.number)
597

    
598
        if mem.empty:
599
            _mem.set_raw(b"\xFF" * 16)
600
            return
601
        elif _mem.get_raw() == (b"\xFF" * 16):
602
            _mem.set_raw(b"\xFF" * 8 + b"\xFF\x00\xFF\x00\xFF\xFE\xF0\xFC")
603

    
604
        _mem.rx_freq = mem.freq / 10
605

    
606
        if mem.duplex == "off":
607
            for i in range(0, 4):
608
                _mem.tx_freq[i].set_raw(b"\xFF")
609
        elif mem.duplex == "split":
610
            _mem.tx_freq = mem.offset / 10
611
        elif mem.duplex == "+":
612
            _mem.tx_freq = (mem.freq + mem.offset) / 10
613
        elif mem.duplex == "-":
614
            _mem.tx_freq = (mem.freq - mem.offset) / 10
615
        else:
616
            _mem.tx_freq = mem.freq / 10
617

    
618
        self._set_tone(mem, _mem)
619

    
620
        _mem.power = mem.power and self._power_levels.index(mem.power) or 0
621
        _mem.mode = MODES.index(mem.mode)
622
        _mem.skip = mem.skip == "S"
623
        _name.name = mem.name.ljust(7)
624

    
625
        # autoset display to name if filled, else show frequency
626
        if mem.extra:
627
            # mem.extra only seems to be populated when called from edit panel
628
            aliasop = mem.extra["aliasop"]
629
        else:
630
            aliasop = None
631
        if mem.name:
632
            _mem.aliasop = False
633
        else:
634
            _mem.aliasop = True
635

    
636
        for setting in mem.extra:
637
            setattr(_mem, setting.get_name(), setting.value)
638

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

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

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

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

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

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

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

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

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

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

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

    
880
        return group
881

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

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

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

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

    
960

    
961
@directory.register
962
class JetstreamJT270MRadio(LeixenVV898Radio):
963

    
964
    """Jetstream JT270M"""
965
    VENDOR = "Jetstream"
966
    MODEL = "JT270M"
967
    ALIASES = []
968

    
969
    _file_ident = b"JET"
970
    _model_ident = b'LX-\x89\x85\x53'
971

    
972

    
973
class LT898UV(LeixenVV898Radio):
974
    VENDOR = "LUITON"
975
    MODEL = "LT-898UV"
976

    
977
    @classmethod
978
    def match_model(cls, filedata, filename):
979
        return False
980

    
981

    
982
@directory.register
983
class JetstreamJT270MHRadio(LeixenVV898Radio):
984

    
985
    """Jetstream JT270MH"""
986
    VENDOR = "Jetstream"
987
    MODEL = "JT270MH"
988

    
989
    _file_ident = b"Leixen"
990
    _model_ident = b'LX-\x89\x85\x85'
991
    _ranges = [(0x0C00, 0x2000)]
992
    _mem_formatter = {'unknownormode': 'mode:1',
993
                      'modeorpower': 'power:2',
994
                      'chanstart': 0x0C00,
995
                      'namestart': 0x1900,
996
                      'defaults': 6}
997
    _power_levels = [chirp_common.PowerLevel("Low", watts=5),
998
                     chirp_common.PowerLevel("Mid", watts=10),
999
                     chirp_common.PowerLevel("High", watts=25)]
1000
    # Base radio has offset zero to distinguish from sub devices
1001
    _offset = 0
1002

    
1003
    def get_features(self):
1004
        rf = super(JetstreamJT270MHRadio, self).get_features()
1005
        rf.has_sub_devices = self._offset == 0
1006
        rf.memory_bounds = (1, 99)
1007
        return rf
1008

    
1009
    def get_sub_devices(self):
1010
        return [JetstreamJT270MHRadioA(self._mmap),
1011
                JetstreamJT270MHRadioB(self._mmap)]
1012

    
1013
    def _get_memobjs(self, number):
1014
        number = number * 2 - self._offset
1015
        _mem = self._memobj.memory[number]
1016
        _name = self._memobj.name[number]
1017
        return _mem, _name
1018

    
1019

    
1020
class JetstreamJT270MHRadioA(JetstreamJT270MHRadio):
1021
    VARIANT = 'A Band'
1022
    _offset = 1
1023

    
1024

    
1025
class JetstreamJT270MHRadioB(JetstreamJT270MHRadio):
1026
    VARIANT = 'B Band'
1027
    _offset = 2
1028

    
1029

    
1030
@directory.register
1031
class LeixenVV898SRadio(LeixenVV898Radio):
1032

    
1033
    """Leixen VV-898S, also VV-898E which is identical"""
1034
    VENDOR = "Leixen"
1035
    MODEL = "VV-898S"
1036

    
1037
    _model_ident = b'LX-\x89\x85\x75'
1038
    _mem_formatter = {'unknownormode': 'mode:1',
1039
                      'modeorpower': 'power:2',
1040
                      'chanstart': 0x0D00,
1041
                      'namestart': 0x19B0,
1042
                      'defaults': 3}
1043
    _power_levels = [chirp_common.PowerLevel("Low", watts=5),
1044
                     chirp_common.PowerLevel("Med", watts=10),
1045
                     chirp_common.PowerLevel("High", watts=25)]
1046

    
1047

    
1048
@directory.register
1049
class VV898E(LeixenVV898SRadio):
1050
    '''Leixen has called this radio both 898E and S historically, ident is
1051
    identical'''
1052
    VENDOR = "Leixen"
1053
    MODEL = "VV-898E"
1054

    
1055
    @classmethod
1056
    def match_model(cls, filedata, filename):
1057
        return False
1058

    
1059

    
1060
@directory.register
1061
@directory.detected_by(LeixenVV898SRadio)
1062
class VV898SDualBank(JetstreamJT270MHRadio):
1063
    '''Newer VV898S 1.06+ firmware that features dual memory banks'''
1064
    VENDOR = "Leixen"
1065
    MODEL = "VV-898S"
1066
    VARIANT = "Dual Bank"
1067

    
1068
    @classmethod
1069
    def match_model(cls, filedata, filename):
1070
        return False
1071

    
1072

    
1073
@directory.register
1074
@directory.detected_by(VV898E)
1075
class VV898EDualBank(JetstreamJT270MHRadio):
1076
    '''Newer VV898E 1.06+ firmware that features dual memory banks'''
1077
    VENDOR = "Leixen"
1078
    MODEL = "VV-898E"
1079
    VARIANT = "Dual Bank"
1080

    
1081
    @classmethod
1082
    def match_model(cls, filedata, filename):
1083
        return False
(21-21/21)