kenwood_live.py

Tom Hayward, 11/20/2016 02:56 pm

Download (43.7 kB)

 
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
LOCK = threading.Lock()
49
COMMAND_RESP_BUFSIZE = 8
50
LAST_BAUD = 9600
51
LAST_DELIMITER = ("\r", " ")
52

    
53
# The Kenwood TS-2000 uses ";" as a CAT command message delimiter, and all
54
# others use "\n". Also, TS-2000 doesn't space delimite the command fields,
55
# but others do.
56

    
57

    
58
def command(ser, cmd, *args):
59
    """Send @cmd to radio via @ser"""
60
    global LOCK, LAST_DELIMITER, COMMAND_RESP_BUFSIZE
61

    
62
    start = time.time()
63

    
64
    LOCK.acquire()
65

    
66
    if args:
67
        cmd += LAST_DELIMITER[1] + LAST_DELIMITER[1].join(args)
68
    cmd += LAST_DELIMITER[0]
69

    
70
    LOG.debug("PC->RADIO: %s" % cmd.strip())
71
    ser.write(cmd)
72

    
73
    result = ""
74
    while not result.endswith(LAST_DELIMITER[0]):
75
        result += ser.read(COMMAND_RESP_BUFSIZE)
76
        if (time.time() - start) > 0.5:
77
            LOG.error("Timeout waiting for data")
78
            break
79

    
80
    if result.endswith(LAST_DELIMITER[0]):
81
        LOG.debug("RADIO->PC: %s" % result.strip())
82
        result = result[:-1]
83
    else:
84
        LOG.error("Giving up")
85

    
86
    LOCK.release()
87

    
88
    return result.strip()
89

    
90

    
91
def get_id(ser):
92
    """Get the ID of the radio attached to @ser"""
93
    global LAST_BAUD
94
    bauds = [9600, 19200, 38400, 57600]
95
    bauds.remove(LAST_BAUD)
96
    bauds.insert(0, LAST_BAUD)
97

    
98
    global LAST_DELIMITER
99
    command_delimiters = [("\r", " "), (";", "")]
100

    
101
    for i in bauds:
102
        for delimiter in command_delimiters:
103
            LAST_DELIMITER = delimiter
104
            LOG.info("Trying ID at baud %i with delimiter \"%s\"" %
105
                     (i, repr(delimiter)))
106
            ser.baudrate = i
107
            ser.write(LAST_DELIMITER[0])
108
            ser.read(25)
109
            resp = command(ser, "ID")
110

    
111
            # most kenwood radios
112
            if " " in resp:
113
                LAST_BAUD = i
114
                return resp.split(" ")[1]
115

    
116
            # TS-2000
117
            if "ID019" == resp:
118
                LAST_BAUD = i
119
                return "TS-2000"
120

    
121
    raise errors.RadioError("No response from radio")
122

    
123

    
124
def get_tmode(tone, ctcss, dcs):
125
    """Get the tone mode based on the values of the tone, ctcss, dcs"""
126
    if dcs and int(dcs) == 1:
127
        return "DTCS"
128
    elif int(ctcss):
129
        return "TSQL"
130
    elif int(tone):
131
        return "Tone"
132
    else:
133
        return ""
134

    
135

    
136
def iserr(result):
137
    """Returns True if the @result from a radio is an error"""
138
    return result in ["N", "?"]
139

    
140

    
141
class KenwoodLiveRadio(chirp_common.LiveRadio):
142
    """Base class for all live-mode kenwood radios"""
143
    BAUD_RATE = 9600
144
    VENDOR = "Kenwood"
145
    MODEL = ""
146

    
147
    _vfo = 0
148
    _upper = 200
149
    _kenwood_split = False
150
    _kenwood_valid_tones = list(chirp_common.TONES)
151

    
152
    def __init__(self, *args, **kwargs):
153
        chirp_common.LiveRadio.__init__(self, *args, **kwargs)
154

    
155
        self._memcache = {}
156

    
157
        if self.pipe:
158
            self.pipe.timeout = 0.1
159
            radio_id = get_id(self.pipe)
160
            if radio_id != self.MODEL.split(" ")[0]:
161
                raise Exception("Radio reports %s (not %s)" % (radio_id,
162
                                                               self.MODEL))
163

    
164
            command(self.pipe, "AI", "0")
165

    
166
    def _cmd_get_memory(self, number):
167
        return "MR", "%i,0,%03i" % (self._vfo, number)
168

    
169
    def _cmd_get_memory_name(self, number):
170
        return "MNA", "%i,%03i" % (self._vfo, number)
171

    
172
    def _cmd_get_split(self, number):
173
        return "MR", "%i,1,%03i" % (self._vfo, number)
174

    
175
    def _cmd_set_memory(self, number, spec):
176
        if spec:
177
            spec = "," + spec
178
        return "MW", "%i,0,%03i%s" % (self._vfo, number, spec)
179

    
180
    def _cmd_set_memory_name(self, number, name):
181
        return "MNA", "%i,%03i,%s" % (self._vfo, number, name)
182

    
183
    def _cmd_set_split(self, number, spec):
184
        return "MW", "%i,1,%03i,%s" % (self._vfo, number, spec)
185

    
186
    def get_raw_memory(self, number):
187
        return command(self.pipe, *self._cmd_get_memory(number))
188

    
189
    def get_memory(self, number):
190
        if number < 0 or number > self._upper:
191
            raise errors.InvalidMemoryLocation(
192
                "Number must be between 0 and %i" % self._upper)
193
        if number in self._memcache and not NOCACHE:
194
            return self._memcache[number]
195

    
196
        result = command(self.pipe, *self._cmd_get_memory(number))
197
        if result == "N" or result == "E":
198
            mem = chirp_common.Memory()
199
            mem.number = number
200
            mem.empty = True
201
            self._memcache[mem.number] = mem
202
            return mem
203
        elif " " not in result:
204
            LOG.error("Not sure what to do with this: `%s'" % result)
205
            raise errors.RadioError("Unexpected result returned from radio")
206

    
207
        value = result.split(" ")[1]
208
        spec = value.split(",")
209

    
210
        mem = self._parse_mem_spec(spec)
211
        self._memcache[mem.number] = mem
212

    
213
        result = command(self.pipe, *self._cmd_get_memory_name(number))
214
        if " " in result:
215
            value = result.split(" ", 1)[1]
216
            if value.count(",") == 2:
217
                _zero, _loc, mem.name = value.split(",")
218
            else:
219
                _loc, mem.name = value.split(",")
220

    
221
        if mem.duplex == "" and self._kenwood_split:
222
            result = command(self.pipe, *self._cmd_get_split(number))
223
            if " " in result:
224
                value = result.split(" ", 1)[1]
225
                self._parse_split_spec(mem, value.split(","))
226

    
227
        return mem
228

    
229
    def _make_mem_spec(self, mem):
230
        pass
231

    
232
    def _parse_mem_spec(self, spec):
233
        pass
234

    
235
    def _parse_split_spec(self, mem, spec):
236
        mem.duplex = "split"
237
        mem.offset = int(spec[2])
238

    
239
    def _make_split_spec(self, mem):
240
        return ("%011i" % mem.offset, "0")
241

    
242
    def set_memory(self, memory):
243
        if memory.number < 0 or memory.number > self._upper:
244
            raise errors.InvalidMemoryLocation(
245
                "Number must be between 0 and %i" % self._upper)
246

    
247
        spec = self._make_mem_spec(memory)
248
        spec = ",".join(spec)
249
        r1 = command(self.pipe, *self._cmd_set_memory(memory.number, spec))
250
        if not iserr(r1):
251
            time.sleep(0.5)
252
            r2 = command(self.pipe, *self._cmd_set_memory_name(memory.number,
253
                                                               memory.name))
254
            if not iserr(r2):
255
                memory.name = memory.name.rstrip()
256
                self._memcache[memory.number] = memory
257
            else:
258
                raise errors.InvalidDataError("Radio refused name %i: %s" %
259
                                              (memory.number,
260
                                               repr(memory.name)))
261
        else:
262
            raise errors.InvalidDataError("Radio refused %i" % memory.number)
263

    
264
        if memory.duplex == "split" and self._kenwood_split:
265
            spec = ",".join(self._make_split_spec(memory))
266
            result = command(self.pipe, *self._cmd_set_split(memory.number,
267
                                                             spec))
268
            if iserr(result):
269
                raise errors.InvalidDataError("Radio refused %i" %
270
                                              memory.number)
271

    
272
    def erase_memory(self, number):
273
        if number not in self._memcache:
274
            return
275

    
276
        resp = command(self.pipe, *self._cmd_set_memory(number, ""))
277
        if iserr(resp):
278
            raise errors.RadioError("Radio refused delete of %i" % number)
279
        del self._memcache[number]
280

    
281
    def _kenwood_get(self, cmd):
282
        resp = command(self.pipe, cmd)
283
        if " " in resp:
284
            return resp.split(" ", 1)
285
        else:
286
            if resp == cmd:
287
                return [resp, ""]
288
            else:
289
                raise errors.RadioError("Radio refused to return %s" % cmd)
290

    
291
    def _kenwood_set(self, cmd, value):
292
        resp = command(self.pipe, cmd, value)
293
        if resp[:len(cmd)] == cmd:
294
            return
295
        raise errors.RadioError("Radio refused to set %s" % cmd)
296

    
297
    def _kenwood_get_bool(self, cmd):
298
        _cmd, result = self._kenwood_get(cmd)
299
        return result == "1"
300

    
301
    def _kenwood_set_bool(self, cmd, value):
302
        return self._kenwood_set(cmd, str(int(value)))
303

    
304
    def _kenwood_get_int(self, cmd):
305
        _cmd, result = self._kenwood_get(cmd)
306
        return int(result)
307

    
308
    def _kenwood_set_int(self, cmd, value, digits=1):
309
        return self._kenwood_set(cmd, ("%%0%ii" % digits) % value)
310

    
311
    def set_settings(self, settings):
312
        for element in settings:
313
            if not isinstance(element, RadioSetting):
314
                self.set_settings(element)
315
                continue
316
            if not element.changed():
317
                continue
318
            if isinstance(element.value, RadioSettingValueBoolean):
319
                self._kenwood_set_bool(element.get_name(), element.value)
320
            elif isinstance(element.value, RadioSettingValueList):
321
                options = self._SETTINGS_OPTIONS[element.get_name()]
322
                self._kenwood_set_int(element.get_name(),
323
                                      options.index(str(element.value)))
324
            elif isinstance(element.value, RadioSettingValueInteger):
325
                if element.value.get_max() > 9:
326
                    digits = 2
327
                else:
328
                    digits = 1
329
                self._kenwood_set_int(element.get_name(),
330
                                      element.value, digits)
331
            elif isinstance(element.value, RadioSettingValueString):
332
                self._kenwood_set(element.get_name(), str(element.value))
333
            else:
334
                LOG.error("Unknown type %s" % element.value)
335

    
336

    
337
class KenwoodOldLiveRadio(KenwoodLiveRadio):
338
    _kenwood_valid_tones = list(chirp_common.OLD_TONES)
339

    
340
    def set_memory(self, memory):
341
        supported_tones = list(chirp_common.OLD_TONES)
342
        supported_tones.remove(69.3)
343
        if memory.rtone not in supported_tones:
344
            raise errors.UnsupportedToneError("This radio does not support " +
345
                                              "tone %.1fHz" % memory.rtone)
346
        if memory.ctone not in supported_tones:
347
            raise errors.UnsupportedToneError("This radio does not support " +
348
                                              "tone %.1fHz" % memory.ctone)
349

    
350
        return KenwoodLiveRadio.set_memory(self, memory)
351

    
352

    
353
@directory.register
354
class THD7Radio(KenwoodOldLiveRadio):
355
    """Kenwood TH-D7"""
356
    MODEL = "TH-D7"
357

    
358
    _kenwood_split = True
359

    
360
    _SETTINGS_OPTIONS = {
361
        "BAL": ["4:0", "3:1", "2:2", "1:3", "0:4"],
362
        "BEP": ["Off", "Key", "Key+Data", "All"],
363
        "BEPT": ["Off", "Mine", "All New"],  # D700 has fourth "All"
364
        "DS": ["Data Band", "Both Bands"],
365
        "DTB": ["A", "B"],
366
        "DTBA": ["A", "B", "A:TX/B:RX"],  # D700 has fourth A:RX/B:TX
367
        "DTX": ["Manual", "PTT", "Auto"],
368
        "ICO": ["Kenwood", "Runner", "House", "Tent", "Boat", "SSTV",
369
                "Plane", "Speedboat", "Car", "Bicycle"],
370
        "MNF": ["Name", "Frequency"],
371
        "PKSA": ["1200", "9600"],
372
        "POSC": ["Off Duty", "Enroute", "In Service", "Returning",
373
                 "Committed", "Special", "Priority", "Emergency"],
374
        "PT": ["100ms", "200ms", "500ms", "750ms",
375
               "1000ms", "1500ms", "2000ms"],
376
        "SCR": ["Time", "Carrier", "Seek"],
377
        "SV": ["Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s",
378
               "2s", "3s", "4s", "5s"],
379
        "TEMP": ["F", "C"],
380
        "TXI": ["30sec", "1min", "2min", "3min", "4min", "5min",
381
                "10min", "20min", "30min"],
382
        "UNIT": ["English", "Metric"],
383
        "WAY": ["Off", "6 digit NMEA", "7 digit NMEA", "8 digit NMEA",
384
                "9 digit NMEA", "6 digit Magellan", "DGPS"],
385
    }
386

    
387
    def get_features(self):
388
        rf = chirp_common.RadioFeatures()
389
        rf.has_settings = True
390
        rf.has_dtcs = False
391
        rf.has_dtcs_polarity = False
392
        rf.has_bank = False
393
        rf.has_mode = True
394
        rf.has_tuning_step = False
395
        rf.can_odd_split = True
396
        rf.valid_duplexes = ["", "-", "+", "split"]
397
        rf.valid_modes = MODES.values()
398
        rf.valid_tmodes = ["", "Tone", "TSQL"]
399
        rf.valid_characters = \
400
            chirp_common.CHARSET_ALPHANUMERIC + "/.-+*)('&%$#! ~}|{"
401
        rf.valid_name_length = 7
402
        rf.memory_bounds = (1, self._upper)
403
        return rf
404

    
405
    def _make_mem_spec(self, mem):
406
        if mem.duplex in " -+":
407
            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
408
            offset = mem.offset
409
        else:
410
            duplex = 0
411
            offset = 0
412

    
413
        spec = (
414
            "%011i" % mem.freq,
415
            "%X" % STEPS.index(mem.tuning_step),
416
            "%i" % duplex,
417
            "0",
418
            "%i" % (mem.tmode == "Tone"),
419
            "%i" % (mem.tmode == "TSQL"),
420
            "",  # DCS Flag
421
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
422
            "",  # DCS Code
423
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
424
            "%09i" % offset,
425
            "%i" % util.get_dict_rev(MODES, mem.mode),
426
            "%i" % ((mem.skip == "S") and 1 or 0))
427

    
428
        return spec
429

    
430
    def _parse_mem_spec(self, spec):
431
        mem = chirp_common.Memory()
432

    
433
        mem.number = int(spec[2])
434
        mem.freq = int(spec[3], 10)
435
        mem.tuning_step = STEPS[int(spec[4], 16)]
436
        mem.duplex = DUPLEX[int(spec[5])]
437
        mem.tmode = get_tmode(spec[7], spec[8], spec[9])
438
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
439
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
440
        if spec[11] and spec[11].isdigit():
441
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
442
        else:
443
            LOG.warn("Unknown or invalid DCS: %s" % spec[11])
444
        if spec[13]:
445
            mem.offset = int(spec[13])
446
        else:
447
            mem.offset = 0
448
        mem.mode = MODES[int(spec[14])]
449
        mem.skip = int(spec[15]) and "S" or ""
450

    
451
        return mem
452

    
453
    def get_settings(self):
454
        main = RadioSettingGroup("main", "Main")
455
        aux = RadioSettingGroup("aux", "Aux")
456
        tnc = RadioSettingGroup("tnc", "TNC")
457
        save = RadioSettingGroup("save", "Save")
458
        display = RadioSettingGroup("display", "Display")
459
        dtmf = RadioSettingGroup("dtmf", "DTMF")
460
        radio = RadioSettingGroup("radio", "Radio",
461
                                  aux, tnc, save, display, dtmf)
462
        sky = RadioSettingGroup("sky", "SkyCommand")
463
        aprs = RadioSettingGroup("aprs", "APRS")
464

    
465
        top = RadioSettings(main, radio, aprs, sky)
466

    
467
        bools = [("AMR", aprs, "APRS Message Auto-Reply"),
468
                 ("AIP", aux, "Advanced Intercept Point"),
469
                 ("ARO", aux, "Automatic Repeater Offset"),
470
                 ("BCN", aprs, "Beacon"),
471
                 ("CH", radio, "Channel Mode Display"),
472
                 # ("DIG", aprs, "APRS Digipeater"),
473
                 ("DL", main, "Dual"),
474
                 ("LK", main, "Lock"),
475
                 ("LMP", main, "Lamp"),
476
                 ("TSP", dtmf, "DTMF Fast Transmission"),
477
                 ("TXH", dtmf, "TX Hold"),
478
                 ]
479

    
480
        for setting, group, name in bools:
481
            value = self._kenwood_get_bool(setting)
482
            rs = RadioSetting(setting, name,
483
                              RadioSettingValueBoolean(value))
484
            group.append(rs)
485

    
486
        lists = [("BAL", main, "Balance"),
487
                 ("BEP", aux, "Beep"),
488
                 ("BEPT", aprs, "APRS Beep"),
489
                 ("DS", tnc, "Data Sense"),
490
                 ("DTB", tnc, "Data Band"),
491
                 ("DTBA", aprs, "APRS Data Band"),
492
                 ("DTX", aprs, "APRS Data TX"),
493
                 # ("ICO", aprs, "APRS Icon"),
494
                 ("MNF", main, "Memory Display Mode"),
495
                 ("PKSA", aprs, "APRS Packet Speed"),
496
                 ("POSC", aprs, "APRS Position Comment"),
497
                 ("PT", dtmf, "DTMF Speed"),
498
                 ("SV", save, "Battery Save"),
499
                 ("TEMP", aprs, "APRS Temperature Units"),
500
                 ("TXI", aprs, "APRS Transmit Interval"),
501
                 # ("UNIT", aprs, "APRS Display Units"),
502
                 ("WAY", aprs, "Waypoint Mode"),
503
                 ]
504

    
505
        for setting, group, name in lists:
506
            value = self._kenwood_get_int(setting)
507
            options = self._SETTINGS_OPTIONS[setting]
508
            rs = RadioSetting(setting, name,
509
                              RadioSettingValueList(options,
510
                                                    options[value]))
511
            group.append(rs)
512

    
513
        ints = [("CNT", display, "Contrast", 1, 16),
514
                ]
515
        for setting, group, name, minv, maxv in ints:
516
            value = self._kenwood_get_int(setting)
517
            rs = RadioSetting(setting, name,
518
                              RadioSettingValueInteger(minv, maxv, value))
519
            group.append(rs)
520

    
521
        strings = [("MES", display, "Power-on Message", 8),
522
                   ("MYC", aprs, "APRS Callsign", 8),
523
                   ("PP", aprs, "APRS Path", 32),
524
                   ("SCC", sky, "SkyCommand Callsign", 8),
525
                   ("SCT", sky, "SkyCommand To Callsign", 8),
526
                   # ("STAT", aprs, "APRS Status Text", 32),
527
                   ]
528
        for setting, group, name, length in strings:
529
            _cmd, value = self._kenwood_get(setting)
530
            rs = RadioSetting(setting, name,
531
                              RadioSettingValueString(0, length, value))
532
            group.append(rs)
533

    
534
        return top
535

    
536

    
537
@directory.register
538
class THD7GRadio(THD7Radio):
539
    """Kenwood TH-D7G"""
540
    MODEL = "TH-D7G"
541

    
542
    def get_features(self):
543
        rf = super(THD7GRadio, self).get_features()
544
        rf.valid_name_length = 8
545
        return rf
546

    
547

    
548
@directory.register
549
class TMD700Radio(KenwoodOldLiveRadio):
550
    """Kenwood TH-D700"""
551
    MODEL = "TM-D700"
552

    
553
    _kenwood_split = True
554

    
555
    def get_features(self):
556
        rf = chirp_common.RadioFeatures()
557
        rf.has_dtcs = True
558
        rf.has_dtcs_polarity = False
559
        rf.has_bank = False
560
        rf.has_mode = False
561
        rf.has_tuning_step = False
562
        rf.can_odd_split = True
563
        rf.valid_duplexes = ["", "-", "+", "split"]
564
        rf.valid_modes = ["FM"]
565
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
566
        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
567
        rf.valid_name_length = 8
568
        rf.memory_bounds = (1, self._upper)
569
        return rf
570

    
571
    def _make_mem_spec(self, mem):
572
        if mem.duplex in " -+":
573
            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
574
        else:
575
            duplex = 0
576
        spec = (
577
            "%011i" % mem.freq,
578
            "%X" % STEPS.index(mem.tuning_step),
579
            "%i" % duplex,
580
            "0",
581
            "%i" % (mem.tmode == "Tone"),
582
            "%i" % (mem.tmode == "TSQL"),
583
            "%i" % (mem.tmode == "DTCS"),
584
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
585
            "%03i0" % (chirp_common.DTCS_CODES.index(mem.dtcs) + 1),
586
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
587
            "%09i" % mem.offset,
588
            "%i" % util.get_dict_rev(MODES, mem.mode),
589
            "%i" % ((mem.skip == "S") and 1 or 0))
590

    
591
        return spec
592

    
593
    def _parse_mem_spec(self, spec):
594
        mem = chirp_common.Memory()
595

    
596
        mem.number = int(spec[2])
597
        mem.freq = int(spec[3])
598
        mem.tuning_step = STEPS[int(spec[4], 16)]
599
        mem.duplex = DUPLEX[int(spec[5])]
600
        mem.tmode = get_tmode(spec[7], spec[8], spec[9])
601
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
602
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
603
        if spec[11] and spec[11].isdigit():
604
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
605
        else:
606
            LOG.warn("Unknown or invalid DCS: %s" % spec[11])
607
        if spec[13]:
608
            mem.offset = int(spec[13])
609
        else:
610
            mem.offset = 0
611
        mem.mode = MODES[int(spec[14])]
612
        mem.skip = int(spec[15]) and "S" or ""
613

    
614
        return mem
615

    
616

    
617
@directory.register
618
class TMV7Radio(KenwoodOldLiveRadio):
619
    """Kenwood TM-V7"""
620
    MODEL = "TM-V7"
621

    
622
    mem_upper_limit = 200  # Will be updated
623

    
624
    def get_features(self):
625
        rf = chirp_common.RadioFeatures()
626
        rf.has_dtcs = False
627
        rf.has_dtcs_polarity = False
628
        rf.has_bank = False
629
        rf.has_mode = False
630
        rf.has_tuning_step = False
631
        rf.valid_modes = ["FM"]
632
        rf.valid_tmodes = ["", "Tone", "TSQL"]
633
        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
634
        rf.valid_name_length = 7
635
        rf.has_sub_devices = True
636
        rf.memory_bounds = (1, self._upper)
637
        return rf
638

    
639
    def _make_mem_spec(self, mem):
640
        spec = (
641
            "%011i" % mem.freq,
642
            "%X" % STEPS.index(mem.tuning_step),
643
            "%i" % util.get_dict_rev(DUPLEX, mem.duplex),
644
            "0",
645
            "%i" % (mem.tmode == "Tone"),
646
            "%i" % (mem.tmode == "TSQL"),
647
            "0",
648
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
649
            "000",
650
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
651
            "",
652
            "0")
653

    
654
        return spec
655

    
656
    def _parse_mem_spec(self, spec):
657
        mem = chirp_common.Memory()
658
        mem.number = int(spec[2])
659
        mem.freq = int(spec[3])
660
        mem.tuning_step = STEPS[int(spec[4], 16)]
661
        mem.duplex = DUPLEX[int(spec[5])]
662
        if int(spec[7]):
663
            mem.tmode = "Tone"
664
        elif int(spec[8]):
665
            mem.tmode = "TSQL"
666
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
667
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
668

    
669
        return mem
670

    
671
    def get_sub_devices(self):
672
        return [TMV7RadioVHF(self.pipe), TMV7RadioUHF(self.pipe)]
673

    
674
    def __test_location(self, loc):
675
        mem = self.get_memory(loc)
676
        if not mem.empty:
677
            # Memory was not empty, must be valid
678
            return True
679

    
680
        # Mem was empty (or invalid), try to set it
681
        if self._vfo == 0:
682
            mem.freq = 144000000
683
        else:
684
            mem.freq = 440000000
685
        mem.empty = False
686
        try:
687
            self.set_memory(mem)
688
        except Exception:
689
            # Failed, so we're past the limit
690
            return False
691

    
692
        # Erase what we did
693
        try:
694
            self.erase_memory(loc)
695
        except Exception:
696
            pass  # V7A Can't delete just yet
697

    
698
        return True
699

    
700
    def _detect_split(self):
701
        return 50
702

    
703

    
704
class TMV7RadioSub(TMV7Radio):
705
    """Base class for the TM-V7 sub devices"""
706
    def __init__(self, pipe):
707
        TMV7Radio.__init__(self, pipe)
708
        self._detect_split()
709

    
710

    
711
class TMV7RadioVHF(TMV7RadioSub):
712
    """TM-V7 VHF subdevice"""
713
    VARIANT = "VHF"
714
    _vfo = 0
715

    
716

    
717
class TMV7RadioUHF(TMV7RadioSub):
718
    """TM-V7 UHF subdevice"""
719
    VARIANT = "UHF"
720
    _vfo = 1
721

    
722

    
723
@directory.register
724
class TMG707Radio(TMV7Radio):
725
    """Kenwood TM-G707"""
726
    MODEL = "TM-G707"
727

    
728
    def get_features(self):
729
        rf = TMV7Radio.get_features(self)
730
        rf.has_sub_devices = False
731
        rf.memory_bounds = (1, 180)
732
        rf.valid_bands = [(118000000, 174000000),
733
                          (300000000, 520000000),
734
                          (800000000, 999000000)]
735
        return rf
736

    
737

    
738
THG71_STEPS = [5, 6.25, 10, 12.5, 15, 20, 25, 30, 50, 100]
739

    
740

    
741
@directory.register
742
class THG71Radio(TMV7Radio):
743
    """Kenwood TH-G71"""
744
    MODEL = "TH-G71"
745

    
746
    def get_features(self):
747
        rf = TMV7Radio.get_features(self)
748
        rf.has_tuning_step = True
749
        rf.valid_tuning_steps = list(THG71_STEPS)
750
        rf.valid_name_length = 6
751
        rf.has_sub_devices = False
752
        rf.valid_bands = [(118000000, 174000000),
753
                          (320000000, 470000000),
754
                          (800000000, 945000000)]
755
        return rf
756

    
757
    def _make_mem_spec(self, mem):
758
        spec = (
759
            "%011i" % mem.freq,
760
            "%X" % THG71_STEPS.index(mem.tuning_step),
761
            "%i" % util.get_dict_rev(DUPLEX, mem.duplex),
762
            "0",
763
            "%i" % (mem.tmode == "Tone"),
764
            "%i" % (mem.tmode == "TSQL"),
765
            "0",
766
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
767
            "000",
768
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
769
            "%09i" % mem.offset,
770
            "%i" % ((mem.skip == "S") and 1 or 0))
771
        return spec
772

    
773
    def _parse_mem_spec(self, spec):
774
        mem = chirp_common.Memory()
775
        mem.number = int(spec[2])
776
        mem.freq = int(spec[3])
777
        mem.tuning_step = THG71_STEPS[int(spec[4], 16)]
778
        mem.duplex = DUPLEX[int(spec[5])]
779
        if int(spec[7]):
780
            mem.tmode = "Tone"
781
        elif int(spec[8]):
782
            mem.tmode = "TSQL"
783
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
784
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
785
        if spec[13]:
786
            mem.offset = int(spec[13])
787
        else:
788
            mem.offset = 0
789
        return mem
790

    
791

    
792
THF6A_STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
793
               100.0]
794

    
795
THF6A_DUPLEX = dict(DUPLEX)
796
THF6A_DUPLEX[3] = "split"
797

    
798

    
799
@directory.register
800
class THF6ARadio(KenwoodLiveRadio):
801
    """Kenwood TH-F6"""
802
    MODEL = "TH-F6"
803

    
804
    _upper = 399
805
    _kenwood_split = True
806
    _kenwood_valid_tones = list(KENWOOD_TONES)
807

    
808
    def get_features(self):
809
        rf = chirp_common.RadioFeatures()
810
        rf.has_dtcs_polarity = False
811
        rf.has_bank = False
812
        rf.can_odd_split = True
813
        rf.valid_modes = list(THF6_MODES)
814
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
815
        rf.valid_tuning_steps = list(THF6A_STEPS)
816
        rf.valid_bands = [(1000, 1300000000)]
817
        rf.valid_skips = ["", "S"]
818
        rf.valid_duplexes = THF6A_DUPLEX.values()
819
        rf.valid_characters = chirp_common.CHARSET_ASCII
820
        rf.valid_name_length = 8
821
        rf.memory_bounds = (0, self._upper)
822
        rf.has_settings = True
823
        return rf
824

    
825
    def _cmd_set_memory(self, number, spec):
826
        if spec:
827
            spec = "," + spec
828
        return "MW", "0,%03i%s" % (number, spec)
829

    
830
    def _cmd_get_memory(self, number):
831
        return "MR", "0,%03i" % number
832

    
833
    def _cmd_get_memory_name(self, number):
834
        return "MNA", "%03i" % number
835

    
836
    def _cmd_set_memory_name(self, number, name):
837
        return "MNA", "%03i,%s" % (number, name)
838

    
839
    def _cmd_get_split(self, number):
840
        return "MR", "1,%03i" % number
841

    
842
    def _cmd_set_split(self, number, spec):
843
        return "MW", "1,%03i,%s" % (number, spec)
844

    
845
    def _parse_mem_spec(self, spec):
846
        mem = chirp_common.Memory()
847

    
848
        mem.number = int(spec[1])
849
        mem.freq = int(spec[2])
850
        mem.tuning_step = THF6A_STEPS[int(spec[3], 16)]
851
        mem.duplex = THF6A_DUPLEX[int(spec[4])]
852
        mem.tmode = get_tmode(spec[6], spec[7], spec[8])
853
        mem.rtone = self._kenwood_valid_tones[int(spec[9])]
854
        mem.ctone = self._kenwood_valid_tones[int(spec[10])]
855
        if spec[11] and spec[11].isdigit():
856
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
857
        else:
858
            LOG.warn("Unknown or invalid DCS: %s" % spec[11])
859
        if spec[12]:
860
            mem.offset = int(spec[12])
861
        else:
862
            mem.offset = 0
863
        mem.mode = THF6_MODES[int(spec[13])]
864
        if spec[14] == "1":
865
            mem.skip = "S"
866

    
867
        return mem
868

    
869
    def _make_mem_spec(self, mem):
870
        if mem.duplex in " +-":
871
            duplex = util.get_dict_rev(THF6A_DUPLEX, mem.duplex)
872
            offset = mem.offset
873
        elif mem.duplex == "split":
874
            duplex = 0
875
            offset = 0
876
        else:
877
            LOG.warn("Bug: unsupported duplex `%s'" % mem.duplex)
878
        spec = (
879
            "%011i" % mem.freq,
880
            "%X" % THF6A_STEPS.index(mem.tuning_step),
881
            "%i" % duplex,
882
            "0",
883
            "%i" % (mem.tmode == "Tone"),
884
            "%i" % (mem.tmode == "TSQL"),
885
            "%i" % (mem.tmode == "DTCS"),
886
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
887
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
888
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
889
            "%09i" % offset,
890
            "%i" % (THF6_MODES.index(mem.mode)),
891
            "%i" % (mem.skip == "S"))
892

    
893
        return spec
894

    
895
    _SETTINGS_OPTIONS = {
896
        "APO": ["Off", "30min", "60min"],
897
        "BAL": ["100%:0%", "75%:25%", "50%:50%", "25%:75%", "%0:100%"],
898
        "BAT": ["Lithium", "Alkaline"],
899
        "CKEY": ["Call", "1750Hz"],
900
        "DATP": ["1200bps", "9600bps"],
901
        "LAN": ["English", "Japanese"],
902
        "MNF": ["Name", "Frequency"],
903
        "MRM": ["All Band", "Current Band"],
904
        "PT": ["100ms", "250ms", "500ms", "750ms",
905
               "1000ms", "1500ms", "2000ms"],
906
        "SCR": ["Time", "Carrier", "Seek"],
907
        "SV": ["Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s",
908
               "2s", "3s", "4s", "5s"],
909
        "VXD": ["250ms", "500ms", "750ms", "1s", "1.5s", "2s", "3s"],
910
    }
911

    
912
    def get_settings(self):
913
        main = RadioSettingGroup("main", "Main")
914
        aux = RadioSettingGroup("aux", "Aux")
915
        save = RadioSettingGroup("save", "Save")
916
        display = RadioSettingGroup("display", "Display")
917
        dtmf = RadioSettingGroup("dtmf", "DTMF")
918
        top = RadioSettings(main, aux, save, display, dtmf)
919

    
920
        lists = [("APO", save, "Automatic Power Off"),
921
                 ("BAL", main, "Balance"),
922
                 ("BAT", save, "Battery Type"),
923
                 ("CKEY", aux, "CALL Key Set Up"),
924
                 ("DATP", aux, "Data Packet Speed"),
925
                 ("LAN", display, "Language"),
926
                 ("MNF", main, "Memory Display Mode"),
927
                 ("MRM", main, "Memory Recall Method"),
928
                 ("PT", dtmf, "DTMF Speed"),
929
                 ("SCR", main, "Scan Resume"),
930
                 ("SV", save, "Battery Save"),
931
                 ("VXD", aux, "VOX Drop Delay"),
932
                 ]
933

    
934
        bools = [("ANT", aux, "Bar Antenna"),
935
                 ("ATT", main, "Attenuator Enabled"),
936
                 ("ARO", main, "Automatic Repeater Offset"),
937
                 ("BEP", aux, "Beep for keypad"),
938
                 ("DL", main, "Dual"),
939
                 ("DLK", dtmf, "DTMF Lockout On Transmit"),
940
                 ("ELK", aux, "Enable Locked Tuning"),
941
                 ("LK", main, "Lock"),
942
                 ("LMP", display, "Lamp"),
943
                 ("NSFT", aux, "Noise Shift"),
944
                 ("TH", aux, "Tx Hold for 1750"),
945
                 ("TSP", dtmf, "DTMF Fast Transmission"),
946
                 ("TXH", dtmf, "TX Hold DTMF"),
947
                 ("TXS", main, "Transmit Inhibit"),
948
                 ("VOX", aux, "VOX Enable"),
949
                 ("VXB", aux, "VOX On Busy"),
950
                 ]
951

    
952
        ints = [("CNT", display, "Contrast", 1, 16),
953
                ("VXG", aux, "VOX Gain", 0, 9),
954
                ]
955

    
956
        strings = [("MES", display, "Power-on Message", 8),
957
                   ]
958

    
959
        for setting, group, name in bools:
960
            value = self._kenwood_get_bool(setting)
961
            rs = RadioSetting(setting, name,
962
                              RadioSettingValueBoolean(value))
963
            group.append(rs)
964

    
965
        for setting, group, name in lists:
966
            value = self._kenwood_get_int(setting)
967
            options = self._SETTINGS_OPTIONS[setting]
968
            rs = RadioSetting(setting, name,
969
                              RadioSettingValueList(options,
970
                                                    options[value]))
971
            group.append(rs)
972

    
973
        for setting, group, name, minv, maxv in ints:
974
            value = self._kenwood_get_int(setting)
975
            rs = RadioSetting(setting, name,
976
                              RadioSettingValueInteger(minv, maxv, value))
977
            group.append(rs)
978

    
979
        for setting, group, name, length in strings:
980
            _cmd, value = self._kenwood_get(setting)
981
            rs = RadioSetting(setting, name,
982
                              RadioSettingValueString(0, length, value))
983
            group.append(rs)
984

    
985
        return top
986

    
987

    
988
@directory.register
989
class THF7ERadio(THF6ARadio):
990
    """Kenwood TH-F7"""
991
    MODEL = "TH-F7"
992

    
993
D710_DUPLEX = ["", "+", "-", "split"]
994
D710_MODES = ["FM", "NFM", "AM"]
995
D710_SKIP = ["", "S"]
996
D710_STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
997

    
998

    
999
@directory.register
1000
class TMD710Radio(KenwoodLiveRadio):
1001
    """Kenwood TM-D710"""
1002
    MODEL = "TM-D710"
1003

    
1004
    _upper = 999
1005
    _kenwood_valid_tones = list(KENWOOD_TONES)
1006

    
1007
    def get_features(self):
1008
        rf = chirp_common.RadioFeatures()
1009
        rf.can_odd_split = True
1010
        rf.has_dtcs_polarity = False
1011
        rf.has_bank = False
1012
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1013
        rf.valid_modes = D710_MODES
1014
        rf.valid_duplexes = D710_DUPLEX
1015
        rf.valid_tuning_steps = D710_STEPS
1016
        rf.valid_characters = chirp_common.CHARSET_ASCII.replace(',', '')
1017
        rf.valid_name_length = 8
1018
        rf.valid_skips = D710_SKIP
1019
        rf.memory_bounds = (0, 999)
1020
        return rf
1021

    
1022
    def _cmd_get_memory(self, number):
1023
        return "ME", "%03i" % number
1024

    
1025
    def _cmd_get_memory_name(self, number):
1026
        return "MN", "%03i" % number
1027

    
1028
    def _cmd_set_memory(self, number, spec):
1029
        return "ME", "%03i,%s" % (number, spec)
1030

    
1031
    def _cmd_set_memory_name(self, number, name):
1032
        return "MN", "%03i,%s" % (number, name)
1033

    
1034
    def _parse_mem_spec(self, spec):
1035
        mem = chirp_common.Memory()
1036

    
1037
        mem.number = int(spec[0])
1038
        mem.freq = int(spec[1])
1039
        mem.tuning_step = D710_STEPS[int(spec[2], 16)]
1040
        mem.duplex = D710_DUPLEX[int(spec[3])]
1041
        # Reverse
1042
        if int(spec[5]):
1043
            mem.tmode = "Tone"
1044
        elif int(spec[6]):
1045
            mem.tmode = "TSQL"
1046
        elif int(spec[7]):
1047
            mem.tmode = "DTCS"
1048
        mem.rtone = self._kenwood_valid_tones[int(spec[8])]
1049
        mem.ctone = self._kenwood_valid_tones[int(spec[9])]
1050
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
1051
        mem.offset = int(spec[11])
1052
        mem.mode = D710_MODES[int(spec[12])]
1053
        # TX Frequency
1054
        if int(spec[13]):
1055
            mem.duplex = "split"
1056
            mem.offset = int(spec[13])
1057
        # Unknown
1058
        mem.skip = D710_SKIP[int(spec[15])]  # Memory Lockout
1059

    
1060
        return mem
1061

    
1062
    def _make_mem_spec(self, mem):
1063
        spec = (
1064
            "%010i" % mem.freq,
1065
            "%X" % D710_STEPS.index(mem.tuning_step),
1066
            "%i" % (0 if mem.duplex == "split"
1067
                    else D710_DUPLEX.index(mem.duplex)),
1068
            "0",  # Reverse
1069
            "%i" % (mem.tmode == "Tone" and 1 or 0),
1070
            "%i" % (mem.tmode == "TSQL" and 1 or 0),
1071
            "%i" % (mem.tmode == "DTCS" and 1 or 0),
1072
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
1073
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
1074
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
1075
            "%08i" % (0 if mem.duplex == "split" else mem.offset),  # Offset
1076
            "%i" % D710_MODES.index(mem.mode),
1077
            "%010i" % (mem.offset if mem.duplex == "split" else 0),  # TX Freq
1078
            "0",  # Unknown
1079
            "%i" % D710_SKIP.index(mem.skip),  # Memory Lockout
1080
            )
1081

    
1082
        return spec
1083

    
1084

    
1085
@directory.register
1086
class THD72Radio(TMD710Radio):
1087
    """Kenwood TH-D72"""
1088
    MODEL = "TH-D72 (live mode)"
1089
    HARDWARE_FLOW = sys.platform == "darwin"  # only OS X driver needs hw flow
1090

    
1091
    def _parse_mem_spec(self, spec):
1092
        mem = chirp_common.Memory()
1093

    
1094
        mem.number = int(spec[0])
1095
        mem.freq = int(spec[1])
1096
        mem.tuning_step = D710_STEPS[int(spec[2], 16)]
1097
        mem.duplex = D710_DUPLEX[int(spec[3])]
1098
        # Reverse
1099
        if int(spec[5]):
1100
            mem.tmode = "Tone"
1101
        elif int(spec[6]):
1102
            mem.tmode = "TSQL"
1103
        elif int(spec[7]):
1104
            mem.tmode = "DTCS"
1105
        mem.rtone = self._kenwood_valid_tones[int(spec[9])]
1106
        mem.ctone = self._kenwood_valid_tones[int(spec[10])]
1107
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
1108
        mem.offset = int(spec[13])
1109
        mem.mode = D710_MODES[int(spec[14])]
1110
        # TX Frequency
1111
        if int(spec[15]):
1112
            mem.duplex = "split"
1113
            mem.offset = int(spec[15])
1114
        # Lockout
1115
        mem.skip = D710_SKIP[int(spec[17])]  # Memory Lockout
1116

    
1117
        return mem
1118

    
1119
    def _make_mem_spec(self, mem):
1120
        spec = (
1121
            "%010i" % mem.freq,
1122
            "%X" % D710_STEPS.index(mem.tuning_step),
1123
            "%i" % (0 if mem.duplex == "split"
1124
                    else D710_DUPLEX.index(mem.duplex)),
1125
            "0",  # Reverse
1126
            "%i" % (mem.tmode == "Tone" and 1 or 0),
1127
            "%i" % (mem.tmode == "TSQL" and 1 or 0),
1128
            "%i" % (mem.tmode == "DTCS" and 1 or 0),
1129
            "0",
1130
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
1131
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
1132
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
1133
            "0",
1134
            "%08i" % (0 if mem.duplex == "split" else mem.offset),  # Offset
1135
            "%i" % D710_MODES.index(mem.mode),
1136
            "%010i" % (mem.offset if mem.duplex == "split" else 0),  # TX Freq
1137
            "0",  # Unknown
1138
            "%i" % D710_SKIP.index(mem.skip),  # Memory Lockout
1139
            )
1140

    
1141
        return spec
1142

    
1143

    
1144
@directory.register
1145
class THD74Radio(THD72Radio):
1146
    """Kenwood TH-D74"""
1147
    MODEL = "TH-D74 (live mode)"
1148

    
1149

    
1150
@directory.register
1151
class TMV71Radio(TMD710Radio):
1152
    """Kenwood TM-V71"""
1153
    MODEL = "TM-V71"
1154

    
1155

    
1156
@directory.register
1157
class TMD710GRadio(TMD710Radio):
1158
    """Kenwood TM-D710G"""
1159
    MODEL = "TM-D710G"
1160

    
1161
    @classmethod
1162
    def get_prompts(cls):
1163
        rp = chirp_common.RadioPrompts()
1164
        rp.experimental = ("This radio driver is currently under development, "
1165
                           "and supports the same features as the TM-D710A/E. "
1166
                           "There are no known issues with it, but you should "
1167
                           "proceed with caution.")
1168
        return rp
1169

    
1170

    
1171
THK2_DUPLEX = ["", "+", "-"]
1172
THK2_MODES = ["FM", "NFM"]
1173

    
1174
THK2_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/"
1175

    
1176

    
1177
@directory.register
1178
class THK2Radio(KenwoodLiveRadio):
1179
    """Kenwood TH-K2"""
1180
    MODEL = "TH-K2"
1181

    
1182
    _kenwood_valid_tones = list(KENWOOD_TONES)
1183

    
1184
    def get_features(self):
1185
        rf = chirp_common.RadioFeatures()
1186
        rf.can_odd_split = False
1187
        rf.has_dtcs_polarity = False
1188
        rf.has_bank = False
1189
        rf.has_tuning_step = False
1190
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1191
        rf.valid_modes = THK2_MODES
1192
        rf.valid_duplexes = THK2_DUPLEX
1193
        rf.valid_characters = THK2_CHARS
1194
        rf.valid_name_length = 6
1195
        rf.valid_bands = [(136000000, 173990000)]
1196
        rf.valid_skips = ["", "S"]
1197
        rf.valid_tuning_steps = [5.0]
1198
        rf.memory_bounds = (0, 49)
1199
        return rf
1200

    
1201
    def _cmd_get_memory(self, number):
1202
        return "ME", "%02i" % number
1203

    
1204
    def _cmd_get_memory_name(self, number):
1205
        return "MN", "%02i" % number
1206

    
1207
    def _cmd_set_memory(self, number, spec):
1208
        return "ME", "%02i,%s" % (number, spec)
1209

    
1210
    def _cmd_set_memory_name(self, number, name):
1211
        return "MN", "%02i,%s" % (number, name)
1212

    
1213
    def _parse_mem_spec(self, spec):
1214
        mem = chirp_common.Memory()
1215

    
1216
        mem.number = int(spec[0])
1217
        mem.freq = int(spec[1])
1218
        # mem.tuning_step =
1219
        mem.duplex = THK2_DUPLEX[int(spec[3])]
1220
        if int(spec[5]):
1221
            mem.tmode = "Tone"
1222
        elif int(spec[6]):
1223
            mem.tmode = "TSQL"
1224
        elif int(spec[7]):
1225
            mem.tmode = "DTCS"
1226
        mem.rtone = self._kenwood_valid_tones[int(spec[8])]
1227
        mem.ctone = self._kenwood_valid_tones[int(spec[9])]
1228
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
1229
        mem.offset = int(spec[11])
1230
        mem.mode = THK2_MODES[int(spec[12])]
1231
        mem.skip = int(spec[16]) and "S" or ""
1232
        return mem
1233

    
1234
    def _make_mem_spec(self, mem):
1235
        try:
1236
            rti = self._kenwood_valid_tones.index(mem.rtone)
1237
            cti = self._kenwood_valid_tones.index(mem.ctone)
1238
        except ValueError:
1239
            raise errors.UnsupportedToneError()
1240

    
1241
        spec = (
1242
            "%010i" % mem.freq,
1243
            "0",
1244
            "%i" % THK2_DUPLEX.index(mem.duplex),
1245
            "0",
1246
            "%i" % int(mem.tmode == "Tone"),
1247
            "%i" % int(mem.tmode == "TSQL"),
1248
            "%i" % int(mem.tmode == "DTCS"),
1249
            "%02i" % rti,
1250
            "%02i" % cti,
1251
            "%03i" % chirp_common.DTCS_CODES.index(mem.dtcs),
1252
            "%08i" % mem.offset,
1253
            "%i" % THK2_MODES.index(mem.mode),
1254
            "0",
1255
            "%010i" % 0,
1256
            "0",
1257
            "%i" % int(mem.skip == "S")
1258
            )
1259
        return spec
1260

    
1261

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

    
1264

    
1265
@directory.register
1266
class TM271Radio(THK2Radio):
1267
    """Kenwood TM-271"""
1268
    MODEL = "TM-271"
1269

    
1270
    def get_features(self):
1271
        rf = chirp_common.RadioFeatures()
1272
        rf.can_odd_split = False
1273
        rf.has_dtcs_polarity = False
1274
        rf.has_bank = False
1275
        rf.has_tuning_step = False
1276
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1277
        rf.valid_modes = THK2_MODES
1278
        rf.valid_duplexes = THK2_DUPLEX
1279
        rf.valid_characters = THK2_CHARS
1280
        rf.valid_name_length = 6
1281
        rf.valid_bands = [(137000000, 173990000)]
1282
        rf.valid_skips = ["", "S"]
1283
        rf.valid_tuning_steps = list(TM271_STEPS)
1284
        rf.memory_bounds = (0, 99)
1285
        return rf
1286

    
1287
    def _cmd_get_memory(self, number):
1288
        return "ME", "%03i" % number
1289

    
1290
    def _cmd_get_memory_name(self, number):
1291
        return "MN", "%03i" % number
1292

    
1293
    def _cmd_set_memory(self, number, spec):
1294
        return "ME", "%03i,%s" % (number, spec)
1295

    
1296
    def _cmd_set_memory_name(self, number, name):
1297
        return "MN", "%03i,%s" % (number, name)
1298

    
1299

    
1300
@directory.register
1301
class TM281Radio(TM271Radio):
1302
    """Kenwood TM-281"""
1303
    MODEL = "TM-281"
1304
    # seems that this is a perfect clone of TM271 with just a different model
1305

    
1306

    
1307
@directory.register
1308
class TM471Radio(THK2Radio):
1309
    """Kenwood TM-471"""
1310
    MODEL = "TM-471"
1311

    
1312
    def get_features(self):
1313
        rf = chirp_common.RadioFeatures()
1314
        rf.can_odd_split = False
1315
        rf.has_dtcs_polarity = False
1316
        rf.has_bank = False
1317
        rf.has_tuning_step = False
1318
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1319
        rf.valid_modes = THK2_MODES
1320
        rf.valid_duplexes = THK2_DUPLEX
1321
        rf.valid_characters = THK2_CHARS
1322
        rf.valid_name_length = 6
1323
        rf.valid_bands = [(444000000, 479990000)]
1324
        rf.valid_skips = ["", "S"]
1325
        rf.valid_tuning_steps = [5.0]
1326
        rf.memory_bounds = (0, 99)
1327
        return rf
1328

    
1329
    def _cmd_get_memory(self, number):
1330
        return "ME", "%03i" % number
1331

    
1332
    def _cmd_get_memory_name(self, number):
1333
        return "MN", "%03i" % number
1334

    
1335
    def _cmd_set_memory(self, number, spec):
1336
        return "ME", "%03i,%s" % (number, spec)
1337

    
1338
    def _cmd_set_memory_name(self, number, name):
1339
        return "MN", "%03i,%s" % (number, name)