Project

General

Profile

New Model #217 » kenwood_live.py

Refactored kenwood_live.py driver - Charles Stewart, 03/25/2015 08:50 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
import re
22

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

    
28
LOG = logging.getLogger(__name__)
29

    
30
NOCACHE = "CHIRP_NOCACHE" in os.environ
31

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

    
37
THF6_MODES = ["FM", "WFM", "AM", "LSB", "USB", "CW"]
38

    
39
# The Kenwood TS-2000 uses ";" as a CAT command message delimiter, and all
40
# others use "\n". Also, TS-2000 doesn't space delimite the command fields,
41
# but others do.
42
LAST_DELIMITER = ("\r", " ")
43

    
44
LOCK = threading.Lock()
45

    
46
def command(ser, cmd, *args):
47
    """Send @cmd to radio via @ser"""
48
    global LOCK, LAST_DELIMITER
49

    
50
    start = time.time()
51

    
52
    LOCK.acquire()
53

    
54
    if args:
55
        cmd += LAST_DELIMITER[1] + LAST_DELIMITER[1].join(args)
56
    cmd += LAST_DELIMITER[0]
57

    
58
    LOG.debug("PC->RADIO: %s" % cmd.strip())
59
    ser.write(cmd)
60

    
61
    result = ""
62
    while not result.endswith(LAST_DELIMITER[0]):
63
        result += ser.read(8)
64
        if (time.time() - start) > 0.5:
65
            LOG.error("Timeout waiting for data")
66
            break
67

    
68
    if result.endswith(LAST_DELIMITER[0]):
69
        result = result[:-1]
70
        LOG.debug("RADIO->PC: %s" % result.strip())
71

    
72
    LOCK.release()
73

    
74
    return result.strip()
75

    
76
LAST_BAUD = 9600
77

    
78

    
79
def get_id(ser):
80
    """Get the ID of the radio attached to @ser"""
81
    global LAST_BAUD
82
    bauds = [9600, 19200, 38400, 57600]
83
    bauds.remove(LAST_BAUD)
84
    bauds.insert(0, LAST_BAUD)
85

    
86
    global LAST_DELIMITER
87
    command_delimiters = [("\r"," "), (";","")]
88

    
89
    for i in bauds:
90
        for j in command_delimiters:
91
            LAST_DELIMITER = j
92
            LOG.info("Trying ID at baud %i" % i)
93
            ser.setBaudrate(i)
94
            ser.write(LAST_DELIMITER[0])
95
            ser.read(25)
96
            resp = command(ser, "ID")
97

    
98
            # most kenwood radios
99
            if " " in resp:
100
                LAST_BAUD = i
101
                return resp.split(" ")[1]
102

    
103
            # TS-2000
104
            if "ID019" == resp:
105
                LAST_BADE = i
106
                return "TS-2000"
107

    
108
    raise errors.RadioError("No response from radio")
109

    
110

    
111
def get_tmode(tone, ctcss, dcs):
112
    """Get the tone mode based on the values of the tone, ctcss, dcs"""
113
    if dcs and int(dcs) == 1:
114
        return "DTCS"
115
    elif int(ctcss):
116
        return "TSQL"
117
    elif int(tone):
118
        return "Tone"
119
    else:
120
        return ""
121

    
122

    
123
def iserr(result):
124
    """Returns True if the @result from a radio is an error"""
125
    return result in ["N", "?"]
126

    
127

    
128
class KenwoodLiveRadio(chirp_common.LiveRadio):
129
    """Base class for all live-mode kenwood radios"""
130
    BAUD_RATE = 9600
131
    VENDOR = "Kenwood"
132
    MODEL = ""
133

    
134
    _vfo = 0
135
    _upper = 200
136
    _kenwood_split = False
137
    _kenwood_valid_tones = list(chirp_common.TONES)
138

    
139
    def __init__(self, *args, **kwargs):
140
        chirp_common.LiveRadio.__init__(self, *args, **kwargs)
141

    
142
        self._memcache = {}
143

    
144
        if self.pipe:
145
            self.pipe.setTimeout(0.1)
146
            radio_id = get_id(self.pipe)
147
            if radio_id != self.MODEL.split(" ")[0]:
148
                raise Exception("Radio reports %s (not %s)" % (radio_id,
149
                                                               self.MODEL))
150

    
151
            command(self.pipe, "AI", "0")
152

    
153
    def _cmd_get_memory(self, number):
154
        return "MR", "%i,0,%03i" % (self._vfo, number)
155

    
156
    def _cmd_get_memory_name(self, number):
157
        return "MNA", "%i,%03i" % (self._vfo, number)
158

    
159
    def _cmd_get_split(self, number):
160
        return "MR", "%i,1,%03i" % (self._vfo, number)
161

    
162
    def _cmd_set_memory(self, number, spec):
163
        if spec:
164
            spec = "," + spec
165
        return "MW", "%i,0,%03i%s" % (self._vfo, number, spec)
166

    
167
    def _cmd_set_memory_name(self, number, name):
168
        return "MNA", "%i,%03i,%s" % (self._vfo, number, name)
169

    
170
    def _cmd_set_split(self, number, spec):
171
        return "MW", "%i,1,%03i,%s" % (self._vfo, number, spec)
172

    
173
    def get_raw_memory(self, number):
174
        return command(self.pipe, *self._cmd_get_memory(number))
175

    
176
    def get_memory(self, number):
177
        if number < 0 or number > self._upper:
178
            raise errors.InvalidMemoryLocation(
179
                "Number must be between 0 and %i" % self._upper)
180
        if number in self._memcache and not NOCACHE:
181
            return self._memcache[number]
182

    
183
        result = command(self.pipe, *self._cmd_get_memory(number))
184
        if result == "N" or result == "E":
185
            mem = chirp_common.Memory()
186
            mem.number = number
187
            mem.empty = True
188
            self._memcache[mem.number] = mem
189
            return mem
190
        elif " " not in result:
191
            LOG.error("Not sure what to do with this: `%s'" % result)
192
            raise errors.RadioError("Unexpected result returned from radio")
193

    
194
        value = result.split(" ")[1]
195
        spec = value.split(",")
196

    
197
        mem = self._parse_mem_spec(spec)
198
        self._memcache[mem.number] = mem
199

    
200
        result = command(self.pipe, *self._cmd_get_memory_name(number))
201
        if " " in result:
202
            value = result.split(" ", 1)[1]
203
            if value.count(",") == 2:
204
                _zero, _loc, mem.name = value.split(",")
205
            else:
206
                _loc, mem.name = value.split(",")
207

    
208
        if mem.duplex == "" and self._kenwood_split:
209
            result = command(self.pipe, *self._cmd_get_split(number))
210
            if " " in result:
211
                value = result.split(" ", 1)[1]
212
                self._parse_split_spec(mem, value.split(","))
213

    
214
        return mem
215

    
216
    def _make_mem_spec(self, mem):
217
        pass
218

    
219
    def _parse_mem_spec(self, spec):
220
        pass
221

    
222
    def _parse_split_spec(self, mem, spec):
223
        mem.duplex = "split"
224
        mem.offset = int(spec[2])
225

    
226
    def _make_split_spec(self, mem):
227
        return ("%011i" % mem.offset, "0")
228

    
229
    def set_memory(self, memory):
230
        if memory.number < 0 or memory.number > self._upper:
231
            raise errors.InvalidMemoryLocation(
232
                "Number must be between 0 and %i" % self._upper)
233

    
234
        spec = self._make_mem_spec(memory)
235
        spec = ",".join(spec)
236
        r1 = command(self.pipe, *self._cmd_set_memory(memory.number, spec))
237
        if not iserr(r1):
238
            time.sleep(0.5)
239
            r2 = command(self.pipe, *self._cmd_set_memory_name(memory.number,
240
                                                               memory.name))
241
            if not iserr(r2):
242
                memory.name = memory.name.rstrip()
243
                self._memcache[memory.number] = memory
244
            else:
245
                raise errors.InvalidDataError("Radio refused name %i: %s" %
246
                                              (memory.number,
247
                                               repr(memory.name)))
248
        else:
249
            raise errors.InvalidDataError("Radio refused %i" % memory.number)
250

    
251
        if memory.duplex == "split" and self._kenwood_split:
252
            spec = ",".join(self._make_split_spec(memory))
253
            result = command(self.pipe, *self._cmd_set_split(memory.number,
254
                                                             spec))
255
            if iserr(result):
256
                raise errors.InvalidDataError("Radio refused %i" %
257
                                              memory.number)
258

    
259
    def erase_memory(self, number):
260
        if number not in self._memcache:
261
            return
262

    
263
        resp = command(self.pipe, *self._cmd_set_memory(number, ""))
264
        if iserr(resp):
265
            raise errors.RadioError("Radio refused delete of %i" % number)
266
        del self._memcache[number]
267

    
268
    def _kenwood_get(self, cmd):
269
        resp = command(self.pipe, cmd)
270
        if " " in resp:
271
            return resp.split(" ", 1)
272
        else:
273
            if resp == cmd:
274
                return [resp, ""]
275
            else:
276
                raise errors.RadioError("Radio refused to return %s" % cmd)
277

    
278
    def _kenwood_set(self, cmd, value):
279
        resp = command(self.pipe, cmd, value)
280
        if resp[:len(cmd)] == cmd:
281
            return
282
        raise errors.RadioError("Radio refused to set %s" % cmd)
283

    
284
    def _kenwood_get_bool(self, cmd):
285
        _cmd, result = self._kenwood_get(cmd)
286
        return result == "1"
287

    
288
    def _kenwood_set_bool(self, cmd, value):
289
        return self._kenwood_set(cmd, str(int(value)))
290

    
291
    def _kenwood_get_int(self, cmd):
292
        _cmd, result = self._kenwood_get(cmd)
293
        return int(result)
294

    
295
    def _kenwood_set_int(self, cmd, value, digits=1):
296
        return self._kenwood_set(cmd, ("%%0%ii" % digits) % value)
297

    
298
    def set_settings(self, settings):
299
        for element in settings:
300
            if not isinstance(element, RadioSetting):
301
                self.set_settings(element)
302
                continue
303
            if not element.changed():
304
                continue
305
            if isinstance(element.value, RadioSettingValueBoolean):
306
                self._kenwood_set_bool(element.get_name(), element.value)
307
            elif isinstance(element.value, RadioSettingValueList):
308
                options = self._SETTINGS_OPTIONS[element.get_name()]
309
                self._kenwood_set_int(element.get_name(),
310
                                      options.index(str(element.value)))
311
            elif isinstance(element.value, RadioSettingValueInteger):
312
                if element.value.get_max() > 9:
313
                    digits = 2
314
                else:
315
                    digits = 1
316
                self._kenwood_set_int(element.get_name(),
317
                                      element.value, digits)
318
            elif isinstance(element.value, RadioSettingValueString):
319
                self._kenwood_set(element.get_name(), str(element.value))
320
            else:
321
                LOG.error("Unknown type %s" % element.value)
322

    
323

    
324
class KenwoodOldLiveRadio(KenwoodLiveRadio):
325
    _kenwood_valid_tones = list(chirp_common.OLD_TONES)
326

    
327
    def set_memory(self, memory):
328
        supported_tones = list(chirp_common.OLD_TONES)
329
        supported_tones.remove(69.3)
330
        if memory.rtone not in supported_tones:
331
            raise errors.UnsupportedToneError("This radio does not support " +
332
                                              "tone %.1fHz" % memory.rtone)
333
        if memory.ctone not in supported_tones:
334
            raise errors.UnsupportedToneError("This radio does not support " +
335
                                              "tone %.1fHz" % memory.ctone)
336

    
337
        return KenwoodLiveRadio.set_memory(self, memory)
338

    
339

    
340
@directory.register
341
class THD7Radio(KenwoodOldLiveRadio):
342
    """Kenwood TH-D7"""
343
    MODEL = "TH-D7"
344

    
345
    _kenwood_split = True
346

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

    
374
    def get_features(self):
375
        rf = chirp_common.RadioFeatures()
376
        rf.has_settings = True
377
        rf.has_dtcs = False
378
        rf.has_dtcs_polarity = False
379
        rf.has_bank = False
380
        rf.has_mode = True
381
        rf.has_tuning_step = False
382
        rf.can_odd_split = True
383
        rf.valid_duplexes = ["", "-", "+", "split"]
384
        rf.valid_modes = MODES.values()
385
        rf.valid_tmodes = ["", "Tone", "TSQL"]
386
        rf.valid_characters = \
387
            chirp_common.CHARSET_ALPHANUMERIC + "/.-+*)('&%$#! ~}|{"
388
        rf.valid_name_length = 7
389
        rf.memory_bounds = (1, self._upper)
390
        return rf
391

    
392
    def _make_mem_spec(self, mem):
393
        if mem.duplex in " -+":
394
            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
395
            offset = mem.offset
396
        else:
397
            duplex = 0
398
            offset = 0
399

    
400
        spec = (
401
            "%011i" % mem.freq,
402
            "%X" % STEPS.index(mem.tuning_step),
403
            "%i" % duplex,
404
            "0",
405
            "%i" % (mem.tmode == "Tone"),
406
            "%i" % (mem.tmode == "TSQL"),
407
            "",  # DCS Flag
408
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
409
            "",  # DCS Code
410
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
411
            "%09i" % offset,
412
            "%i" % util.get_dict_rev(MODES, mem.mode),
413
            "%i" % ((mem.skip == "S") and 1 or 0))
414

    
415
        return spec
416

    
417
    def _parse_mem_spec(self, spec):
418
        mem = chirp_common.Memory()
419

    
420
        mem.number = int(spec[2])
421
        mem.freq = int(spec[3], 10)
422
        mem.tuning_step = STEPS[int(spec[4], 16)]
423
        mem.duplex = DUPLEX[int(spec[5])]
424
        mem.tmode = get_tmode(spec[7], spec[8], spec[9])
425
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
426
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
427
        if spec[11] and spec[11].isdigit():
428
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
429
        else:
430
            LOG.warn("Unknown or invalid DCS: %s" % spec[11])
431
        if spec[13]:
432
            mem.offset = int(spec[13])
433
        else:
434
            mem.offset = 0
435
        mem.mode = MODES[int(spec[14])]
436
        mem.skip = int(spec[15]) and "S" or ""
437

    
438
        return mem
439

    
440
    def get_settings(self):
441
        main = RadioSettingGroup("main", "Main")
442
        aux = RadioSettingGroup("aux", "Aux")
443
        tnc = RadioSettingGroup("tnc", "TNC")
444
        save = RadioSettingGroup("save", "Save")
445
        display = RadioSettingGroup("display", "Display")
446
        dtmf = RadioSettingGroup("dtmf", "DTMF")
447
        radio = RadioSettingGroup("radio", "Radio",
448
                                  aux, tnc, save, display, dtmf)
449
        sky = RadioSettingGroup("sky", "SkyCommand")
450
        aprs = RadioSettingGroup("aprs", "APRS")
451

    
452
        top = RadioSettings(main, radio, aprs, sky)
453

    
454
        bools = [("AMR", aprs, "APRS Message Auto-Reply"),
455
                 ("AIP", aux, "Advanced Intercept Point"),
456
                 ("ARO", aux, "Automatic Repeater Offset"),
457
                 ("BCN", aprs, "Beacon"),
458
                 ("CH", radio, "Channel Mode Display"),
459
                 # ("DIG", aprs, "APRS Digipeater"),
460
                 ("DL", main, "Dual"),
461
                 ("LK", main, "Lock"),
462
                 ("LMP", main, "Lamp"),
463
                 ("TSP", dtmf, "DTMF Fast Transmission"),
464
                 ("TXH", dtmf, "TX Hold"),
465
                 ]
466

    
467
        for setting, group, name in bools:
468
            value = self._kenwood_get_bool(setting)
469
            rs = RadioSetting(setting, name,
470
                              RadioSettingValueBoolean(value))
471
            group.append(rs)
472

    
473
        lists = [("BAL", main, "Balance"),
474
                 ("BEP", aux, "Beep"),
475
                 ("BEPT", aprs, "APRS Beep"),
476
                 ("DS", tnc, "Data Sense"),
477
                 ("DTB", tnc, "Data Band"),
478
                 ("DTBA", aprs, "APRS Data Band"),
479
                 ("DTX", aprs, "APRS Data TX"),
480
                 # ("ICO", aprs, "APRS Icon"),
481
                 ("MNF", main, "Memory Display Mode"),
482
                 ("PKSA", aprs, "APRS Packet Speed"),
483
                 ("POSC", aprs, "APRS Position Comment"),
484
                 ("PT", dtmf, "DTMF Speed"),
485
                 ("SV", save, "Battery Save"),
486
                 ("TEMP", aprs, "APRS Temperature Units"),
487
                 ("TXI", aprs, "APRS Transmit Interval"),
488
                 # ("UNIT", aprs, "APRS Display Units"),
489
                 ("WAY", aprs, "Waypoint Mode"),
490
                 ]
491

    
492
        for setting, group, name in lists:
493
            value = self._kenwood_get_int(setting)
494
            options = self._SETTINGS_OPTIONS[setting]
495
            rs = RadioSetting(setting, name,
496
                              RadioSettingValueList(options,
497
                                                    options[value]))
498
            group.append(rs)
499

    
500
        ints = [("CNT", display, "Contrast", 1, 16),
501
                ]
502
        for setting, group, name, minv, maxv in ints:
503
            value = self._kenwood_get_int(setting)
504
            rs = RadioSetting(setting, name,
505
                              RadioSettingValueInteger(minv, maxv, value))
506
            group.append(rs)
507

    
508
        strings = [("MES", display, "Power-on Message", 8),
509
                   ("MYC", aprs, "APRS Callsign", 8),
510
                   ("PP", aprs, "APRS Path", 32),
511
                   ("SCC", sky, "SkyCommand Callsign", 8),
512
                   ("SCT", sky, "SkyCommand To Callsign", 8),
513
                   # ("STAT", aprs, "APRS Status Text", 32),
514
                   ]
515
        for setting, group, name, length in strings:
516
            _cmd, value = self._kenwood_get(setting)
517
            rs = RadioSetting(setting, name,
518
                              RadioSettingValueString(0, length, value))
519
            group.append(rs)
520

    
521
        return top
522

    
523

    
524
@directory.register
525
class THD7GRadio(THD7Radio):
526
    """Kenwood TH-D7G"""
527
    MODEL = "TH-D7G"
528

    
529
    def get_features(self):
530
        rf = super(THD7GRadio, self).get_features()
531
        rf.valid_name_length = 8
532
        return rf
533

    
534

    
535
@directory.register
536
class TMD700Radio(KenwoodOldLiveRadio):
537
    """Kenwood TH-D700"""
538
    MODEL = "TM-D700"
539

    
540
    _kenwood_split = True
541

    
542
    def get_features(self):
543
        rf = chirp_common.RadioFeatures()
544
        rf.has_dtcs = True
545
        rf.has_dtcs_polarity = False
546
        rf.has_bank = False
547
        rf.has_mode = False
548
        rf.has_tuning_step = False
549
        rf.can_odd_split = True
550
        rf.valid_duplexes = ["", "-", "+", "split"]
551
        rf.valid_modes = ["FM"]
552
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
553
        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
554
        rf.valid_name_length = 8
555
        rf.memory_bounds = (1, self._upper)
556
        return rf
557

    
558
    def _make_mem_spec(self, mem):
559
        if mem.duplex in " -+":
560
            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
561
        else:
562
            duplex = 0
563
        spec = (
564
            "%011i" % mem.freq,
565
            "%X" % STEPS.index(mem.tuning_step),
566
            "%i" % duplex,
567
            "0",
568
            "%i" % (mem.tmode == "Tone"),
569
            "%i" % (mem.tmode == "TSQL"),
570
            "%i" % (mem.tmode == "DTCS"),
571
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
572
            "%03i0" % (chirp_common.DTCS_CODES.index(mem.dtcs) + 1),
573
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
574
            "%09i" % mem.offset,
575
            "%i" % util.get_dict_rev(MODES, mem.mode),
576
            "%i" % ((mem.skip == "S") and 1 or 0))
577

    
578
        return spec
579

    
580
    def _parse_mem_spec(self, spec):
581
        mem = chirp_common.Memory()
582

    
583
        mem.number = int(spec[2])
584
        mem.freq = int(spec[3])
585
        mem.tuning_step = STEPS[int(spec[4], 16)]
586
        mem.duplex = DUPLEX[int(spec[5])]
587
        mem.tmode = get_tmode(spec[7], spec[8], spec[9])
588
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
589
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
590
        if spec[11] and spec[11].isdigit():
591
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
592
        else:
593
            LOG.warn("Unknown or invalid DCS: %s" % spec[11])
594
        if spec[13]:
595
            mem.offset = int(spec[13])
596
        else:
597
            mem.offset = 0
598
        mem.mode = MODES[int(spec[14])]
599
        mem.skip = int(spec[15]) and "S" or ""
600

    
601
        return mem
602

    
603

    
604
@directory.register
605
class TMV7Radio(KenwoodOldLiveRadio):
606
    """Kenwood TM-V7"""
607
    MODEL = "TM-V7"
608

    
609
    mem_upper_limit = 200  # Will be updated
610

    
611
    def get_features(self):
612
        rf = chirp_common.RadioFeatures()
613
        rf.has_dtcs = False
614
        rf.has_dtcs_polarity = False
615
        rf.has_bank = False
616
        rf.has_mode = False
617
        rf.has_tuning_step = False
618
        rf.valid_modes = ["FM"]
619
        rf.valid_tmodes = ["", "Tone", "TSQL"]
620
        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
621
        rf.valid_name_length = 7
622
        rf.has_sub_devices = True
623
        rf.memory_bounds = (1, self._upper)
624
        return rf
625

    
626
    def _make_mem_spec(self, mem):
627
        spec = (
628
            "%011i" % mem.freq,
629
            "%X" % STEPS.index(mem.tuning_step),
630
            "%i" % util.get_dict_rev(DUPLEX, mem.duplex),
631
            "0",
632
            "%i" % (mem.tmode == "Tone"),
633
            "%i" % (mem.tmode == "TSQL"),
634
            "0",
635
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
636
            "000",
637
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
638
            "",
639
            "0")
640

    
641
        return spec
642

    
643
    def _parse_mem_spec(self, spec):
644
        mem = chirp_common.Memory()
645
        mem.number = int(spec[2])
646
        mem.freq = int(spec[3])
647
        mem.tuning_step = STEPS[int(spec[4], 16)]
648
        mem.duplex = DUPLEX[int(spec[5])]
649
        if int(spec[7]):
650
            mem.tmode = "Tone"
651
        elif int(spec[8]):
652
            mem.tmode = "TSQL"
653
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
654
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
655

    
656
        return mem
657

    
658
    def get_sub_devices(self):
659
        return [TMV7RadioVHF(self.pipe), TMV7RadioUHF(self.pipe)]
660

    
661
    def __test_location(self, loc):
662
        mem = self.get_memory(loc)
663
        if not mem.empty:
664
            # Memory was not empty, must be valid
665
            return True
666

    
667
        # Mem was empty (or invalid), try to set it
668
        if self._vfo == 0:
669
            mem.freq = 144000000
670
        else:
671
            mem.freq = 440000000
672
        mem.empty = False
673
        try:
674
            self.set_memory(mem)
675
        except Exception:
676
            # Failed, so we're past the limit
677
            return False
678

    
679
        # Erase what we did
680
        try:
681
            self.erase_memory(loc)
682
        except Exception:
683
            pass  # V7A Can't delete just yet
684

    
685
        return True
686

    
687
    def _detect_split(self):
688
        return 50
689

    
690

    
691
class TMV7RadioSub(TMV7Radio):
692
    """Base class for the TM-V7 sub devices"""
693
    def __init__(self, pipe):
694
        TMV7Radio.__init__(self, pipe)
695
        self._detect_split()
696

    
697

    
698
class TMV7RadioVHF(TMV7RadioSub):
699
    """TM-V7 VHF subdevice"""
700
    VARIANT = "VHF"
701
    _vfo = 0
702

    
703

    
704
class TMV7RadioUHF(TMV7RadioSub):
705
    """TM-V7 UHF subdevice"""
706
    VARIANT = "UHF"
707
    _vfo = 1
708

    
709

    
710
@directory.register
711
class TMG707Radio(TMV7Radio):
712
    """Kenwood TM-G707"""
713
    MODEL = "TM-G707"
714

    
715
    def get_features(self):
716
        rf = TMV7Radio.get_features(self)
717
        rf.has_sub_devices = False
718
        rf.memory_bounds = (1, 180)
719
        rf.valid_bands = [(118000000, 174000000),
720
                          (300000000, 520000000),
721
                          (800000000, 999000000)]
722
        return rf
723

    
724

    
725
THG71_STEPS = [5, 6.25, 10, 12.5, 15, 20, 25, 30, 50, 100]
726

    
727

    
728
@directory.register
729
class THG71Radio(TMV7Radio):
730
    """Kenwood TH-G71"""
731
    MODEL = "TH-G71"
732

    
733
    def get_features(self):
734
        rf = TMV7Radio.get_features(self)
735
        rf.has_tuning_step = True
736
        rf.valid_tuning_steps = list(THG71_STEPS)
737
        rf.valid_name_length = 6
738
        rf.has_sub_devices = False
739
        rf.valid_bands = [(118000000, 174000000),
740
                          (320000000, 470000000),
741
                          (800000000, 945000000)]
742
        return rf
743

    
744
    def _make_mem_spec(self, mem):
745
        spec = (
746
            "%011i" % mem.freq,
747
            "%X" % THG71_STEPS.index(mem.tuning_step),
748
            "%i" % util.get_dict_rev(DUPLEX, mem.duplex),
749
            "0",
750
            "%i" % (mem.tmode == "Tone"),
751
            "%i" % (mem.tmode == "TSQL"),
752
            "0",
753
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
754
            "000",
755
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
756
            "%09i" % mem.offset,
757
            "%i" % ((mem.skip == "S") and 1 or 0))
758
        return spec
759

    
760
    def _parse_mem_spec(self, spec):
761
        mem = chirp_common.Memory()
762
        mem.number = int(spec[2])
763
        mem.freq = int(spec[3])
764
        mem.tuning_step = THG71_STEPS[int(spec[4], 16)]
765
        mem.duplex = DUPLEX[int(spec[5])]
766
        if int(spec[7]):
767
            mem.tmode = "Tone"
768
        elif int(spec[8]):
769
            mem.tmode = "TSQL"
770
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
771
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
772
        if spec[13]:
773
            mem.offset = int(spec[13])
774
        else:
775
            mem.offset = 0
776
        return mem
777

    
778

    
779
THF6A_STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
780
               100.0]
781

    
782
THF6A_DUPLEX = dict(DUPLEX)
783
THF6A_DUPLEX[3] = "split"
784

    
785

    
786
@directory.register
787
class THF6ARadio(KenwoodLiveRadio):
788
    """Kenwood TH-F6"""
789
    MODEL = "TH-F6"
790

    
791
    _upper = 399
792
    _kenwood_split = True
793

    
794
    def get_features(self):
795
        rf = chirp_common.RadioFeatures()
796
        rf.has_dtcs_polarity = False
797
        rf.has_bank = False
798
        rf.can_odd_split = True
799
        rf.valid_modes = list(THF6_MODES)
800
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
801
        rf.valid_tuning_steps = list(THF6A_STEPS)
802
        rf.valid_bands = [(1000, 1300000000)]
803
        rf.valid_skips = ["", "S"]
804
        rf.valid_duplexes = THF6A_DUPLEX.values()
805
        rf.valid_characters = chirp_common.CHARSET_ASCII
806
        rf.valid_name_length = 8
807
        rf.memory_bounds = (0, self._upper)
808
        rf.has_settings = True
809
        return rf
810

    
811
    def _cmd_set_memory(self, number, spec):
812
        if spec:
813
            spec = "," + spec
814
        return "MW", "0,%03i%s" % (number, spec)
815

    
816
    def _cmd_get_memory(self, number):
817
        return "MR", "0,%03i" % number
818

    
819
    def _cmd_get_memory_name(self, number):
820
        return "MNA", "%03i" % number
821

    
822
    def _cmd_set_memory_name(self, number, name):
823
        return "MNA", "%03i,%s" % (number, name)
824

    
825
    def _cmd_get_split(self, number):
826
        return "MR", "1,%03i" % number
827

    
828
    def _cmd_set_split(self, number, spec):
829
        return "MW", "1,%03i,%s" % (number, spec)
830

    
831
    def _parse_mem_spec(self, spec):
832
        mem = chirp_common.Memory()
833

    
834
        mem.number = int(spec[1])
835
        mem.freq = int(spec[2])
836
        mem.tuning_step = THF6A_STEPS[int(spec[3], 16)]
837
        mem.duplex = THF6A_DUPLEX[int(spec[4])]
838
        mem.tmode = get_tmode(spec[6], spec[7], spec[8])
839
        mem.rtone = self._kenwood_valid_tones[int(spec[9])]
840
        mem.ctone = self._kenwood_valid_tones[int(spec[10])]
841
        if spec[11] and spec[11].isdigit():
842
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
843
        else:
844
            LOG.warn("Unknown or invalid DCS: %s" % spec[11])
845
        if spec[12]:
846
            mem.offset = int(spec[12])
847
        else:
848
            mem.offset = 0
849
        mem.mode = THF6_MODES[int(spec[13])]
850
        if spec[14] == "1":
851
            mem.skip = "S"
852

    
853
        return mem
854

    
855
    def _make_mem_spec(self, mem):
856
        if mem.duplex in " +-":
857
            duplex = util.get_dict_rev(THF6A_DUPLEX, mem.duplex)
858
            offset = mem.offset
859
        elif mem.duplex == "split":
860
            duplex = 0
861
            offset = 0
862
        else:
863
            LOG.warn("Bug: unsupported duplex `%s'" % mem.duplex)
864
        spec = (
865
            "%011i" % mem.freq,
866
            "%X" % THF6A_STEPS.index(mem.tuning_step),
867
            "%i" % duplex,
868
            "0",
869
            "%i" % (mem.tmode == "Tone"),
870
            "%i" % (mem.tmode == "TSQL"),
871
            "%i" % (mem.tmode == "DTCS"),
872
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
873
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
874
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
875
            "%09i" % offset,
876
            "%i" % (THF6_MODES.index(mem.mode)),
877
            "%i" % (mem.skip == "S"))
878

    
879
        return spec
880

    
881
    _SETTINGS_OPTIONS = {
882
        "APO": ["Off", "30min", "60min"],
883
        "BAL": ["100%:0%", "75%:25%", "50%:50%", "25%:75%", "%0:100%"],
884
        "BAT": ["Lithium", "Alkaline"],
885
        "CKEY": ["Call", "1750Hz"],
886
        "DATP": ["1200bps", "9600bps"],
887
        "LAN": ["English", "Japanese"],
888
        "MNF": ["Name", "Frequency"],
889
        "MRM": ["All Band", "Current Band"],
890
        "PT": ["100ms", "250ms", "500ms", "750ms",
891
               "1000ms", "1500ms", "2000ms"],
892
        "SCR": ["Time", "Carrier", "Seek"],
893
        "SV": ["Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s",
894
               "2s", "3s", "4s", "5s"],
895
        "VXD": ["250ms", "500ms", "750ms", "1s", "1.5s", "2s", "3s"],
896
    }
897

    
898
    def get_settings(self):
899
        main = RadioSettingGroup("main", "Main")
900
        aux = RadioSettingGroup("aux", "Aux")
901
        save = RadioSettingGroup("save", "Save")
902
        display = RadioSettingGroup("display", "Display")
903
        dtmf = RadioSettingGroup("dtmf", "DTMF")
904
        top = RadioSettings(main, aux, save, display, dtmf)
905

    
906
        lists = [("APO", save, "Automatic Power Off"),
907
                 ("BAL", main, "Balance"),
908
                 ("BAT", save, "Battery Type"),
909
                 ("CKEY", aux, "CALL Key Set Up"),
910
                 ("DATP", aux, "Data Packet Speed"),
911
                 ("LAN", display, "Language"),
912
                 ("MNF", main, "Memory Display Mode"),
913
                 ("MRM", main, "Memory Recall Method"),
914
                 ("PT", dtmf, "DTMF Speed"),
915
                 ("SCR", main, "Scan Resume"),
916
                 ("SV", save, "Battery Save"),
917
                 ("VXD", aux, "VOX Drop Delay"),
918
                 ]
919

    
920
        bools = [("ANT", aux, "Bar Antenna"),
921
                 ("ATT", main, "Attenuator Enabled"),
922
                 ("ARO", main, "Automatic Repeater Offset"),
923
                 ("BEP", aux, "Beep for keypad"),
924
                 ("DL", main, "Dual"),
925
                 ("DLK", dtmf, "DTMF Lockout On Transmit"),
926
                 ("ELK", aux, "Enable Locked Tuning"),
927
                 ("LK", main, "Lock"),
928
                 ("LMP", display, "Lamp"),
929
                 ("NSFT", aux, "Noise Shift"),
930
                 ("TH", aux, "Tx Hold for 1750"),
931
                 ("TSP", dtmf, "DTMF Fast Transmission"),
932
                 ("TXH", dtmf, "TX Hold DTMF"),
933
                 ("TXS", main, "Transmit Inhibit"),
934
                 ("VOX", aux, "VOX Enable"),
935
                 ("VXB", aux, "VOX On Busy"),
936
                 ]
937

    
938
        ints = [("CNT", display, "Contrast", 1, 16),
939
                ("VXG", aux, "VOX Gain", 0, 9),
940
                ]
941

    
942
        strings = [("MES", display, "Power-on Message", 8),
943
                   ]
944

    
945
        for setting, group, name in bools:
946
            value = self._kenwood_get_bool(setting)
947
            rs = RadioSetting(setting, name,
948
                              RadioSettingValueBoolean(value))
949
            group.append(rs)
950

    
951
        for setting, group, name in lists:
952
            value = self._kenwood_get_int(setting)
953
            options = self._SETTINGS_OPTIONS[setting]
954
            rs = RadioSetting(setting, name,
955
                              RadioSettingValueList(options,
956
                                                    options[value]))
957
            group.append(rs)
958

    
959
        for setting, group, name, minv, maxv in ints:
960
            value = self._kenwood_get_int(setting)
961
            rs = RadioSetting(setting, name,
962
                              RadioSettingValueInteger(minv, maxv, value))
963
            group.append(rs)
964

    
965
        for setting, group, name, length in strings:
966
            _cmd, value = self._kenwood_get(setting)
967
            rs = RadioSetting(setting, name,
968
                              RadioSettingValueString(0, length, value))
969
            group.append(rs)
970

    
971
        return top
972

    
973

    
974
@directory.register
975
class THF7ERadio(THF6ARadio):
976
    """Kenwood TH-F7"""
977
    MODEL = "TH-F7"
978

    
979
D710_DUPLEX = ["", "+", "-", "split"]
980
D710_MODES = ["FM", "NFM", "AM"]
981
D710_SKIP = ["", "S"]
982
D710_STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
983
D710_TONES = list(chirp_common.TONES)
984
D710_TONES.remove(159.8)
985
D710_TONES.remove(165.5)
986
D710_TONES.remove(171.3)
987
D710_TONES.remove(177.3)
988
D710_TONES.remove(183.5)
989
D710_TONES.remove(189.9)
990
D710_TONES.remove(196.6)
991
D710_TONES.remove(199.5)
992

    
993

    
994
@directory.register
995
class TMD710Radio(KenwoodLiveRadio):
996
    """Kenwood TM-D710"""
997
    MODEL = "TM-D710"
998

    
999
    _upper = 999
1000
    _kenwood_valid_tones = list(D710_TONES)
1001

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

    
1017
    def _cmd_get_memory(self, number):
1018
        return "ME", "%03i" % number
1019

    
1020
    def _cmd_get_memory_name(self, number):
1021
        return "MN", "%03i" % number
1022

    
1023
    def _cmd_set_memory(self, number, spec):
1024
        return "ME", "%03i,%s" % (number, spec)
1025

    
1026
    def _cmd_set_memory_name(self, number, name):
1027
        return "MN", "%03i,%s" % (number, name)
1028

    
1029
    def _parse_mem_spec(self, spec):
1030
        mem = chirp_common.Memory()
1031

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

    
1055
        return mem
1056

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

    
1077
        return spec
1078

    
1079

    
1080
@directory.register
1081
class THD72Radio(TMD710Radio):
1082
    """Kenwood TH-D72"""
1083
    MODEL = "TH-D72 (live mode)"
1084
    HARDWARE_FLOW = sys.platform == "darwin"  # only OS X driver needs hw flow
1085

    
1086
    def _parse_mem_spec(self, spec):
1087
        mem = chirp_common.Memory()
1088

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

    
1112
        return mem
1113

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

    
1136
        return spec
1137

    
1138

    
1139
@directory.register
1140
class TMV71Radio(TMD710Radio):
1141
    """Kenwood TM-V71"""
1142
    MODEL = "TM-V71"
1143

    
1144

    
1145
@directory.register
1146
class TMD710GRadio(TMD710Radio):
1147
    """Kenwood TM-D710G"""
1148
    MODEL = "TM-D710G"
1149

    
1150
    @classmethod
1151
    def get_prompts(cls):
1152
        rp = chirp_common.RadioPrompts()
1153
        rp.experimental = ("This radio driver is currently under development, "
1154
                           "and supports the same features as the TM-D710A/E. "
1155
                           "There are no known issues with it, but you should "
1156
                           "proceed with caution.")
1157
        return rp
1158

    
1159

    
1160
THK2_DUPLEX = ["", "+", "-"]
1161
THK2_MODES = ["FM", "NFM"]
1162
THK2_TONES = list(chirp_common.TONES)
1163
THK2_TONES.remove(159.8)  # ??
1164
THK2_TONES.remove(165.5)  # ??
1165
THK2_TONES.remove(171.3)  # ??
1166
THK2_TONES.remove(177.3)  # ??
1167
THK2_TONES.remove(183.5)  # ??
1168
THK2_TONES.remove(189.9)  # ??
1169
THK2_TONES.remove(196.6)  # ??
1170
THK2_TONES.remove(199.5)  # ??
1171

    
1172
THK2_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/"
1173

    
1174

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

    
1180
    _kenwood_valid_tones = list(THK2_TONES)
1181

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

    
1199
    def _cmd_get_memory(self, number):
1200
        return "ME", "%02i" % number
1201

    
1202
    def _cmd_get_memory_name(self, number):
1203
        return "MN", "%02i" % number
1204

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

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

    
1211
    def _parse_mem_spec(self, spec):
1212
        mem = chirp_common.Memory()
1213

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

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

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

    
1259

    
1260
@directory.register
1261
class TM271Radio(THK2Radio):
1262
    """Kenwood TM-271"""
1263
    MODEL = "TM-271"
1264

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

    
1282
    def _cmd_get_memory(self, number):
1283
        return "ME", "%03i" % number
1284

    
1285
    def _cmd_get_memory_name(self, number):
1286
        return "MN", "%03i" % number
1287

    
1288
    def _cmd_set_memory(self, number, spec):
1289
        return "ME", "%03i,%s" % (number, spec)
1290

    
1291
    def _cmd_set_memory_name(self, number, name):
1292
        return "MN", "%03i,%s" % (number, name)
1293

    
1294

    
1295
@directory.register
1296
class TM281Radio(TM271Radio):
1297
    """Kenwood TM-281"""
1298
    MODEL = "TM-281"
1299
    # seems that this is a perfect clone of TM271 with just a different model
1300

    
1301

    
1302
@directory.register
1303
class TM471Radio(THK2Radio):
1304
    """Kenwood TM-471"""
1305
    MODEL = "TM-471"
1306

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

    
1324
    def _cmd_get_memory(self, number):
1325
        return "ME", "%03i" % number
1326

    
1327
    def _cmd_get_memory_name(self, number):
1328
        return "MN", "%03i" % number
1329

    
1330
    def _cmd_set_memory(self, number, spec):
1331
        return "ME", "%03i,%s" % (number, spec)
1332

    
1333
    def _cmd_set_memory_name(self, number, name):
1334
        return "MN", "%03i,%s" % (number, name)
(6-6/10)