Project

General

Profile

Bug #10515 » kenwood_live.py

Dan Smith, 04/14/2023 03:07 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
from chirp import chirp_common, errors, directory, util
23
from chirp.settings import RadioSetting, RadioSettingGroup, \
24
    RadioSettingValueInteger, RadioSettingValueBoolean, \
25
    RadioSettingValueString, RadioSettingValueList, RadioSettings
26

    
27
LOG = logging.getLogger(__name__)
28

    
29
NOCACHE = "CHIRP_NOCACHE" in os.environ
30

    
31
DUPLEX = {0: "", 1: "+", 2: "-"}
32
MODES = {0: "FM", 1: "AM"}
33
STEPS = list(chirp_common.TUNING_STEPS)
34
STEPS.append(100.0)
35

    
36
KENWOOD_TONES = list(chirp_common.TONES)
37
KENWOOD_TONES.remove(159.8)
38
KENWOOD_TONES.remove(165.5)
39
KENWOOD_TONES.remove(171.3)
40
KENWOOD_TONES.remove(177.3)
41
KENWOOD_TONES.remove(183.5)
42
KENWOOD_TONES.remove(189.9)
43
KENWOOD_TONES.remove(196.6)
44
KENWOOD_TONES.remove(199.5)
45

    
46
THF6_MODES = ["FM", "WFM", "AM", "LSB", "USB", "CW"]
47

    
48

    
49
RADIO_IDS = {
50
    "ID019": "TS-2000",
51
    "ID009": "TS-850",
52
    "ID020": "TS-480_LiveMode",
53
    "ID021": "TS-590S/SG_LiveMode",         # S-model uses same class
54
    "ID023": "TS-590S/SG_LiveMode"          # as SG
55
}
56

    
57
LOCK = threading.Lock()
58
COMMAND_RESP_BUFSIZE = 8
59
LAST_BAUD = 4800
60
LAST_DELIMITER = ("\r", " ")
61

    
62
# The Kenwood TS-2000, TS-480, TS-590 & TS-850 use ";"
63
# as a CAT command message delimiter, and all others use "\n".
64
# Also, TS-2000 and TS-590 don't space delimite the command
65
# fields, but others do.
66

    
67

    
68
def _command(ser, cmd, *args):
69
    """Send @cmd to radio via @ser"""
70
    global LOCK, LAST_DELIMITER, COMMAND_RESP_BUFSIZE
71

    
72
    start = time.time()
73

    
74
    # TODO: This global use of LAST_DELIMITER breaks reentrancy
75
    # and needs to be fixed.
76
    if args:
77
        cmd += LAST_DELIMITER[1] + LAST_DELIMITER[1].join(args)
78
    cmd += LAST_DELIMITER[0]
79

    
80
    LOG.debug("PC->RADIO: %s" % cmd.strip())
81
    ser.write(cmd.encode('cp1252'))
82

    
83
    result = ""
84
    while not result.endswith(LAST_DELIMITER[0]):
85
        result += ser.read(COMMAND_RESP_BUFSIZE).decode('cp1252')
86
        if (time.time() - start) > 0.5:
87
            LOG.error("Timeout waiting for data")
88
            break
89

    
90
    if result.endswith(LAST_DELIMITER[0]):
91
        LOG.debug("RADIO->PC: %r" % result.strip())
92
        result = result[:-1]
93
    else:
94
        LOG.error("Giving up")
95

    
96
    return result.strip()
97

    
98

    
99
def command(ser, cmd, *args):
100
    with LOCK:
101
        return _command(ser, cmd, *args)
102

    
103

    
104
def get_id(ser):
105
    """Get the ID of the radio attached to @ser"""
106
    global LAST_BAUD
107
    bauds = [4800, 9600, 19200, 38400, 57600, 115200]
108
    bauds.remove(LAST_BAUD)
109
    # Make sure LAST_BAUD is last so that it is tried first below
110
    bauds.append(LAST_BAUD)
111

    
112
    global LAST_DELIMITER
113
    command_delimiters = [("\r", " "), (";", "")]
114

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

    
132
            # most kenwood radios
133
            if " " in resp:
134
                LAST_BAUD = i
135
                return resp.split(" ")[1]
136

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

    
147
            # Kenwood radios that return ID numbers
148
            if resp in list(RADIO_IDS.keys()):
149
                return RADIO_IDS[resp]
150

    
151
    raise errors.RadioError("No response from radio")
152

    
153

    
154
def get_tmode(tone, ctcss, dcs):
155
    """Get the tone mode based on the values of the tone, ctcss, dcs"""
156
    if dcs and int(dcs) == 1:
157
        return "DTCS"
158
    elif int(ctcss):
159
        return "TSQL"
160
    elif int(tone):
161
        return "Tone"
162
    else:
163
        return ""
164

    
165

    
166
def iserr(result):
167
    """Returns True if the @result from a radio is an error"""
168
    return result in ["N", "?"]
169

    
170

    
171
class KenwoodLiveRadio(chirp_common.LiveRadio):
172
    """Base class for all live-mode kenwood radios"""
173
    BAUD_RATE = 9600
174
    VENDOR = "Kenwood"
175
    MODEL = ""
176
    NEEDS_COMPAT_SERIAL = False
177
    HARDWARE_FLOW = True
178

    
179
    _vfo = 0
180
    _upper = 200
181
    _kenwood_split = False
182
    _kenwood_valid_tones = list(chirp_common.TONES)
183
    _has_name = True
184

    
185
    def __init__(self, *args, **kwargs):
186
        chirp_common.LiveRadio.__init__(self, *args, **kwargs)
187

    
188
        self._memcache = {}
189

    
190
        if self.pipe:
191
            self.pipe.timeout = 0.1
192
            radio_id = get_id(self.pipe)
193
            if radio_id != self.MODEL.split(" ")[0]:
194
                raise Exception("Radio reports %s (not %s)" % (radio_id,
195
                                                               self.MODEL))
196

    
197
            command(self.pipe, "AI", "0")
198

    
199
    def _cmd_get_memory(self, number):
200
        return "MR", "%i,0,%03i" % (self._vfo, number)
201

    
202
    def _cmd_get_memory_name(self, number):
203
        return "MNA", "%i,%03i" % (self._vfo, number)
204

    
205
    def _cmd_get_split(self, number):
206
        return "MR", "%i,1,%03i" % (self._vfo, number)
207

    
208
    def _cmd_set_memory(self, number, spec):
209
        if spec:
210
            spec = "," + spec
211
        return "MW", "%i,0,%03i%s" % (self._vfo, number, spec)
212

    
213
    def _cmd_set_memory_name(self, number, name):
214
        return "MNA", "%i,%03i,%s" % (self._vfo, number, name)
215

    
216
    def _cmd_set_split(self, number, spec):
217
        return "MW", "%i,1,%03i,%s" % (self._vfo, number, spec)
218

    
219
    def get_raw_memory(self, number):
220
        return command(self.pipe, *self._cmd_get_memory(number))
221

    
222
    def get_memory(self, number):
223
        if number < 0 or number > self._upper:
224
            raise errors.InvalidMemoryLocation(
225
                "Number must be between 0 and %i" % self._upper)
226
        if number in self._memcache and not NOCACHE:
227
            return self._memcache[number]
228

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

    
240
        value = result.split(" ")[1]
241
        spec = value.split(",")
242

    
243
        mem = self._parse_mem_spec(spec)
244
        self._memcache[mem.number] = mem
245

    
246
        if self._has_name:
247
            result = command(self.pipe, *self._cmd_get_memory_name(number))
248
            if " " in result:
249
                value = result.split(" ", 1)[1]
250
                if value.count(",") == 2:
251
                    _zero, _loc, mem.name = value.split(",")
252
                else:
253
                    _loc, mem.name = value.split(",")
254

    
255
        if mem.duplex == "" and self._kenwood_split:
256
            result = command(self.pipe, *self._cmd_get_split(number))
257
            if " " in result:
258
                value = result.split(" ", 1)[1]
259
                self._parse_split_spec(mem, value.split(","))
260

    
261
        return mem
262

    
263
    def _make_mem_spec(self, mem):
264
        pass
265

    
266
    def _parse_mem_spec(self, spec):
267
        pass
268

    
269
    def _parse_split_spec(self, mem, spec):
270
        mem.duplex = "split"
271
        mem.offset = int(spec[2])
272

    
273
    def _make_split_spec(self, mem):
274
        return ("%011i" % mem.offset, "0")
275

    
276
    def set_memory(self, memory):
277
        if memory.number < 0 or memory.number > self._upper:
278
            raise errors.InvalidMemoryLocation(
279
                "Number must be between 0 and %i" % self._upper)
280

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

    
298
        if memory.duplex == "split" and self._kenwood_split:
299
            spec = ",".join(self._make_split_spec(memory))
300
            result = command(self.pipe, *self._cmd_set_split(memory.number,
301
                                                             spec))
302
            if iserr(result):
303
                raise errors.InvalidDataError("Radio refused %i" %
304
                                              memory.number)
305

    
306
    def erase_memory(self, number):
307
        if number not in self._memcache:
308
            return
309

    
310
        resp = command(self.pipe, *self._cmd_set_memory(number, ""))
311
        if iserr(resp):
312
            raise errors.RadioError("Radio refused delete of %i" % number)
313
        del self._memcache[number]
314

    
315
    def _kenwood_get(self, cmd):
316
        resp = command(self.pipe, cmd)
317
        if " " in resp:
318
            return resp.split(" ", 1)
319
        else:
320
            if resp == cmd:
321
                return [resp, ""]
322
            else:
323
                raise errors.RadioError("Radio refused to return %s" % cmd)
324

    
325
    def _kenwood_set(self, cmd, value):
326
        resp = command(self.pipe, cmd, value)
327
        if resp[:len(cmd)] == cmd:
328
            return
329
        raise errors.RadioError("Radio refused to set %s" % cmd)
330

    
331
    def _kenwood_get_bool(self, cmd):
332
        _cmd, result = self._kenwood_get(cmd)
333
        return result == "1"
334

    
335
    def _kenwood_set_bool(self, cmd, value):
336
        return self._kenwood_set(cmd, str(int(value)))
337

    
338
    def _kenwood_get_int(self, cmd):
339
        _cmd, result = self._kenwood_get(cmd)
340
        return int(result)
341

    
342
    def _kenwood_set_int(self, cmd, value, digits=1):
343
        return self._kenwood_set(cmd, ("%%0%ii" % digits) % value)
344

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

    
375

    
376
class KenwoodOldLiveRadio(KenwoodLiveRadio):
377
    _kenwood_valid_tones = list(chirp_common.OLD_TONES)
378

    
379
    def set_memory(self, memory):
380
        supported_tones = list(chirp_common.OLD_TONES)
381
        supported_tones.remove(69.3)
382
        if memory.rtone not in supported_tones:
383
            raise errors.UnsupportedToneError("This radio does not support " +
384
                                              "tone %.1fHz" % memory.rtone)
385
        if memory.ctone not in supported_tones:
386
            raise errors.UnsupportedToneError("This radio does not support " +
387
                                              "tone %.1fHz" % memory.ctone)
388

    
389
        return KenwoodLiveRadio.set_memory(self, memory)
390

    
391

    
392
@directory.register
393
class THD7Radio(KenwoodOldLiveRadio):
394
    """Kenwood TH-D7"""
395
    MODEL = "TH-D7"
396

    
397
    _kenwood_split = True
398

    
399
    _BEP_OPTIONS = ["Off", "Key", "Key+Data", "All"]
400
    _POSC_OPTIONS = ["Off Duty", "Enroute", "In Service", "Returning",
401
                     "Committed", "Special", "Priority", "Emergency"]
402

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

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

    
448
    def _make_mem_spec(self, mem):
449
        if mem.duplex in " -+":
450
            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
451
            offset = mem.offset
452
        else:
453
            duplex = 0
454
            offset = 0
455

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

    
471
        return spec
472

    
473
    def _parse_mem_spec(self, spec):
474
        mem = chirp_common.Memory()
475

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

    
494
        return mem
495

    
496
    EXTRA_BOOL_SETTINGS = {
497
        'main': [("LMP", "Lamp")],
498
        'dtmf': [("TXH", "TX Hold")],
499
    }
500
    EXTRA_LIST_SETTINGS = {
501
        'main': [("BAL", "Balance"),
502
                 ("MNF", "Memory Display Mode")],
503
        'save': [("SV", "Battery Save")],
504
    }
505

    
506
    def _get_setting_options(self, setting):
507
        opts = self._SETTINGS_OPTIONS[setting]
508
        if opts is None:
509
            opts = getattr(self, '_%s_OPTIONS' % setting)
510
        return opts
511

    
512
    def get_settings(self):
513
        main = RadioSettingGroup("main", "Main")
514
        aux = RadioSettingGroup("aux", "Aux")
515
        tnc = RadioSettingGroup("tnc", "TNC")
516
        save = RadioSettingGroup("save", "Save")
517
        display = RadioSettingGroup("display", "Display")
518
        dtmf = RadioSettingGroup("dtmf", "DTMF")
519
        radio = RadioSettingGroup("radio", "Radio",
520
                                  aux, tnc, save, display, dtmf)
521
        sky = RadioSettingGroup("sky", "SkyCommand")
522
        aprs = RadioSettingGroup("aprs", "APRS")
523

    
524
        top = RadioSettings(main, radio, aprs, sky)
525

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

    
537
        for setting, group, name in bools:
538
            value = self._kenwood_get_bool(setting)
539
            rs = RadioSetting(setting, name,
540
                              RadioSettingValueBoolean(value))
541
            group.append(rs)
542

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

    
559
        for setting, group, name in lists:
560
            value = self._kenwood_get_int(setting)
561
            options = self._get_setting_options(setting)
562
            rs = RadioSetting(setting, name,
563
                              RadioSettingValueList(options,
564
                                                    options[value]))
565
            group.append(rs)
566

    
567
        for group_name, settings in self.EXTRA_BOOL_SETTINGS.items():
568
            group = locals()[group_name]
569
            for setting, name in settings:
570
                value = self._kenwood_get_bool(setting)
571
                rs = RadioSetting(setting, name,
572
                                  RadioSettingValueBoolean(value))
573
                group.append(rs)
574

    
575
        for group_name, settings in self.EXTRA_LIST_SETTINGS.items():
576
            group = locals()[group_name]
577
            for setting, name in settings:
578
                value = self._kenwood_get_int(setting)
579
                options = self._get_setting_options(setting)
580
                rs = RadioSetting(setting, name,
581
                                  RadioSettingValueBoolean(value))
582
                group.append(rs)
583

    
584
        ints = [("CNT", display, "Contrast", 1, 16),
585
                ]
586
        for setting, group, name, minv, maxv in ints:
587
            value = self._kenwood_get_int(setting)
588
            rs = RadioSetting(setting, name,
589
                              RadioSettingValueInteger(minv, maxv, value))
590
            group.append(rs)
591

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

    
605
        return top
606

    
607

    
608
@directory.register
609
class THD7GRadio(THD7Radio):
610
    """Kenwood TH-D7G"""
611
    MODEL = "TH-D7G"
612

    
613
    def get_features(self):
614
        rf = super(THD7GRadio, self).get_features()
615
        rf.valid_name_length = 8
616
        return rf
617

    
618

    
619
@directory.register
620
class TMD700Radio(THD7Radio):
621
    """Kenwood TH-D700"""
622
    MODEL = "TM-D700"
623

    
624
    _kenwood_split = True
625

    
626
    _BEP_OPTIONS = ["Off", "Key"]
627
    _POSC_OPTIONS = ["Off Duty", "Enroute", "In Service", "Returning",
628
                     "Committed", "Special", "Priority", "CUSTOM 0",
629
                     "CUSTOM 1", "CUSTOM 2", "CUSTOM 4", "CUSTOM 5",
630
                     "CUSTOM 6", "Emergency"]
631
    EXTRA_BOOL_SETTINGS = {}
632
    EXTRA_LIST_SETTINGS = {}
633

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

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

    
672
        return spec
673

    
674
    def _parse_mem_spec(self, spec):
675
        mem = chirp_common.Memory()
676

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

    
695
        return mem
696

    
697

    
698
@directory.register
699
class TMV7Radio(KenwoodOldLiveRadio):
700
    """Kenwood TM-V7"""
701
    MODEL = "TM-V7"
702

    
703
    mem_upper_limit = 200  # Will be updated
704

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

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

    
736
        return spec
737

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

    
751
        return mem
752

    
753
    def get_sub_devices(self):
754
        return [TMV7RadioVHF(self.pipe), TMV7RadioUHF(self.pipe)]
755

    
756
    def __test_location(self, loc):
757
        mem = self.get_memory(loc)
758
        if not mem.empty:
759
            # Memory was not empty, must be valid
760
            return True
761

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

    
774
        # Erase what we did
775
        try:
776
            self.erase_memory(loc)
777
        except Exception:
778
            pass  # V7A Can't delete just yet
779

    
780
        return True
781

    
782
    def _detect_split(self):
783
        return 50
784

    
785

    
786
class TMV7RadioSub(TMV7Radio):
787
    """Base class for the TM-V7 sub devices"""
788
    def __init__(self, pipe):
789
        TMV7Radio.__init__(self, pipe)
790
        self._detect_split()
791

    
792

    
793
class TMV7RadioVHF(TMV7RadioSub):
794
    """TM-V7 VHF subdevice"""
795
    VARIANT = "VHF"
796
    _vfo = 0
797

    
798

    
799
class TMV7RadioUHF(TMV7RadioSub):
800
    """TM-V7 UHF subdevice"""
801
    VARIANT = "UHF"
802
    _vfo = 1
803

    
804

    
805
@directory.register
806
class TMG707Radio(TMV7Radio):
807
    """Kenwood TM-G707"""
808
    MODEL = "TM-G707"
809

    
810
    def get_features(self):
811
        rf = TMV7Radio.get_features(self)
812
        rf.has_sub_devices = False
813
        rf.memory_bounds = (1, 180)
814
        rf.valid_bands = [(118000000, 174000000),
815
                          (300000000, 520000000),
816
                          (800000000, 999000000)]
817
        return rf
818

    
819

    
820
THG71_STEPS = [5, 6.25, 10, 12.5, 15, 20, 25, 30, 50, 100]
821

    
822

    
823
@directory.register
824
class THG71Radio(TMV7Radio):
825
    """Kenwood TH-G71"""
826
    MODEL = "TH-G71"
827

    
828
    def get_features(self):
829
        rf = TMV7Radio.get_features(self)
830
        rf.has_tuning_step = True
831
        rf.valid_tuning_steps = list(THG71_STEPS)
832
        rf.valid_name_length = 6
833
        rf.has_sub_devices = False
834
        rf.valid_bands = [(118000000, 174000000),
835
                          (320000000, 470000000),
836
                          (800000000, 945000000)]
837
        return rf
838

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

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

    
873

    
874
THF6A_STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
875
               100.0]
876

    
877
THF6A_DUPLEX = dict(DUPLEX)
878
THF6A_DUPLEX[3] = "split"
879

    
880

    
881
@directory.register
882
class THF6ARadio(KenwoodLiveRadio):
883
    """Kenwood TH-F6"""
884
    MODEL = "TH-F6"
885

    
886
    _charset = chirp_common.CHARSET_ASCII
887
    _upper = 399
888
    _kenwood_split = True
889
    _kenwood_valid_tones = list(KENWOOD_TONES)
890

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

    
908
    def _cmd_set_memory(self, number, spec):
909
        if spec:
910
            spec = "," + spec
911
        return "MW", "0,%03i%s" % (number, spec)
912

    
913
    def _cmd_get_memory(self, number):
914
        return "MR", "0,%03i" % number
915

    
916
    def _cmd_get_memory_name(self, number):
917
        return "MNA", "%03i" % number
918

    
919
    def _cmd_set_memory_name(self, number, name):
920
        return "MNA", "%03i,%s" % (number, name)
921

    
922
    def _cmd_get_split(self, number):
923
        return "MR", "1,%03i" % number
924

    
925
    def _cmd_set_split(self, number, spec):
926
        return "MW", "1,%03i,%s" % (number, spec)
927

    
928
    def _parse_mem_spec(self, spec):
929
        mem = chirp_common.Memory()
930

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

    
950
        return mem
951

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

    
976
        return spec
977

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

    
995
    def get_settings(self):
996
        main = RadioSettingGroup("main", "Main")
997
        aux = RadioSettingGroup("aux", "Aux")
998
        save = RadioSettingGroup("save", "Save")
999
        display = RadioSettingGroup("display", "Display")
1000
        dtmf = RadioSettingGroup("dtmf", "DTMF")
1001
        top = RadioSettings(main, aux, save, display, dtmf)
1002

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

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

    
1035
        ints = [("CNT", display, "Contrast", 1, 16),
1036
                ("VXG", aux, "VOX Gain", 0, 9),
1037
                ]
1038

    
1039
        strings = [("MES", display, "Power-on Message", 8),
1040
                   ]
1041

    
1042
        for setting, group, name in bools:
1043
            value = self._kenwood_get_bool(setting)
1044
            rs = RadioSetting(setting, name,
1045
                              RadioSettingValueBoolean(value))
1046
            group.append(rs)
1047

    
1048
        for setting, group, name in lists:
1049
            value = self._kenwood_get_int(setting)
1050
            options = self._SETTINGS_OPTIONS[setting]
1051
            rs = RadioSetting(setting, name,
1052
                              RadioSettingValueList(options,
1053
                                                    options[value]))
1054
            group.append(rs)
1055

    
1056
        for setting, group, name, minv, maxv in ints:
1057
            value = self._kenwood_get_int(setting)
1058
            rs = RadioSetting(setting, name,
1059
                              RadioSettingValueInteger(minv, maxv, value))
1060
            group.append(rs)
1061

    
1062
        for setting, group, name, length in strings:
1063
            _cmd, value = self._kenwood_get(setting)
1064
            rs = RadioSetting(setting, name,
1065
                              RadioSettingValueString(0, length, value))
1066
            group.append(rs)
1067

    
1068
        return top
1069

    
1070

    
1071
@directory.register
1072
class THF7ERadio(THF6ARadio):
1073
    """Kenwood TH-F7"""
1074
    MODEL = "TH-F7"
1075
    _charset = chirp_common.CHARSET_1252
1076

    
1077

    
1078
D710_DUPLEX = ["", "+", "-", "split"]
1079
D710_MODES = ["FM", "NFM", "AM"]
1080
D710_SKIP = ["", "S"]
1081
D710_STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
1082

    
1083

    
1084
@directory.register
1085
class TMD710Radio(KenwoodLiveRadio):
1086
    """Kenwood TM-D710"""
1087
    MODEL = "TM-D710"
1088

    
1089
    _upper = 999
1090
    _kenwood_valid_tones = list(KENWOOD_TONES)
1091

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

    
1107
    def _cmd_get_memory(self, number):
1108
        return "ME", "%03i" % number
1109

    
1110
    def _cmd_get_memory_name(self, number):
1111
        return "MN", "%03i" % number
1112

    
1113
    def _cmd_set_memory(self, number, spec):
1114
        return "ME", "%03i,%s" % (number, spec)
1115

    
1116
    def _cmd_set_memory_name(self, number, name):
1117
        return "MN", "%03i,%s" % (number, name)
1118

    
1119
    def _parse_mem_spec(self, spec):
1120
        mem = chirp_common.Memory()
1121

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

    
1145
        return mem
1146

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

    
1167
        return spec
1168

    
1169

    
1170
@directory.register
1171
class THD72Radio(TMD710Radio):
1172
    """Kenwood TH-D72"""
1173
    MODEL = "TH-D72 (live mode)"
1174
    HARDWARE_FLOW = sys.platform == "darwin"  # only OS X driver needs hw flow
1175

    
1176
    def _parse_mem_spec(self, spec):
1177
        mem = chirp_common.Memory()
1178

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

    
1202
        return mem
1203

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

    
1226
        return spec
1227

    
1228

    
1229
@directory.register
1230
class THD74Radio(TMD710Radio):
1231
    """Kenwood TH_D74"""
1232
    MODEL = "TH-D74 (live mode)"
1233
    HARDWARE_FLOW = sys.platform == "darwin"
1234

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

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

    
1252
    def _cmd_get_memory_name(self, number):
1253
        return ''
1254

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

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

    
1282
        mem.duplex = self.DUPLEX[int(spec[14])]
1283
        mem.rtone = chirp_common.TONES[int(spec[15])]
1284
        mem.ctone = chirp_common.TONES[int(spec[16])]
1285
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[17])]
1286
        mem.skip = int(spec[22]) and 'S' or ''
1287

    
1288
        return mem
1289

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

    
1316
        return spec
1317

    
1318

    
1319
@directory.register
1320
class TMV71Radio(TMD710Radio):
1321
    """Kenwood TM-V71"""
1322
    MODEL = "TM-V71"
1323

    
1324

    
1325
@directory.register
1326
class TMD710GRadio(TMD710Radio):
1327
    """Kenwood TM-D710G"""
1328
    MODEL = "TM-D710G"
1329

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

    
1339

    
1340
THK2_DUPLEX = ["", "+", "-"]
1341
THK2_MODES = ["FM", "NFM"]
1342

    
1343
THK2_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/"
1344

    
1345

    
1346
@directory.register
1347
class THK2Radio(KenwoodLiveRadio):
1348
    """Kenwood TH-K2"""
1349
    MODEL = "TH-K2"
1350

    
1351
    _kenwood_valid_tones = list(KENWOOD_TONES)
1352

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

    
1370
    def _cmd_get_memory(self, number):
1371
        return "ME", "%02i" % number
1372

    
1373
    def _cmd_get_memory_name(self, number):
1374
        return "MN", "%02i" % number
1375

    
1376
    def _cmd_set_memory(self, number, spec):
1377
        return "ME", "%02i,%s" % (number, spec)
1378

    
1379
    def _cmd_set_memory_name(self, number, name):
1380
        return "MN", "%02i,%s" % (number, name)
1381

    
1382
    def _parse_mem_spec(self, spec):
1383
        mem = chirp_common.Memory()
1384

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

    
1403
    def _make_mem_spec(self, mem):
1404
        try:
1405
            rti = self._kenwood_valid_tones.index(mem.rtone)
1406
            cti = self._kenwood_valid_tones.index(mem.ctone)
1407
        except ValueError:
1408
            raise errors.UnsupportedToneError()
1409

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

    
1430

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

    
1433

    
1434
@directory.register
1435
class TM271Radio(THK2Radio):
1436
    """Kenwood TM-271"""
1437
    MODEL = "TM-271"
1438

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

    
1456
    def _cmd_get_memory(self, number):
1457
        return "ME", "%03i" % number
1458

    
1459
    def _cmd_get_memory_name(self, number):
1460
        return "MN", "%03i" % number
1461

    
1462
    def _cmd_set_memory(self, number, spec):
1463
        return "ME", "%03i,%s" % (number, spec)
1464

    
1465
    def _cmd_set_memory_name(self, number, name):
1466
        return "MN", "%03i,%s" % (number, name)
1467

    
1468

    
1469
@directory.register
1470
class TM281Radio(TM271Radio):
1471
    """Kenwood TM-281"""
1472
    MODEL = "TM-281"
1473
    # seems that this is a perfect clone of TM271 with just a different model
1474

    
1475

    
1476
@directory.register
1477
class TM471Radio(THK2Radio):
1478
    """Kenwood TM-471"""
1479
    MODEL = "TM-471"
1480

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

    
1498
    def _cmd_get_memory(self, number):
1499
        return "ME", "%03i" % number
1500

    
1501
    def _cmd_get_memory_name(self, number):
1502
        return "MN", "%03i" % number
1503

    
1504
    def _cmd_set_memory(self, number, spec):
1505
        return "ME", "%03i,%s" % (number, spec)
1506

    
1507
    def _cmd_set_memory_name(self, number, name):
1508
        return "MN", "%03i,%s" % (number, name)
1509

    
1510

    
1511
@directory.register
1512
class TS590Radio(KenwoodLiveRadio):
1513
    """Kenwood TS-590S/SG"""
1514
    MODEL = "TS-590S/SG_LiveMode"
1515

    
1516
    _kenwood_valid_tones = list(KENWOOD_TONES)
1517
    _kenwood_valid_tones.append(1750)
1518

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

    
1536
    def get_features(self):
1537
        rf = chirp_common.RadioFeatures()
1538

    
1539
        rf.can_odd_split = False
1540
        rf.has_bank = False
1541
        rf.has_ctone = True
1542
        rf.has_dtcs = False
1543
        rf.has_dtcs_polarity = False
1544
        rf.has_name = True
1545
        rf.has_settings = False
1546
        rf.has_offset = True
1547
        rf.has_mode = True
1548
        rf.has_tuning_step = False
1549
        rf.has_nostep_tuning = True
1550
        rf.has_cross = True
1551
        rf.has_comment = False
1552

    
1553
        rf.memory_bounds = (0, self._upper)
1554

    
1555
        rf.valid_bands = self._bands
1556
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/"
1557
        rf.valid_duplexes = ["", "-", "+"]
1558
        rf.valid_modes = self._modes
1559
        rf.valid_skips = ["", "S"]
1560
        rf.valid_tmodes = ["", "Tone", "TSQL", "Cross"]
1561
        rf.valid_cross_modes = ["Tone->Tone", "->Tone"]
1562
        rf.valid_name_length = 8    # 8 character channel names
1563

    
1564
        return rf
1565

    
1566
    def _my_val_list(setting, opts, obj, atrb):
1567
        """Callback:from ValueList. Set the integer index."""
1568
        value = opts.index(str(setting.value))
1569
        setattr(obj, atrb, value)
1570
        return
1571

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

    
1629
        rx = RadioSettingValueBoolean(bool(_p14))
1630
        rset = RadioSetting("fmnrw", "FM Narrow mode (off = Wide)", rx)
1631
        mem.extra.append(rset)
1632
        return mem
1633

    
1634
    def erase_memory(self, number):
1635
        """ Send the blank string to MW0 """
1636
        mem = chirp_common.Memory()
1637
        mem.empty = True
1638
        mem.freq = 0
1639
        mem.offset = 0
1640
        spx = "MW0%03i00000000000000000000000000000000000" % number
1641
        rx = command(self.pipe, spx)      # Send MW0
1642
        return mem
1643

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

    
1696

    
1697
@directory.register
1698
class TS480Radio(KenwoodLiveRadio):
1699
    """Kenwood TS-480"""
1700
    MODEL = "TS-480_LiveMode"
1701

    
1702
    _kenwood_valid_tones = list(KENWOOD_TONES)
1703
    _kenwood_valid_tones.append(1750)
1704

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

    
1722
    _tsteps = [0.5, 1.0, 2.5, 5.0, 6.25, 10.0, 12.5,
1723
               15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
1724

    
1725
    def get_features(self):
1726
        rf = chirp_common.RadioFeatures()
1727

    
1728
        rf.can_odd_split = False
1729
        rf.has_bank = False
1730
        rf.has_ctone = True
1731
        rf.has_dtcs = False
1732
        rf.has_dtcs_polarity = False
1733
        rf.has_name = True
1734
        rf.has_settings = False
1735
        rf.has_offset = True
1736
        rf.has_mode = True
1737
        rf.has_tuning_step = True
1738
        rf.has_nostep_tuning = True
1739
        rf.has_cross = True
1740
        rf.has_comment = False
1741

    
1742
        rf.memory_bounds = (0, self._upper)
1743

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

    
1754
        return rf
1755

    
1756
    def _my_val_list(setting, opts, obj, atrb):
1757
        """Callback:from ValueList. Set the integer index."""
1758
        value = opts.index(str(setting.value))
1759
        setattr(obj, atrb, value)
1760
        return
1761

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

    
1813
        return mem
1814

    
1815
    def erase_memory(self, number):
1816
        mem = chirp_common.Memory()
1817
        mem.empty = True
1818
        mem.freq = 0
1819
        mem.offset = 0
1820
        spx = "MW0%03i00000000000000000000000000000000000" % number
1821
        rx = command(self.pipe, spx)      # Send MW0
1822
        return mem
1823

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