Project

General

Profile

Bug #10312 » kenwood_live.py

Dan Smith, 01/26/2023 04:21 PM

 
1
# Copyright 2010 Dan Smith <dsmith@danplanet.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 3 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 threading
17
import os
18
import sys
19
import time
20
import logging
21

    
22

    
23

    
24
from chirp import chirp_common, errors, directory, util
25
from chirp.settings import RadioSetting, RadioSettingGroup, \
26
    RadioSettingValueInteger, RadioSettingValueBoolean, \
27
    RadioSettingValueString, RadioSettingValueList, RadioSettings
28

    
29
chirp_common.CHARSET_1252 = bytes(
30
    [x for x in range(0x21, 0x100)
31
     if x not in [0x81, 0x8D, 0x8F, 0x90, 0x9D]]).decode('cp1252')
32

    
33
LOG = logging.getLogger(__name__)
34

    
35
NOCACHE = "CHIRP_NOCACHE" in os.environ
36

    
37
DUPLEX = {0: "", 1: "+", 2: "-"}
38
MODES = {0: "FM", 1: "AM"}
39
STEPS = list(chirp_common.TUNING_STEPS)
40
STEPS.append(100.0)
41

    
42
KENWOOD_TONES = list(chirp_common.TONES)
43
KENWOOD_TONES.remove(159.8)
44
KENWOOD_TONES.remove(165.5)
45
KENWOOD_TONES.remove(171.3)
46
KENWOOD_TONES.remove(177.3)
47
KENWOOD_TONES.remove(183.5)
48
KENWOOD_TONES.remove(189.9)
49
KENWOOD_TONES.remove(196.6)
50
KENWOOD_TONES.remove(199.5)
51

    
52
THF6_MODES = ["FM", "WFM", "AM", "LSB", "USB", "CW"]
53

    
54

    
55
RADIO_IDS = {
56
    "ID019": "TS-2000",
57
    "ID009": "TS-850",
58
    "ID020": "TS-480_LiveMode",
59
    "ID021": "TS-590S/SG_LiveMode",         # S-model uses same class
60
    "ID023": "TS-590S/SG_LiveMode"          # as SG
61
}
62

    
63
LOCK = threading.Lock()
64
COMMAND_RESP_BUFSIZE = 8
65
LAST_BAUD = 4800
66
LAST_DELIMITER = ("\r", " ")
67

    
68
# The Kenwood TS-2000, TS-480, TS-590 & TS-850 use ";"
69
# as a CAT command message delimiter, and all others use "\n".
70
# Also, TS-2000 and TS-590 don't space delimite the command
71
# fields, but others do.
72

    
73

    
74
def _command(ser, cmd, *args):
75
    """Send @cmd to radio via @ser"""
76
    global LOCK, LAST_DELIMITER, COMMAND_RESP_BUFSIZE
77

    
78
    start = time.time()
79

    
80
    # TODO: This global use of LAST_DELIMITER breaks reentrancy
81
    # and needs to be fixed.
82
    if args:
83
        cmd += LAST_DELIMITER[1] + LAST_DELIMITER[1].join(args)
84
    cmd += LAST_DELIMITER[0]
85

    
86
    LOG.debug("PC->RADIO: %s" % cmd.strip())
87
    ser.write(cmd.encode('cp1252'))
88

    
89
    result = ""
90
    while not result.endswith(LAST_DELIMITER[0]):
91
        result += ser.read(COMMAND_RESP_BUFSIZE).decode('cp1252')
92
        if (time.time() - start) > 0.5:
93
            LOG.error("Timeout waiting for data")
94
            break
95

    
96
    if result.endswith(LAST_DELIMITER[0]):
97
        LOG.debug("RADIO->PC: %r" % result.strip())
98
        result = result[:-1]
99
    else:
100
        LOG.error("Giving up")
101

    
102
    return result.strip()
103

    
104

    
105
def command(ser, cmd, *args):
106
    with LOCK:
107
        return _command(ser, cmd, *args)
108

    
109

    
110
def get_id(ser):
111
    """Get the ID of the radio attached to @ser"""
112
    global LAST_BAUD
113
    bauds = [4800, 9600, 19200, 38400, 57600, 115200]
114
    bauds.remove(LAST_BAUD)
115
    # Make sure LAST_BAUD is last so that it is tried first below
116
    bauds.append(LAST_BAUD)
117

    
118
    global LAST_DELIMITER
119
    command_delimiters = [("\r", " "), (";", "")]
120

    
121
    for delimiter in command_delimiters:
122
        # Process the baud options in reverse order so that we try the
123
        # last one first, and then start with the high-speed ones next
124
        for i in reversed(bauds):
125
            LAST_DELIMITER = delimiter
126
            LOG.info("Trying ID at baud %i with delimiter \"%s\"" %
127
                     (i, repr(delimiter)))
128
            ser.baudrate = i
129
            ser.write(LAST_DELIMITER[0].encode())
130
            ser.read(25)
131
            try:
132
                resp = command(ser, "ID")
133
            except UnicodeDecodeError:
134
                # If we got binary here, we are using the wrong rate
135
                # or not talking to a kenwood live radio.
136
                continue
137

    
138
            # most kenwood radios
139
            if " " in resp:
140
                LAST_BAUD = i
141
                return resp.split(" ")[1]
142

    
143
            # Radio responded in the right baud rate,
144
            # but threw an error because of all the crap
145
            # we have been hurling at it. Retry the ID at this
146
            # baud rate, which will almost definitely work.
147
            if "?" in resp:
148
                resp = command(ser, "ID")
149
                LAST_BAUD = i
150
                if " " in resp:
151
                    return resp.split(" ")[1]
152

    
153
            # Kenwood radios that return ID numbers
154
            if resp in list(RADIO_IDS.keys()):
155
                return RADIO_IDS[resp]
156

    
157
    raise errors.RadioError("No response from radio")
158

    
159

    
160
def get_tmode(tone, ctcss, dcs):
161
    """Get the tone mode based on the values of the tone, ctcss, dcs"""
162
    if dcs and int(dcs) == 1:
163
        return "DTCS"
164
    elif int(ctcss):
165
        return "TSQL"
166
    elif int(tone):
167
        return "Tone"
168
    else:
169
        return ""
170

    
171

    
172
def iserr(result):
173
    """Returns True if the @result from a radio is an error"""
174
    return result in ["N", "?"]
175

    
176

    
177
class KenwoodLiveRadio(chirp_common.LiveRadio):
178
    """Base class for all live-mode kenwood radios"""
179
    BAUD_RATE = 9600
180
    VENDOR = "Kenwood"
181
    MODEL = ""
182
    NEEDS_COMPAT_SERIAL = False
183

    
184
    _vfo = 0
185
    _upper = 200
186
    _kenwood_split = False
187
    _kenwood_valid_tones = list(chirp_common.TONES)
188
    _has_name = True
189

    
190
    def __init__(self, *args, **kwargs):
191
        chirp_common.LiveRadio.__init__(self, *args, **kwargs)
192

    
193
        self._memcache = {}
194

    
195
        if self.pipe:
196
            self.pipe.timeout = 0.1
197
            radio_id = get_id(self.pipe)
198
            if radio_id != self.MODEL.split(" ")[0]:
199
                raise Exception("Radio reports %s (not %s)" % (radio_id,
200
                                                               self.MODEL))
201

    
202
            command(self.pipe, "AI", "0")
203

    
204
    def _cmd_get_memory(self, number):
205
        return "MR", "%i,0,%03i" % (self._vfo, number)
206

    
207
    def _cmd_get_memory_name(self, number):
208
        return "MNA", "%i,%03i" % (self._vfo, number)
209

    
210
    def _cmd_get_split(self, number):
211
        return "MR", "%i,1,%03i" % (self._vfo, number)
212

    
213
    def _cmd_set_memory(self, number, spec):
214
        if spec:
215
            spec = "," + spec
216
        return "MW", "%i,0,%03i%s" % (self._vfo, number, spec)
217

    
218
    def _cmd_set_memory_name(self, number, name):
219
        return "MNA", "%i,%03i,%s" % (self._vfo, number, name)
220

    
221
    def _cmd_set_split(self, number, spec):
222
        return "MW", "%i,1,%03i,%s" % (self._vfo, number, spec)
223

    
224
    def get_raw_memory(self, number):
225
        return command(self.pipe, *self._cmd_get_memory(number))
226

    
227
    def get_memory(self, number):
228
        if number < 0 or number > self._upper:
229
            raise errors.InvalidMemoryLocation(
230
                "Number must be between 0 and %i" % self._upper)
231
        if number in self._memcache and not NOCACHE:
232
            return self._memcache[number]
233

    
234
        result = command(self.pipe, *self._cmd_get_memory(number))
235
        if result == "N" or result == "E":
236
            mem = chirp_common.Memory()
237
            mem.number = number
238
            mem.empty = True
239
            self._memcache[mem.number] = mem
240
            return mem
241
        elif " " not in result:
242
            LOG.error("Not sure what to do with this: `%s'" % result)
243
            raise errors.RadioError("Unexpected result returned from radio")
244

    
245
        value = result.split(" ")[1]
246
        spec = value.split(",")
247

    
248
        mem = self._parse_mem_spec(spec)
249
        self._memcache[mem.number] = mem
250

    
251
        if self._has_name:
252
            result = command(self.pipe, *self._cmd_get_memory_name(number))
253
            if " " in result:
254
                value = result.split(" ", 1)[1]
255
                if value.count(",") == 2:
256
                    _zero, _loc, mem.name = value.split(",")
257
                else:
258
                    _loc, mem.name = value.split(",")
259

    
260
        if mem.duplex == "" and self._kenwood_split:
261
            result = command(self.pipe, *self._cmd_get_split(number))
262
            if " " in result:
263
                value = result.split(" ", 1)[1]
264
                self._parse_split_spec(mem, value.split(","))
265

    
266
        return mem
267

    
268
    def _make_mem_spec(self, mem):
269
        pass
270

    
271
    def _parse_mem_spec(self, spec):
272
        pass
273

    
274
    def _parse_split_spec(self, mem, spec):
275
        mem.duplex = "split"
276
        mem.offset = int(spec[2])
277

    
278
    def _make_split_spec(self, mem):
279
        return ("%011i" % mem.offset, "0")
280

    
281
    def set_memory(self, memory):
282
        if memory.number < 0 or memory.number > self._upper:
283
            raise errors.InvalidMemoryLocation(
284
                "Number must be between 0 and %i" % self._upper)
285

    
286
        spec = self._make_mem_spec(memory)
287
        spec = ",".join(spec)
288
        r1 = command(self.pipe, *self._cmd_set_memory(memory.number, spec))
289
        if not iserr(r1) and self._has_name:
290
            time.sleep(0.5)
291
            r2 = command(self.pipe, *self._cmd_set_memory_name(memory.number,
292
                                                               memory.name))
293
            if not iserr(r2):
294
                memory.name = memory.name.rstrip()
295
                self._memcache[memory.number] = memory
296
            else:
297
                raise errors.InvalidDataError("Radio refused name %i: %s" %
298
                                              (memory.number,
299
                                               repr(memory.name)))
300
        elif self._has_name:
301
            raise errors.InvalidDataError("Radio refused %i" % memory.number)
302

    
303
        if memory.duplex == "split" and self._kenwood_split:
304
            spec = ",".join(self._make_split_spec(memory))
305
            result = command(self.pipe, *self._cmd_set_split(memory.number,
306
                                                             spec))
307
            if iserr(result):
308
                raise errors.InvalidDataError("Radio refused %i" %
309
                                              memory.number)
310

    
311
    def erase_memory(self, number):
312
        if number not in self._memcache:
313
            return
314

    
315
        resp = command(self.pipe, *self._cmd_set_memory(number, ""))
316
        if iserr(resp):
317
            raise errors.RadioError("Radio refused delete of %i" % number)
318
        del self._memcache[number]
319

    
320
    def _kenwood_get(self, cmd):
321
        resp = command(self.pipe, cmd)
322
        if " " in resp:
323
            return resp.split(" ", 1)
324
        else:
325
            if resp == cmd:
326
                return [resp, ""]
327
            else:
328
                raise errors.RadioError("Radio refused to return %s" % cmd)
329

    
330
    def _kenwood_set(self, cmd, value):
331
        resp = command(self.pipe, cmd, value)
332
        if resp[:len(cmd)] == cmd:
333
            return
334
        raise errors.RadioError("Radio refused to set %s" % cmd)
335

    
336
    def _kenwood_get_bool(self, cmd):
337
        _cmd, result = self._kenwood_get(cmd)
338
        return result == "1"
339

    
340
    def _kenwood_set_bool(self, cmd, value):
341
        return self._kenwood_set(cmd, str(int(value)))
342

    
343
    def _kenwood_get_int(self, cmd):
344
        _cmd, result = self._kenwood_get(cmd)
345
        return int(result)
346

    
347
    def _kenwood_set_int(self, cmd, value, digits=1):
348
        return self._kenwood_set(cmd, ("%%0%ii" % digits) % value)
349

    
350
    def set_settings(self, settings):
351
        for element in settings:
352
            if not isinstance(element, RadioSetting):
353
                self.set_settings(element)
354
                continue
355
            if not element.changed():
356
                continue
357
            if isinstance(element.value, RadioSettingValueBoolean):
358
                self._kenwood_set_bool(element.get_name(), element.value)
359
            elif isinstance(element.value, RadioSettingValueList):
360
                options = self._get_setting_options(element.get_name())
361
                if len(options) > 9:
362
                    digits = 2
363
                else:
364
                    digits = 1
365
                self._kenwood_set_int(element.get_name(),
366
                                      options.index(str(element.value)),
367
                                      digits)
368
            elif isinstance(element.value, RadioSettingValueInteger):
369
                if element.value.get_max() > 9:
370
                    digits = 2
371
                else:
372
                    digits = 1
373
                self._kenwood_set_int(element.get_name(),
374
                                      element.value, digits)
375
            elif isinstance(element.value, RadioSettingValueString):
376
                self._kenwood_set(element.get_name(), str(element.value))
377
            else:
378
                LOG.error("Unknown type %s" % element.value)
379

    
380

    
381
class KenwoodOldLiveRadio(KenwoodLiveRadio):
382
    _kenwood_valid_tones = list(chirp_common.OLD_TONES)
383

    
384
    def set_memory(self, memory):
385
        supported_tones = list(chirp_common.OLD_TONES)
386
        supported_tones.remove(69.3)
387
        if memory.rtone not in supported_tones:
388
            raise errors.UnsupportedToneError("This radio does not support " +
389
                                              "tone %.1fHz" % memory.rtone)
390
        if memory.ctone not in supported_tones:
391
            raise errors.UnsupportedToneError("This radio does not support " +
392
                                              "tone %.1fHz" % memory.ctone)
393

    
394
        return KenwoodLiveRadio.set_memory(self, memory)
395

    
396

    
397
@directory.register
398
class THD7Radio(KenwoodOldLiveRadio):
399
    """Kenwood TH-D7"""
400
    MODEL = "TH-D7"
401

    
402
    _kenwood_split = True
403

    
404
    _BEP_OPTIONS = ["Off", "Key", "Key+Data", "All"]
405
    _POSC_OPTIONS = ["Off Duty", "Enroute", "In Service", "Returning",
406
                     "Committed", "Special", "Priority", "Emergency"]
407

    
408
    _SETTINGS_OPTIONS = {
409
        "BAL": ["4:0", "3:1", "2:2", "1:3", "0:4"],
410
        "BEP": None,
411
        "BEPT": ["Off", "Mine", "All New"],  # D700 has fourth "All"
412
        "DS": ["Data Band", "Both Bands"],
413
        "DTB": ["A", "B"],
414
        "DTBA": ["A", "B", "A:TX/B:RX"],  # D700 has fourth A:RX/B:TX
415
        "DTX": ["Manual", "PTT", "Auto"],
416
        "ICO": ["Kenwood", "Runner", "House", "Tent", "Boat", "SSTV",
417
                "Plane", "Speedboat", "Car", "Bicycle"],
418
        "MNF": ["Name", "Frequency"],
419
        "PKSA": ["1200", "9600"],
420
        "POSC": None,
421
        "PT": ["100ms", "200ms", "500ms", "750ms",
422
               "1000ms", "1500ms", "2000ms"],
423
        "SCR": ["Time", "Carrier", "Seek"],
424
        "SV": ["Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s",
425
               "2s", "3s", "4s", "5s"],
426
        "TEMP": ["F", "C"],
427
        "TXI": ["30sec", "1min", "2min", "3min", "4min", "5min",
428
                "10min", "20min", "30min"],
429
        "UNIT": ["English", "Metric"],
430
        "WAY": ["Off", "6 digit NMEA", "7 digit NMEA", "8 digit NMEA",
431
                "9 digit NMEA", "6 digit Magellan", "DGPS"],
432
    }
433

    
434
    def get_features(self):
435
        rf = chirp_common.RadioFeatures()
436
        rf.has_settings = True
437
        rf.has_dtcs = False
438
        rf.has_dtcs_polarity = False
439
        rf.has_bank = False
440
        rf.has_mode = True
441
        rf.has_tuning_step = False
442
        rf.can_odd_split = True
443
        rf.valid_duplexes = ["", "-", "+", "split"]
444
        rf.valid_modes = list(MODES.values())
445
        rf.valid_tmodes = ["", "Tone", "TSQL"]
446
        rf.valid_characters = \
447
            chirp_common.CHARSET_ALPHANUMERIC + "/.-+*)('&%$#! ~}|{"
448
        rf.valid_name_length = 7
449
        rf.valid_tuning_steps = STEPS
450
        rf.memory_bounds = (1, self._upper)
451
        return rf
452

    
453
    def _make_mem_spec(self, mem):
454
        if mem.duplex in " -+":
455
            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
456
            offset = mem.offset
457
        else:
458
            duplex = 0
459
            offset = 0
460

    
461
        spec = (
462
            "%011i" % mem.freq,
463
            "%X" % STEPS.index(mem.tuning_step),
464
            "%i" % duplex,
465
            "0",
466
            "%i" % (mem.tmode == "Tone"),
467
            "%i" % (mem.tmode == "TSQL"),
468
            "",  # DCS Flag
469
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
470
            "",  # DCS Code
471
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
472
            "%09i" % offset,
473
            "%i" % util.get_dict_rev(MODES, mem.mode),
474
            "%i" % ((mem.skip == "S") and 1 or 0))
475

    
476
        return spec
477

    
478
    def _parse_mem_spec(self, spec):
479
        mem = chirp_common.Memory()
480

    
481
        mem.number = int(spec[2])
482
        mem.freq = int(spec[3], 10)
483
        mem.tuning_step = STEPS[int(spec[4], 16)]
484
        mem.duplex = DUPLEX[int(spec[5])]
485
        mem.tmode = get_tmode(spec[7], spec[8], spec[9])
486
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
487
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
488
        if spec[11] and spec[11].isdigit():
489
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
490
        else:
491
            LOG.warn("Unknown or invalid DCS: %s" % spec[11])
492
        if spec[13]:
493
            mem.offset = int(spec[13])
494
        else:
495
            mem.offset = 0
496
        mem.mode = MODES[int(spec[14])]
497
        mem.skip = int(spec[15]) and "S" or ""
498

    
499
        return mem
500

    
501
    EXTRA_BOOL_SETTINGS = {
502
        'main': [("LMP", "Lamp")],
503
        'dtmf': [("TXH", "TX Hold")],
504
    }
505
    EXTRA_LIST_SETTINGS = {
506
        'main': [("BAL", "Balance"),
507
                 ("MNF", "Memory Display Mode")],
508
        'save': [("SV", "Battery Save")],
509
    }
510

    
511
    def _get_setting_options(self, setting):
512
        opts = self._SETTINGS_OPTIONS[setting]
513
        if opts is None:
514
            opts = getattr(self, '_%s_OPTIONS' % setting)
515
        return opts
516

    
517
    def get_settings(self):
518
        main = RadioSettingGroup("main", "Main")
519
        aux = RadioSettingGroup("aux", "Aux")
520
        tnc = RadioSettingGroup("tnc", "TNC")
521
        save = RadioSettingGroup("save", "Save")
522
        display = RadioSettingGroup("display", "Display")
523
        dtmf = RadioSettingGroup("dtmf", "DTMF")
524
        radio = RadioSettingGroup("radio", "Radio",
525
                                  aux, tnc, save, display, dtmf)
526
        sky = RadioSettingGroup("sky", "SkyCommand")
527
        aprs = RadioSettingGroup("aprs", "APRS")
528

    
529
        top = RadioSettings(main, radio, aprs, sky)
530

    
531
        bools = [("AMR", aprs, "APRS Message Auto-Reply"),
532
                 ("AIP", aux, "Advanced Intercept Point"),
533
                 ("ARO", aux, "Automatic Repeater Offset"),
534
                 ("BCN", aprs, "Beacon"),
535
                 ("CH", radio, "Channel Mode Display"),
536
                 # ("DIG", aprs, "APRS Digipeater"),
537
                 ("DL", main, "Dual"),
538
                 ("LK", main, "Lock"),
539
                 ("TSP", dtmf, "DTMF Fast Transmission"),
540
                 ]
541

    
542
        for setting, group, name in bools:
543
            value = self._kenwood_get_bool(setting)
544
            rs = RadioSetting(setting, name,
545
                              RadioSettingValueBoolean(value))
546
            group.append(rs)
547

    
548
        lists = [("BEP", aux, "Beep"),
549
                 ("BEPT", aprs, "APRS Beep"),
550
                 ("DS", tnc, "Data Sense"),
551
                 ("DTB", tnc, "Data Band"),
552
                 ("DTBA", aprs, "APRS Data Band"),
553
                 ("DTX", aprs, "APRS Data TX"),
554
                 # ("ICO", aprs, "APRS Icon"),
555
                 ("PKSA", aprs, "APRS Packet Speed"),
556
                 ("POSC", aprs, "APRS Position Comment"),
557
                 ("PT", dtmf, "DTMF Speed"),
558
                 ("TEMP", aprs, "APRS Temperature Units"),
559
                 ("TXI", aprs, "APRS Transmit Interval"),
560
                 # ("UNIT", aprs, "APRS Display Units"),
561
                 ("WAY", aprs, "Waypoint Mode"),
562
                 ]
563

    
564
        for setting, group, name in lists:
565
            value = self._kenwood_get_int(setting)
566
            options = self._get_setting_options(setting)
567
            rs = RadioSetting(setting, name,
568
                              RadioSettingValueList(options,
569
                                                    options[value]))
570
            group.append(rs)
571

    
572
        for group_name, settings in self.EXTRA_BOOL_SETTINGS.items():
573
            group = locals()[group_name]
574
            for setting, name in settings:
575
                value = self._kenwood_get_bool(setting)
576
                rs = RadioSetting(setting, name,
577
                                  RadioSettingValueBoolean(value))
578
                group.append(rs)
579

    
580
        for group_name, settings in self.EXTRA_LIST_SETTINGS.items():
581
            group = locals()[group_name]
582
            for setting, name in settings:
583
                value = self._kenwood_get_int(setting)
584
                options = self._get_setting_options(setting)
585
                rs = RadioSetting(setting, name,
586
                                  RadioSettingValueBoolean(value))
587
                group.append(rs)
588

    
589
        ints = [("CNT", display, "Contrast", 1, 16),
590
                ]
591
        for setting, group, name, minv, maxv in ints:
592
            value = self._kenwood_get_int(setting)
593
            rs = RadioSetting(setting, name,
594
                              RadioSettingValueInteger(minv, maxv, value))
595
            group.append(rs)
596

    
597
        strings = [("MES", display, "Power-on Message", 8),
598
                   ("MYC", aprs, "APRS Callsign", 8),
599
                   ("PP", aprs, "APRS Path", 32),
600
                   ("SCC", sky, "SkyCommand Callsign", 8),
601
                   ("SCT", sky, "SkyCommand To Callsign", 8),
602
                   # ("STAT", aprs, "APRS Status Text", 32),
603
                   ]
604
        for setting, group, name, length in strings:
605
            _cmd, value = self._kenwood_get(setting)
606
            rs = RadioSetting(setting, name,
607
                              RadioSettingValueString(0, length, value))
608
            group.append(rs)
609

    
610
        return top
611

    
612

    
613
@directory.register
614
class THD7GRadio(THD7Radio):
615
    """Kenwood TH-D7G"""
616
    MODEL = "TH-D7G"
617

    
618
    def get_features(self):
619
        rf = super(THD7GRadio, self).get_features()
620
        rf.valid_name_length = 8
621
        return rf
622

    
623

    
624
@directory.register
625
class TMD700Radio(THD7Radio):
626
    """Kenwood TH-D700"""
627
    MODEL = "TM-D700"
628

    
629
    _kenwood_split = True
630

    
631
    _BEP_OPTIONS = ["Off", "Key"]
632
    _POSC_OPTIONS = ["Off Duty", "Enroute", "In Service", "Returning",
633
                     "Committed", "Special", "Priority", "CUSTOM 0",
634
                     "CUSTOM 1", "CUSTOM 2", "CUSTOM 4", "CUSTOM 5",
635
                     "CUSTOM 6", "Emergency"]
636
    EXTRA_BOOL_SETTINGS = {}
637
    EXTRA_LIST_SETTINGS = {}
638

    
639
    def get_features(self):
640
        rf = chirp_common.RadioFeatures()
641
        rf.has_settings = True
642
        rf.has_dtcs = True
643
        rf.has_dtcs_polarity = False
644
        rf.has_bank = False
645
        rf.has_mode = True
646
        rf.has_tuning_step = False
647
        rf.can_odd_split = True
648
        rf.valid_duplexes = ["", "-", "+", "split"]
649
        rf.valid_modes = ["FM", "AM"]
650
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
651
        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
652
        rf.valid_name_length = 8
653
        rf.valid_tuning_steps = STEPS
654
        rf.memory_bounds = (1, self._upper)
655
        return rf
656

    
657
    def _make_mem_spec(self, mem):
658
        if mem.duplex in " -+":
659
            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
660
        else:
661
            duplex = 0
662
        spec = (
663
            "%011i" % mem.freq,
664
            "%X" % STEPS.index(mem.tuning_step),
665
            "%i" % duplex,
666
            "0",
667
            "%i" % (mem.tmode == "Tone"),
668
            "%i" % (mem.tmode == "TSQL"),
669
            "%i" % (mem.tmode == "DTCS"),
670
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
671
            "%03i0" % (chirp_common.DTCS_CODES.index(mem.dtcs) + 1),
672
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
673
            "%09i" % mem.offset,
674
            "%i" % util.get_dict_rev(MODES, mem.mode),
675
            "%i" % ((mem.skip == "S") and 1 or 0))
676

    
677
        return spec
678

    
679
    def _parse_mem_spec(self, spec):
680
        mem = chirp_common.Memory()
681

    
682
        mem.number = int(spec[2])
683
        mem.freq = int(spec[3])
684
        mem.tuning_step = STEPS[int(spec[4], 16)]
685
        mem.duplex = DUPLEX[int(spec[5])]
686
        mem.tmode = get_tmode(spec[7], spec[8], spec[9])
687
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
688
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
689
        if spec[11] and spec[11].isdigit():
690
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
691
        else:
692
            LOG.warn("Unknown or invalid DCS: %s" % spec[11])
693
        if spec[13]:
694
            mem.offset = int(spec[13])
695
        else:
696
            mem.offset = 0
697
        mem.mode = MODES[int(spec[14])]
698
        mem.skip = int(spec[15]) and "S" or ""
699

    
700
        return mem
701

    
702

    
703
@directory.register
704
class TMV7Radio(KenwoodOldLiveRadio):
705
    """Kenwood TM-V7"""
706
    MODEL = "TM-V7"
707

    
708
    mem_upper_limit = 200  # Will be updated
709

    
710
    def get_features(self):
711
        rf = chirp_common.RadioFeatures()
712
        rf.has_dtcs = False
713
        rf.has_dtcs_polarity = False
714
        rf.has_bank = False
715
        rf.has_mode = False
716
        rf.has_tuning_step = False
717
        rf.valid_modes = ["FM"]
718
        rf.valid_tmodes = ["", "Tone", "TSQL"]
719
        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
720
        rf.valid_name_length = 7
721
        rf.valid_tuning_steps = STEPS
722
        rf.has_sub_devices = True
723
        rf.memory_bounds = (1, self._upper)
724
        return rf
725

    
726
    def _make_mem_spec(self, mem):
727
        spec = (
728
            "%011i" % mem.freq,
729
            "%X" % STEPS.index(mem.tuning_step),
730
            "%i" % util.get_dict_rev(DUPLEX, mem.duplex),
731
            "0",
732
            "%i" % (mem.tmode == "Tone"),
733
            "%i" % (mem.tmode == "TSQL"),
734
            "0",
735
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
736
            "000",
737
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
738
            "",
739
            "0")
740

    
741
        return spec
742

    
743
    def _parse_mem_spec(self, spec):
744
        mem = chirp_common.Memory()
745
        mem.number = int(spec[2])
746
        mem.freq = int(spec[3])
747
        mem.tuning_step = STEPS[int(spec[4], 16)]
748
        mem.duplex = DUPLEX[int(spec[5])]
749
        if int(spec[7]):
750
            mem.tmode = "Tone"
751
        elif int(spec[8]):
752
            mem.tmode = "TSQL"
753
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
754
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
755

    
756
        return mem
757

    
758
    def get_sub_devices(self):
759
        return [TMV7RadioVHF(self.pipe), TMV7RadioUHF(self.pipe)]
760

    
761
    def __test_location(self, loc):
762
        mem = self.get_memory(loc)
763
        if not mem.empty:
764
            # Memory was not empty, must be valid
765
            return True
766

    
767
        # Mem was empty (or invalid), try to set it
768
        if self._vfo == 0:
769
            mem.freq = 144000000
770
        else:
771
            mem.freq = 440000000
772
        mem.empty = False
773
        try:
774
            self.set_memory(mem)
775
        except Exception:
776
            # Failed, so we're past the limit
777
            return False
778

    
779
        # Erase what we did
780
        try:
781
            self.erase_memory(loc)
782
        except Exception:
783
            pass  # V7A Can't delete just yet
784

    
785
        return True
786

    
787
    def _detect_split(self):
788
        return 50
789

    
790

    
791
class TMV7RadioSub(TMV7Radio):
792
    """Base class for the TM-V7 sub devices"""
793
    def __init__(self, pipe):
794
        TMV7Radio.__init__(self, pipe)
795
        self._detect_split()
796

    
797

    
798
class TMV7RadioVHF(TMV7RadioSub):
799
    """TM-V7 VHF subdevice"""
800
    VARIANT = "VHF"
801
    _vfo = 0
802

    
803

    
804
class TMV7RadioUHF(TMV7RadioSub):
805
    """TM-V7 UHF subdevice"""
806
    VARIANT = "UHF"
807
    _vfo = 1
808

    
809

    
810
@directory.register
811
class TMG707Radio(TMV7Radio):
812
    """Kenwood TM-G707"""
813
    MODEL = "TM-G707"
814

    
815
    def get_features(self):
816
        rf = TMV7Radio.get_features(self)
817
        rf.has_sub_devices = False
818
        rf.memory_bounds = (1, 180)
819
        rf.valid_bands = [(118000000, 174000000),
820
                          (300000000, 520000000),
821
                          (800000000, 999000000)]
822
        return rf
823

    
824

    
825
THG71_STEPS = [5, 6.25, 10, 12.5, 15, 20, 25, 30, 50, 100]
826

    
827

    
828
@directory.register
829
class THG71Radio(TMV7Radio):
830
    """Kenwood TH-G71"""
831
    MODEL = "TH-G71"
832

    
833
    def get_features(self):
834
        rf = TMV7Radio.get_features(self)
835
        rf.has_tuning_step = True
836
        rf.valid_tuning_steps = list(THG71_STEPS)
837
        rf.valid_name_length = 6
838
        rf.has_sub_devices = False
839
        rf.valid_bands = [(118000000, 174000000),
840
                          (320000000, 470000000),
841
                          (800000000, 945000000)]
842
        return rf
843

    
844
    def _make_mem_spec(self, mem):
845
        spec = (
846
            "%011i" % mem.freq,
847
            "%X" % THG71_STEPS.index(mem.tuning_step),
848
            "%i" % util.get_dict_rev(DUPLEX, mem.duplex),
849
            "0",
850
            "%i" % (mem.tmode == "Tone"),
851
            "%i" % (mem.tmode == "TSQL"),
852
            "0",
853
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
854
            "000",
855
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
856
            "%09i" % mem.offset,
857
            "%i" % ((mem.skip == "S") and 1 or 0))
858
        return spec
859

    
860
    def _parse_mem_spec(self, spec):
861
        mem = chirp_common.Memory()
862
        mem.number = int(spec[2])
863
        mem.freq = int(spec[3])
864
        mem.tuning_step = THG71_STEPS[int(spec[4], 16)]
865
        mem.duplex = DUPLEX[int(spec[5])]
866
        if int(spec[7]):
867
            mem.tmode = "Tone"
868
        elif int(spec[8]):
869
            mem.tmode = "TSQL"
870
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
871
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
872
        if spec[13]:
873
            mem.offset = int(spec[13])
874
        else:
875
            mem.offset = 0
876
        return mem
877

    
878

    
879
THF6A_STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
880
               100.0]
881

    
882
THF6A_DUPLEX = dict(DUPLEX)
883
THF6A_DUPLEX[3] = "split"
884

    
885

    
886
@directory.register
887
class THF6ARadio(KenwoodLiveRadio):
888
    """Kenwood TH-F6"""
889
    MODEL = "TH-F6"
890

    
891
    _charset = chirp_common.CHARSET_ASCII
892
    _upper = 399
893
    _kenwood_split = True
894
    _kenwood_valid_tones = list(KENWOOD_TONES)
895

    
896
    def get_features(self):
897
        rf = chirp_common.RadioFeatures()
898
        rf.has_dtcs_polarity = False
899
        rf.has_bank = False
900
        rf.can_odd_split = True
901
        rf.valid_modes = list(THF6_MODES)
902
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
903
        rf.valid_tuning_steps = list(THF6A_STEPS)
904
        rf.valid_bands = [(1000, 1300000000)]
905
        rf.valid_skips = ["", "S"]
906
        rf.valid_duplexes = list(THF6A_DUPLEX.values())
907
        rf.valid_characters = self._charset
908
        rf.valid_name_length = 8
909
        rf.memory_bounds = (0, self._upper)
910
        rf.has_settings = True
911
        return rf
912

    
913
    def _cmd_set_memory(self, number, spec):
914
        if spec:
915
            spec = "," + spec
916
        return "MW", "0,%03i%s" % (number, spec)
917

    
918
    def _cmd_get_memory(self, number):
919
        return "MR", "0,%03i" % number
920

    
921
    def _cmd_get_memory_name(self, number):
922
        return "MNA", "%03i" % number
923

    
924
    def _cmd_set_memory_name(self, number, name):
925
        return "MNA", "%03i,%s" % (number, name)
926

    
927
    def _cmd_get_split(self, number):
928
        return "MR", "1,%03i" % number
929

    
930
    def _cmd_set_split(self, number, spec):
931
        return "MW", "1,%03i,%s" % (number, spec)
932

    
933
    def _parse_mem_spec(self, spec):
934
        mem = chirp_common.Memory()
935

    
936
        mem.number = int(spec[1])
937
        mem.freq = int(spec[2])
938
        mem.tuning_step = THF6A_STEPS[int(spec[3], 16)]
939
        mem.duplex = THF6A_DUPLEX[int(spec[4])]
940
        mem.tmode = get_tmode(spec[6], spec[7], spec[8])
941
        mem.rtone = self._kenwood_valid_tones[int(spec[9])]
942
        mem.ctone = self._kenwood_valid_tones[int(spec[10])]
943
        if spec[11] and spec[11].isdigit():
944
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
945
        else:
946
            LOG.warn("Unknown or invalid DCS: %s" % spec[11])
947
        if spec[12]:
948
            mem.offset = int(spec[12])
949
        else:
950
            mem.offset = 0
951
        mem.mode = THF6_MODES[int(spec[13])]
952
        if spec[14] == "1":
953
            mem.skip = "S"
954

    
955
        return mem
956

    
957
    def _make_mem_spec(self, mem):
958
        if mem.duplex in " +-":
959
            duplex = util.get_dict_rev(THF6A_DUPLEX, mem.duplex)
960
            offset = mem.offset
961
        elif mem.duplex == "split":
962
            duplex = 0
963
            offset = 0
964
        else:
965
            LOG.warn("Bug: unsupported duplex `%s'" % mem.duplex)
966
        spec = (
967
            "%011i" % mem.freq,
968
            "%X" % THF6A_STEPS.index(mem.tuning_step),
969
            "%i" % duplex,
970
            "0",
971
            "%i" % (mem.tmode == "Tone"),
972
            "%i" % (mem.tmode == "TSQL"),
973
            "%i" % (mem.tmode == "DTCS"),
974
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
975
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
976
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
977
            "%09i" % offset,
978
            "%i" % (THF6_MODES.index(mem.mode)),
979
            "%i" % (mem.skip == "S"))
980

    
981
        return spec
982

    
983
    _SETTINGS_OPTIONS = {
984
        "APO": ["Off", "30min", "60min"],
985
        "BAL": ["100%:0%", "75%:25%", "50%:50%", "25%:75%", "%0:100%"],
986
        "BAT": ["Lithium", "Alkaline"],
987
        "CKEY": ["Call", "1750Hz"],
988
        "DATP": ["1200bps", "9600bps"],
989
        "LAN": ["English", "Japanese"],
990
        "MNF": ["Name", "Frequency"],
991
        "MRM": ["All Band", "Current Band"],
992
        "PT": ["100ms", "250ms", "500ms", "750ms",
993
               "1000ms", "1500ms", "2000ms"],
994
        "SCR": ["Time", "Carrier", "Seek"],
995
        "SV": ["Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s",
996
               "2s", "3s", "4s", "5s"],
997
        "VXD": ["250ms", "500ms", "750ms", "1s", "1.5s", "2s", "3s"],
998
    }
999

    
1000
    def get_settings(self):
1001
        main = RadioSettingGroup("main", "Main")
1002
        aux = RadioSettingGroup("aux", "Aux")
1003
        save = RadioSettingGroup("save", "Save")
1004
        display = RadioSettingGroup("display", "Display")
1005
        dtmf = RadioSettingGroup("dtmf", "DTMF")
1006
        top = RadioSettings(main, aux, save, display, dtmf)
1007

    
1008
        lists = [("APO", save, "Automatic Power Off"),
1009
                 ("BAL", main, "Balance"),
1010
                 ("BAT", save, "Battery Type"),
1011
                 ("CKEY", aux, "CALL Key Set Up"),
1012
                 ("DATP", aux, "Data Packet Speed"),
1013
                 ("LAN", display, "Language"),
1014
                 ("MNF", main, "Memory Display Mode"),
1015
                 ("MRM", main, "Memory Recall Method"),
1016
                 ("PT", dtmf, "DTMF Speed"),
1017
                 ("SCR", main, "Scan Resume"),
1018
                 ("SV", save, "Battery Save"),
1019
                 ("VXD", aux, "VOX Drop Delay"),
1020
                 ]
1021

    
1022
        bools = [("ANT", aux, "Bar Antenna"),
1023
                 ("ATT", main, "Attenuator Enabled"),
1024
                 ("ARO", main, "Automatic Repeater Offset"),
1025
                 ("BEP", aux, "Beep for keypad"),
1026
                 ("DL", main, "Dual"),
1027
                 ("DLK", dtmf, "DTMF Lockout On Transmit"),
1028
                 ("ELK", aux, "Enable Locked Tuning"),
1029
                 ("LK", main, "Lock"),
1030
                 ("LMP", display, "Lamp"),
1031
                 ("NSFT", aux, "Noise Shift"),
1032
                 ("TH", aux, "Tx Hold for 1750"),
1033
                 ("TSP", dtmf, "DTMF Fast Transmission"),
1034
                 ("TXH", dtmf, "TX Hold DTMF"),
1035
                 ("TXS", main, "Transmit Inhibit"),
1036
                 ("VOX", aux, "VOX Enable"),
1037
                 ("VXB", aux, "VOX On Busy"),
1038
                 ]
1039

    
1040
        ints = [("CNT", display, "Contrast", 1, 16),
1041
                ("VXG", aux, "VOX Gain", 0, 9),
1042
                ]
1043

    
1044
        strings = [("MES", display, "Power-on Message", 8),
1045
                   ]
1046

    
1047
        for setting, group, name in bools:
1048
            value = self._kenwood_get_bool(setting)
1049
            rs = RadioSetting(setting, name,
1050
                              RadioSettingValueBoolean(value))
1051
            group.append(rs)
1052

    
1053
        for setting, group, name in lists:
1054
            value = self._kenwood_get_int(setting)
1055
            options = self._SETTINGS_OPTIONS[setting]
1056
            rs = RadioSetting(setting, name,
1057
                              RadioSettingValueList(options,
1058
                                                    options[value]))
1059
            group.append(rs)
1060

    
1061
        for setting, group, name, minv, maxv in ints:
1062
            value = self._kenwood_get_int(setting)
1063
            rs = RadioSetting(setting, name,
1064
                              RadioSettingValueInteger(minv, maxv, value))
1065
            group.append(rs)
1066

    
1067
        for setting, group, name, length in strings:
1068
            _cmd, value = self._kenwood_get(setting)
1069
            rs = RadioSetting(setting, name,
1070
                              RadioSettingValueString(0, length, value))
1071
            group.append(rs)
1072

    
1073
        return top
1074

    
1075

    
1076
@directory.register
1077
class THF7ERadio(THF6ARadio):
1078
    """Kenwood TH-F7"""
1079
    MODEL = "TH-F7"
1080
    _charset = chirp_common.CHARSET_1252
1081

    
1082

    
1083
D710_DUPLEX = ["", "+", "-", "split"]
1084
D710_MODES = ["FM", "NFM", "AM"]
1085
D710_SKIP = ["", "S"]
1086
D710_STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
1087

    
1088

    
1089
@directory.register
1090
class TMD710Radio(KenwoodLiveRadio):
1091
    """Kenwood TM-D710"""
1092
    MODEL = "TM-D710"
1093

    
1094
    _upper = 999
1095
    _kenwood_valid_tones = list(KENWOOD_TONES)
1096

    
1097
    def get_features(self):
1098
        rf = chirp_common.RadioFeatures()
1099
        rf.can_odd_split = True
1100
        rf.has_dtcs_polarity = False
1101
        rf.has_bank = False
1102
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1103
        rf.valid_modes = D710_MODES
1104
        rf.valid_duplexes = D710_DUPLEX
1105
        rf.valid_tuning_steps = D710_STEPS
1106
        rf.valid_characters = chirp_common.CHARSET_ASCII.replace(',', '')
1107
        rf.valid_name_length = 8
1108
        rf.valid_skips = D710_SKIP
1109
        rf.memory_bounds = (0, 999)
1110
        return rf
1111

    
1112
    def _cmd_get_memory(self, number):
1113
        return "ME", "%03i" % number
1114

    
1115
    def _cmd_get_memory_name(self, number):
1116
        return "MN", "%03i" % number
1117

    
1118
    def _cmd_set_memory(self, number, spec):
1119
        return "ME", "%03i,%s" % (number, spec)
1120

    
1121
    def _cmd_set_memory_name(self, number, name):
1122
        return "MN", "%03i,%s" % (number, name)
1123

    
1124
    def _parse_mem_spec(self, spec):
1125
        mem = chirp_common.Memory()
1126

    
1127
        mem.number = int(spec[0])
1128
        mem.freq = int(spec[1])
1129
        mem.tuning_step = D710_STEPS[int(spec[2], 16)]
1130
        mem.duplex = D710_DUPLEX[int(spec[3])]
1131
        # Reverse
1132
        if int(spec[5]):
1133
            mem.tmode = "Tone"
1134
        elif int(spec[6]):
1135
            mem.tmode = "TSQL"
1136
        elif int(spec[7]):
1137
            mem.tmode = "DTCS"
1138
        mem.rtone = self._kenwood_valid_tones[int(spec[8])]
1139
        mem.ctone = self._kenwood_valid_tones[int(spec[9])]
1140
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
1141
        mem.offset = int(spec[11])
1142
        mem.mode = D710_MODES[int(spec[12])]
1143
        # TX Frequency
1144
        if int(spec[13]):
1145
            mem.duplex = "split"
1146
            mem.offset = int(spec[13])
1147
        # Unknown
1148
        mem.skip = D710_SKIP[int(spec[15])]  # Memory Lockout
1149

    
1150
        return mem
1151

    
1152
    def _make_mem_spec(self, mem):
1153
        spec = (
1154
            "%010i" % mem.freq,
1155
            "%X" % D710_STEPS.index(mem.tuning_step),
1156
            "%i" % (0 if mem.duplex == "split"
1157
                    else D710_DUPLEX.index(mem.duplex)),
1158
            "0",  # Reverse
1159
            "%i" % (mem.tmode == "Tone" and 1 or 0),
1160
            "%i" % (mem.tmode == "TSQL" and 1 or 0),
1161
            "%i" % (mem.tmode == "DTCS" and 1 or 0),
1162
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
1163
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
1164
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
1165
            "%08i" % (0 if mem.duplex == "split" else mem.offset),  # Offset
1166
            "%i" % D710_MODES.index(mem.mode),
1167
            "%010i" % (mem.offset if mem.duplex == "split" else 0),  # TX Freq
1168
            "0",  # Unknown
1169
            "%i" % D710_SKIP.index(mem.skip),  # Memory Lockout
1170
            )
1171

    
1172
        return spec
1173

    
1174

    
1175
@directory.register
1176
class THD72Radio(TMD710Radio):
1177
    """Kenwood TH-D72"""
1178
    MODEL = "TH-D72 (live mode)"
1179
    HARDWARE_FLOW = sys.platform == "darwin"  # only OS X driver needs hw flow
1180

    
1181
    def _parse_mem_spec(self, spec):
1182
        mem = chirp_common.Memory()
1183

    
1184
        mem.number = int(spec[0])
1185
        mem.freq = int(spec[1])
1186
        mem.tuning_step = D710_STEPS[int(spec[2], 16)]
1187
        mem.duplex = D710_DUPLEX[int(spec[3])]
1188
        # Reverse
1189
        if int(spec[5]):
1190
            mem.tmode = "Tone"
1191
        elif int(spec[6]):
1192
            mem.tmode = "TSQL"
1193
        elif int(spec[7]):
1194
            mem.tmode = "DTCS"
1195
        mem.rtone = self._kenwood_valid_tones[int(spec[9])]
1196
        mem.ctone = self._kenwood_valid_tones[int(spec[10])]
1197
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
1198
        mem.offset = int(spec[13])
1199
        mem.mode = D710_MODES[int(spec[14])]
1200
        # TX Frequency
1201
        if int(spec[15]):
1202
            mem.duplex = "split"
1203
            mem.offset = int(spec[15])
1204
        # Lockout
1205
        mem.skip = D710_SKIP[int(spec[17])]  # Memory Lockout
1206

    
1207
        return mem
1208

    
1209
    def _make_mem_spec(self, mem):
1210
        spec = (
1211
            "%010i" % mem.freq,
1212
            "%X" % D710_STEPS.index(mem.tuning_step),
1213
            "%i" % (0 if mem.duplex == "split"
1214
                    else D710_DUPLEX.index(mem.duplex)),
1215
            "0",  # Reverse
1216
            "%i" % (mem.tmode == "Tone" and 1 or 0),
1217
            "%i" % (mem.tmode == "TSQL" and 1 or 0),
1218
            "%i" % (mem.tmode == "DTCS" and 1 or 0),
1219
            "0",
1220
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
1221
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
1222
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
1223
            "0",
1224
            "%08i" % (0 if mem.duplex == "split" else mem.offset),  # Offset
1225
            "%i" % D710_MODES.index(mem.mode),
1226
            "%010i" % (mem.offset if mem.duplex == "split" else 0),  # TX Freq
1227
            "0",  # Unknown
1228
            "%i" % D710_SKIP.index(mem.skip),  # Memory Lockout
1229
            )
1230

    
1231
        return spec
1232

    
1233

    
1234
@directory.register
1235
class THD74Radio(TMD710Radio):
1236
    """Kenwood TH_D74"""
1237
    MODEL = "TH-D74 (live mode)"
1238
    HARDWARE_FLOW = sys.platform == "darwin"
1239

    
1240
    STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0,
1241
             25.0, 30.0, 50.0, 100.0]
1242
    MODES = ['FM', 'DV', 'AM', 'LSB', 'USB', 'CW', 'NFM', 'DR',
1243
             'WFM', 'R-CW']
1244
    CROSS_MODES = ['DTCS->', 'Tone->DTCS', 'DTCS->Tone', 'Tone->Tone']
1245
    DUPLEX = ['', '+', '-', 'split']
1246
    _has_name = False
1247

    
1248
    @classmethod
1249
    def get_prompts(cls):
1250
        rp = chirp_common.RadioPrompts()
1251
        rp.experimental = ("This driver is incomplete as the D74 lacks "
1252
                           "the full serial command set of older radios. "
1253
                           "As such, this should be considered permanently "
1254
                           "experimental.")
1255
        return rp
1256

    
1257
    def _cmd_get_memory_name(self, number):
1258
        return ''
1259

    
1260
    def get_features(self):
1261
        rf = super(THD74Radio, self).get_features()
1262
        rf.valid_tuning_steps = self.STEPS
1263
        rf.valid_modes = self.MODES
1264
        rf.valid_cross_modes = self.CROSS_MODES
1265
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
1266
        rf.has_name = False  # Radio has it, but no command to retrieve
1267
        rf.has_cross = True
1268
        return rf
1269

    
1270
    def _parse_mem_spec(self, spec):
1271
        mem = chirp_common.Memory()
1272
        mem.number = int(spec[0])
1273
        mem.freq = int(spec[1])
1274
        mem.offset = int(spec[2])
1275
        mem.tuning_step = self.STEPS[int(spec[3])]
1276
        mem.mode = self.MODES[int(spec[5])]
1277
        if int(spec[11]):
1278
            mem.tmode = "Cross"
1279
            mem.cross_mode = self.CROSS_MODES[int(spec[18])]
1280
        elif int(spec[8]):
1281
            mem.tmode = "Tone"
1282
        elif int(spec[9]):
1283
            mem.tmode = "TSQL"
1284
        elif int(spec[10]):
1285
            mem.tmode = "DTCS"
1286

    
1287
        mem.duplex = self.DUPLEX[int(spec[14])]
1288
        mem.rtone = chirp_common.TONES[int(spec[15])]
1289
        mem.ctone = chirp_common.TONES[int(spec[16])]
1290
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[17])]
1291
        mem.skip = int(spec[22]) and 'S' or ''
1292

    
1293
        return mem
1294

    
1295
    def _make_mem_spec(self, mem):
1296
        spec = (
1297
            "%010i" % mem.freq,
1298
            "%010i" % mem.offset,
1299
            "%X" % self.STEPS.index(mem.tuning_step),
1300
            "%X" % self.STEPS.index(mem.tuning_step),
1301
            "%i" % self.MODES.index(mem.mode),
1302
            "0",  # Fine mode
1303
            "0",  # Fine step
1304
            "%i" % (mem.tmode == "Tone" and 1 or 0),
1305
            "%i" % (mem.tmode == "TSQL" and 1 or 0),
1306
            "%i" % (mem.tmode == "DTCS" and 1 or 0),
1307
            "%i" % (mem.tmode == 'Cross'),
1308
            "0",  # Reverse
1309
            "0",  # Odd split channel
1310
            "%i" % self.DUPLEX.index(mem.duplex),
1311
            "%02i" % (chirp_common.TONES.index(mem.rtone)),
1312
            "%02i" % (chirp_common.TONES.index(mem.ctone)),
1313
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
1314
            "%i" % self.CROSS_MODES.index(mem.cross_mode),
1315
            "CQCQCQ",  # URCALL
1316
            "0",   # D-STAR squelch type
1317
            "00",  # D-STAR squelch code
1318
            "%i" % D710_SKIP.index(mem.skip),  # Memory Lockout
1319
            )
1320

    
1321
        return spec
1322

    
1323

    
1324
@directory.register
1325
class TMV71Radio(TMD710Radio):
1326
    """Kenwood TM-V71"""
1327
    MODEL = "TM-V71"
1328

    
1329

    
1330
@directory.register
1331
class TMD710GRadio(TMD710Radio):
1332
    """Kenwood TM-D710G"""
1333
    MODEL = "TM-D710G"
1334

    
1335
    @classmethod
1336
    def get_prompts(cls):
1337
        rp = chirp_common.RadioPrompts()
1338
        rp.experimental = ("This radio driver is currently under development, "
1339
                           "and supports the same features as the TM-D710A/E. "
1340
                           "There are no known issues with it, but you should "
1341
                           "proceed with caution.")
1342
        return rp
1343

    
1344

    
1345
THK2_DUPLEX = ["", "+", "-"]
1346
THK2_MODES = ["FM", "NFM"]
1347

    
1348
THK2_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/"
1349

    
1350

    
1351
@directory.register
1352
class THK2Radio(KenwoodLiveRadio):
1353
    """Kenwood TH-K2"""
1354
    MODEL = "TH-K2"
1355

    
1356
    _kenwood_valid_tones = list(KENWOOD_TONES)
1357

    
1358
    def get_features(self):
1359
        rf = chirp_common.RadioFeatures()
1360
        rf.can_odd_split = False
1361
        rf.has_dtcs_polarity = False
1362
        rf.has_bank = False
1363
        rf.has_tuning_step = False
1364
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1365
        rf.valid_modes = THK2_MODES
1366
        rf.valid_duplexes = THK2_DUPLEX
1367
        rf.valid_characters = THK2_CHARS
1368
        rf.valid_name_length = 6
1369
        rf.valid_bands = [(136000000, 173990000)]
1370
        rf.valid_skips = ["", "S"]
1371
        rf.valid_tuning_steps = [5.0]
1372
        rf.memory_bounds = (0, 49)
1373
        return rf
1374

    
1375
    def _cmd_get_memory(self, number):
1376
        return "ME", "%02i" % number
1377

    
1378
    def _cmd_get_memory_name(self, number):
1379
        return "MN", "%02i" % number
1380

    
1381
    def _cmd_set_memory(self, number, spec):
1382
        return "ME", "%02i,%s" % (number, spec)
1383

    
1384
    def _cmd_set_memory_name(self, number, name):
1385
        return "MN", "%02i,%s" % (number, name)
1386

    
1387
    def _parse_mem_spec(self, spec):
1388
        mem = chirp_common.Memory()
1389

    
1390
        mem.number = int(spec[0])
1391
        mem.freq = int(spec[1])
1392
        # mem.tuning_step =
1393
        mem.duplex = THK2_DUPLEX[int(spec[3])]
1394
        if int(spec[5]):
1395
            mem.tmode = "Tone"
1396
        elif int(spec[6]):
1397
            mem.tmode = "TSQL"
1398
        elif int(spec[7]):
1399
            mem.tmode = "DTCS"
1400
        mem.rtone = self._kenwood_valid_tones[int(spec[8])]
1401
        mem.ctone = self._kenwood_valid_tones[int(spec[9])]
1402
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
1403
        mem.offset = int(spec[11])
1404
        mem.mode = THK2_MODES[int(spec[12])]
1405
        mem.skip = int(spec[16]) and "S" or ""
1406
        return mem
1407

    
1408
    def _make_mem_spec(self, mem):
1409
        try:
1410
            rti = self._kenwood_valid_tones.index(mem.rtone)
1411
            cti = self._kenwood_valid_tones.index(mem.ctone)
1412
        except ValueError:
1413
            raise errors.UnsupportedToneError()
1414

    
1415
        spec = (
1416
            "%010i" % mem.freq,
1417
            "0",
1418
            "%i" % THK2_DUPLEX.index(mem.duplex),
1419
            "0",
1420
            "%i" % int(mem.tmode == "Tone"),
1421
            "%i" % int(mem.tmode == "TSQL"),
1422
            "%i" % int(mem.tmode == "DTCS"),
1423
            "%02i" % rti,
1424
            "%02i" % cti,
1425
            "%03i" % chirp_common.DTCS_CODES.index(mem.dtcs),
1426
            "%08i" % mem.offset,
1427
            "%i" % THK2_MODES.index(mem.mode),
1428
            "0",
1429
            "%010i" % 0,
1430
            "0",
1431
            "%i" % int(mem.skip == "S")
1432
            )
1433
        return spec
1434

    
1435

    
1436
TM271_STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
1437

    
1438

    
1439
@directory.register
1440
class TM271Radio(THK2Radio):
1441
    """Kenwood TM-271"""
1442
    MODEL = "TM-271"
1443

    
1444
    def get_features(self):
1445
        rf = chirp_common.RadioFeatures()
1446
        rf.can_odd_split = False
1447
        rf.has_dtcs_polarity = False
1448
        rf.has_bank = False
1449
        rf.has_tuning_step = False
1450
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1451
        rf.valid_modes = THK2_MODES
1452
        rf.valid_duplexes = THK2_DUPLEX
1453
        rf.valid_characters = THK2_CHARS
1454
        rf.valid_name_length = 6
1455
        rf.valid_bands = [(137000000, 173990000)]
1456
        rf.valid_skips = ["", "S"]
1457
        rf.valid_tuning_steps = list(TM271_STEPS)
1458
        rf.memory_bounds = (0, 99)
1459
        return rf
1460

    
1461
    def _cmd_get_memory(self, number):
1462
        return "ME", "%03i" % number
1463

    
1464
    def _cmd_get_memory_name(self, number):
1465
        return "MN", "%03i" % number
1466

    
1467
    def _cmd_set_memory(self, number, spec):
1468
        return "ME", "%03i,%s" % (number, spec)
1469

    
1470
    def _cmd_set_memory_name(self, number, name):
1471
        return "MN", "%03i,%s" % (number, name)
1472

    
1473

    
1474
@directory.register
1475
class TM281Radio(TM271Radio):
1476
    """Kenwood TM-281"""
1477
    MODEL = "TM-281"
1478
    # seems that this is a perfect clone of TM271 with just a different model
1479

    
1480

    
1481
@directory.register
1482
class TM471Radio(THK2Radio):
1483
    """Kenwood TM-471"""
1484
    MODEL = "TM-471"
1485

    
1486
    def get_features(self):
1487
        rf = chirp_common.RadioFeatures()
1488
        rf.can_odd_split = False
1489
        rf.has_dtcs_polarity = False
1490
        rf.has_bank = False
1491
        rf.has_tuning_step = False
1492
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1493
        rf.valid_modes = THK2_MODES
1494
        rf.valid_duplexes = THK2_DUPLEX
1495
        rf.valid_characters = THK2_CHARS
1496
        rf.valid_name_length = 6
1497
        rf.valid_bands = [(444000000, 479990000)]
1498
        rf.valid_skips = ["", "S"]
1499
        rf.valid_tuning_steps = [5.0]
1500
        rf.memory_bounds = (0, 99)
1501
        return rf
1502

    
1503
    def _cmd_get_memory(self, number):
1504
        return "ME", "%03i" % number
1505

    
1506
    def _cmd_get_memory_name(self, number):
1507
        return "MN", "%03i" % number
1508

    
1509
    def _cmd_set_memory(self, number, spec):
1510
        return "ME", "%03i,%s" % (number, spec)
1511

    
1512
    def _cmd_set_memory_name(self, number, name):
1513
        return "MN", "%03i,%s" % (number, name)
1514

    
1515

    
1516
@directory.register
1517
class TS590Radio(KenwoodLiveRadio):
1518
    """Kenwood TS-590S/SG"""
1519
    MODEL = "TS-590S/SG_LiveMode"
1520

    
1521
    _kenwood_valid_tones = list(KENWOOD_TONES)
1522
    _kenwood_valid_tones.append(1750)
1523

    
1524
    _upper = 99
1525
    _duplex = ["", "-", "+"]
1526
    _skip = ["", "S"]
1527
    _modes = ["LSB", "USB", "CW", "FM", "AM", "FSK", "CW-R",
1528
              "FSK-R", "Data+LSB", "Data+USB", "Data+FM"]
1529
    _bands = [(1800000, 2000000),    # 160M Band
1530
              (3500000, 4000000),    # 80M Band
1531
              (5167500, 5450000),    # 60M Band
1532
              (7000000, 7300000),    # 40M Band
1533
              (10100000, 10150000),  # 30M Band
1534
              (14000000, 14350000),  # 20M Band
1535
              (18068000, 18168000),  # 17M Band
1536
              (21000000, 21450000),  # 15M Band
1537
              (24890000, 24990000),  # 12M Band
1538
              (28000000, 29700000),  # 10M Band
1539
              (50000000, 54000000)]   # 6M Band
1540

    
1541
    def get_features(self):
1542
        rf = chirp_common.RadioFeatures()
1543

    
1544
        rf.can_odd_split = False
1545
        rf.has_bank = False
1546
        rf.has_ctone = True
1547
        rf.has_dtcs = False
1548
        rf.has_dtcs_polarity = False
1549
        rf.has_name = True
1550
        rf.has_settings = False
1551
        rf.has_offset = True
1552
        rf.has_mode = True
1553
        rf.has_tuning_step = False
1554
        rf.has_nostep_tuning = True
1555
        rf.has_cross = True
1556
        rf.has_comment = False
1557

    
1558
        rf.memory_bounds = (0, self._upper)
1559

    
1560
        rf.valid_bands = self._bands
1561
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/"
1562
        rf.valid_duplexes = ["", "-", "+"]
1563
        rf.valid_modes = self._modes
1564
        rf.valid_skips = ["", "S"]
1565
        rf.valid_tmodes = ["", "Tone", "TSQL", "Cross"]
1566
        rf.valid_cross_modes = ["Tone->Tone", "->Tone"]
1567
        rf.valid_name_length = 8    # 8 character channel names
1568

    
1569
        return rf
1570

    
1571
    def _my_val_list(setting, opts, obj, atrb):
1572
        """Callback:from ValueList. Set the integer index."""
1573
        value = opts.index(str(setting.value))
1574
        setattr(obj, atrb, value)
1575
        return
1576

    
1577
    def get_memory(self, number):
1578
        """Convert ascii channel data spec into UI columns (mem)"""
1579
        mem = chirp_common.Memory()
1580
        mem.extra = RadioSettingGroup("extra", "Extra")
1581
        # Read the base and split MR strings
1582
        mem.number = number
1583
        spec0 = command(self.pipe, "MR0 %02i" % mem.number)
1584
        spec1 = command(self.pipe, "MR1 %02i" % mem.number)
1585
        mem.name = spec0[41:49]  # Max 8-Char Name if assigned
1586
        mem.name = mem.name.strip()
1587
        mem.name = mem.name.upper()
1588
        _p4 = int(spec0[6:17])    # Rx Frequency
1589
        _p4s = int(spec1[6:17])   # Offset freq (Tx)
1590
        _p5 = int(spec0[17])      # Mode
1591
        _p6 = int(spec0[18])      # Data Mode
1592
        _p7 = int(spec0[19])      # Tone Mode
1593
        _p8 = int(spec0[20:22])   # Tone Frequency Index
1594
        _p9 = int(spec0[22:24])   # CTCSS Frequency Index
1595
        _p11 = int(spec0[27])     # Filter A/B
1596
        _p14 = int(spec0[38:40])  # FM Mode
1597
        _p15 = int(spec0[40])     # Chan Lockout (Skip)
1598
        if _p4 == 0:
1599
            mem.empty = True
1600
            return mem
1601
        mem.empty = False
1602
        mem.freq = _p4
1603
        mem.duplex = self._duplex[0]    # None by default
1604
        mem.offset = 0
1605
        if _p4 < _p4s:   # + shift
1606
            mem.duplex = self._duplex[2]
1607
            mem.offset = _p4s - _p4
1608
        if _p4 > _p4s:   # - shift
1609
            mem.duplex = self._duplex[1]
1610
            mem.offset = _p4 - _p4s
1611
        mx = _p5 - 1     # CAT modes start at 1
1612
        if _p5 == 9:     # except CAT FSK-R is 9, there is no 8
1613
            mx = 7
1614
        if _p6:       # LSB+Data= 8, USB+Data= 9, FM+Data= 10
1615
            if _p5 == 1:     # CAT LSB
1616
                mx = 8
1617
            elif _p5 == 2:   # CAT USB
1618
                mx = 9
1619
            elif _p5 == 4:   # CAT FM
1620
                mx = 10
1621
        mem.mode = self._modes[mx]
1622
        mem.tmode = ""
1623
        mem.cross_mode = "Tone->Tone"
1624
        mem.ctone = self._kenwood_valid_tones[_p9]
1625
        mem.rtone = self._kenwood_valid_tones[_p8]
1626
        if _p7 == 1:
1627
            mem.tmode = "Tone"
1628
        elif _p7 == 2:
1629
            mem.tmode = "TSQL"
1630
        elif _p7 == 3:
1631
            mem.tmode = "Cross"
1632
        mem.skip = self._skip[_p15]
1633

    
1634
        rx = RadioSettingValueBoolean(bool(_p14))
1635
        rset = RadioSetting("fmnrw", "FM Narrow mode (off = Wide)", rx)
1636
        mem.extra.append(rset)
1637
        return mem
1638

    
1639
    def erase_memory(self, number):
1640
        """ Send the blank string to MW0 """
1641
        mem = chirp_common.Memory()
1642
        mem.empty = True
1643
        mem.freq = 0
1644
        mem.offset = 0
1645
        spx = "MW0%03i00000000000000000000000000000000000" % number
1646
        rx = command(self.pipe, spx)      # Send MW0
1647
        return mem
1648

    
1649
    def set_memory(self, mem):
1650
        """Send UI column data (mem) to radio"""
1651
        pfx = "MW0%03i" % mem.number
1652
        xmode = 0
1653
        xtmode = 0
1654
        xrtone = 8
1655
        xctone = 8
1656
        xdata = 0
1657
        xfltr = 0
1658
        xfm = 0
1659
        xskip = 0
1660
        xfreq = mem.freq
1661
        if xfreq > 0:       # if empty; use those defaults
1662
            ix = self._modes.index(mem.mode)
1663
            xmode = ix + 1     # stored as CAT values, LSB= 1
1664
            if ix == 7:        # FSK-R
1665
                xmode = 9     # There is no CAT 8
1666
            if ix > 7:         # a Data mode
1667
                xdata = 1
1668
                if ix == 8:
1669
                    xmode = 1      # LSB
1670
                elif ix == 9:
1671
                    xmode = 2      # USB
1672
                elif ix == 10:
1673
                    xmode = 4      # FM
1674
            if mem.tmode == "Tone":
1675
                xtmode = 1
1676
                xrtone = self._kenwood_valid_tones.index(mem.rtone)
1677
            if mem.tmode == "TSQL" or mem.tmode == "Cross":
1678
                xtmode = 2
1679
                if mem.tmode == "Cross":
1680
                    xtmode = 3
1681
                xctone = self._kenwood_valid_tones.index(mem.ctone)
1682
            for setting in mem.extra:
1683
                if setting.get_name() == "fmnrw":
1684
                    xfm = setting.value
1685
            if mem.skip == "S":
1686
                xskip = 1
1687
        spx = "%011i%1i%1i%1i%02i%02i000%1i0000000000%02i%1i%s" \
1688
            % (xfreq, xmode, xdata, xtmode, xrtone,
1689
                xctone, xfltr, xfm, xskip, mem.name)
1690
        rx = command(self.pipe, pfx, spx)      # Send MW0
1691
        if mem.offset != 0:
1692
            pfx = "MW1%03i" % mem.number
1693
            xfreq = mem.freq - mem.offset
1694
            if mem.duplex == "+":
1695
                xfreq = mem.freq + mem.offset
1696
            spx = "%011i%1i%1i%1i%02i%02i000%1i0000000000%02i%1i%s" \
1697
                % (xfreq, xmode, xdata, xtmode, xrtone,
1698
                   xctone, xfltr, xfm, xskip, mem.name)
1699
            rx = command(self.pipe, pfx, spx)      # Send MW1
1700

    
1701

    
1702
@directory.register
1703
class TS480Radio(KenwoodLiveRadio):
1704
    """Kenwood TS-480"""
1705
    MODEL = "TS-480_LiveMode"
1706

    
1707
    _kenwood_valid_tones = list(KENWOOD_TONES)
1708
    _kenwood_valid_tones.append(1750)
1709

    
1710
    _upper = 99
1711
    _duplex = ["", "-", "+"]
1712
    _skip = ["", "S"]
1713
    _modes = ["LSB", "USB", "CW", "FM", "AM", "FSK", "CW-R", "N/A",
1714
              "FSK-R"]
1715
    _bands = [(1800000, 2000000),    # 160M Band
1716
              (3500000, 4000000),    # 80M Band
1717
              (5167500, 5450000),    # 60M Band
1718
              (7000000, 7300000),    # 40M Band
1719
              (10100000, 10150000),  # 30M Band
1720
              (14000000, 14350000),  # 20M Band
1721
              (18068000, 18168000),  # 17M Band
1722
              (21000000, 21450000),  # 15M Band
1723
              (24890000, 24990000),  # 12M Band
1724
              (28000000, 29700000),  # 10M Band
1725
              (50000000, 54000000)]   # 6M Band
1726

    
1727
    _tsteps = [0.5, 1.0, 2.5, 5.0, 6.25, 10.0, 12.5,
1728
               15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
1729

    
1730
    def get_features(self):
1731
        rf = chirp_common.RadioFeatures()
1732

    
1733
        rf.can_odd_split = False
1734
        rf.has_bank = False
1735
        rf.has_ctone = True
1736
        rf.has_dtcs = False
1737
        rf.has_dtcs_polarity = False
1738
        rf.has_name = True
1739
        rf.has_settings = False
1740
        rf.has_offset = True
1741
        rf.has_mode = True
1742
        rf.has_tuning_step = True
1743
        rf.has_nostep_tuning = True
1744
        rf.has_cross = True
1745
        rf.has_comment = False
1746

    
1747
        rf.memory_bounds = (0, self._upper)
1748

    
1749
        rf.valid_bands = self._bands
1750
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/"
1751
        rf.valid_duplexes = ["", "-", "+"]
1752
        rf.valid_modes = self._modes
1753
        rf.valid_skips = ["", "S"]
1754
        rf.valid_tmodes = ["", "Tone", "TSQL", "Cross"]
1755
        rf.valid_cross_modes = ["Tone->Tone", "->Tone"]
1756
        rf.valid_name_length = 8    # 8 character channel names
1757
        rf.valid_tuning_steps = self._tsteps
1758

    
1759
        return rf
1760

    
1761
    def _my_val_list(setting, opts, obj, atrb):
1762
        """Callback:from ValueList. Set the integer index."""
1763
        value = opts.index(str(setting.value))
1764
        setattr(obj, atrb, value)
1765
        return
1766

    
1767
    def get_memory(self, number):
1768
        """Convert ascii channel data spec into UI columns (mem)"""
1769
        mem = chirp_common.Memory()
1770
        # Read the base and split MR strings
1771
        mem.number = number
1772
        spec0 = command(self.pipe, "MR0%03i" % mem.number)
1773
        spec1 = command(self.pipe, "MR1%03i" % mem.number)
1774
        # Add 1 to string idecis if referring to CAT manual
1775
        mem.name = spec0[41:49]  # Max 8-Char Name if assigned
1776
        mem.name = mem.name.strip()
1777
        mem.name = mem.name.upper()
1778
        _p4 = int(spec0[6:17])    # Rx Frequency
1779
        _p4s = int(spec1[6:17])   # Offset freq (Tx)
1780
        _p5 = int(spec0[17])      # Mode
1781
        _p6 = int(spec0[18])      # Chan Lockout (Skip)
1782
        _p7 = int(spec0[19])      # Tone Mode
1783
        _p8 = int(spec0[20:22])   # Tone Frequency Index
1784
        _p9 = int(spec0[22:24])   # CTCSS Frequency Index
1785
        _p14 = int(spec0[38:40])  # Tune Step
1786
        if _p4 == 0:
1787
            mem.empty = True
1788
            return mem
1789
        mem.empty = False
1790
        mem.freq = _p4
1791
        mem.duplex = self._duplex[0]    # None by default
1792
        mem.offset = 0
1793
        if _p4 < _p4s:   # + shift
1794
            mem.duplex = self._duplex[2]
1795
            mem.offset = _p4s - _p4
1796
        if _p4 > _p4s:   # - shift
1797
            mem.duplex = self._duplex[1]
1798
            mem.offset = _p4 - _p4s
1799
        mx = _p5 - 1     # CAT modes start at 1
1800
        mem.mode = self._modes[mx]
1801
        mem.tmode = ""
1802
        mem.cross_mode = "Tone->Tone"
1803
        mem.ctone = self._kenwood_valid_tones[_p9]
1804
        mem.rtone = self._kenwood_valid_tones[_p8]
1805
        if _p7 == 1:
1806
            mem.tmode = "Tone"
1807
        elif _p7 == 2:
1808
            mem.tmode = "TSQL"
1809
        elif _p7 == 3:
1810
            mem.tmode = "Cross"
1811
        mem.skip = self._skip[_p6]
1812
        # Tuning step depends on mode
1813
        options = [0.5, 1.0, 2.5, 5.0, 10.0]    # SSB/CS/FSK
1814
        if _p14 == 4 or _p14 == 5:   # AM/FM
1815
            options = self._tsteps[3:]
1816
        mem.tuning_step = options[_p14]
1817

    
1818
        return mem
1819

    
1820
    def erase_memory(self, number):
1821
        mem = chirp_common.Memory()
1822
        mem.empty = True
1823
        mem.freq = 0
1824
        mem.offset = 0
1825
        spx = "MW0%03i00000000000000000000000000000000000" % number
1826
        rx = command(self.pipe, spx)      # Send MW0
1827
        return mem
1828

    
1829
    def set_memory(self, mem):
1830
        """Send UI column data (mem) to radio"""
1831
        pfx = "MW0%03i" % mem.number
1832
        xtmode = 0
1833
        xdata = 0
1834
        xrtone = 8
1835
        xctone = 8
1836
        xskip = 0
1837
        xstep = 0
1838
        xfreq = mem.freq
1839
        if xfreq > 0:       # if empty, use those defaults
1840
            ix = self._modes.index(mem.mode)
1841
            xmode = ix + 1     # stored as CAT values, LSB= 1
1842
            if ix == 7:        # FSK-R
1843
                xmode = 9     # There is no CAT 8
1844
            if mem.tmode == "Tone":
1845
                xtmode = 1
1846
                xrtone = self._kenwood_valid_tones.index(mem.rtone)
1847
            if mem.tmode == "TSQL" or mem.tmode == "Cross":
1848
                xtmode = 2
1849
                if mem.tmode == "Cross":
1850
                    xtmode = 3
1851
                xctone = self._kenwood_valid_tones.index(mem.ctone)
1852
            if mem.skip == "S":
1853
                xskip = 1
1854
            options = [0.5, 1.0, 2.5, 5.0, 10.0]    # SSB/CS/FSK
1855
            if xmode == 4 or xmode == 5:
1856
                options = self._tsteps[3:]
1857
            xstep = options.index(mem.tuning_step)
1858
        spx = "%011i%1i%1i%1i%02i%02i00000000000000%02i%s" \
1859
            % (xfreq, xmode, xskip, xtmode, xrtone,
1860
                xctone, xstep, mem.name)
1861
        rx = command(self.pipe, pfx, spx)      # Send MW0
1862
        if mem.offset != 0:             # Don't send MW1 if empty
1863
            pfx = "MW1%03i" % mem.number
1864
            xfreq = mem.freq - mem.offset
1865
            if mem.duplex == "+":
1866
                xfreq = mem.freq + mem.offset
1867
            spx = "%011i%1i%1i%1i%02i%02i00000000000000%02i%s" \
1868
                  % (xfreq, xmode, xskip, xtmode, xrtone,
1869
                     xctone, xstep, mem.name)
1870
            rx = command(self.pipe, pfx, spx)      # Send MW1
(7-7/8)