Project

General

Profile

New Model #217 » kenwood_live.py

latest update - Charles Stewart, 03/27/2015 07:49 PM

 
1
# Copyright 2010 Dan Smith <dsmith@danplanet.com>
2
#
3
# This program is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

    
16
import threading
17
import os
18
import sys
19
import time
20
import logging
21

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

    
27
LOG = logging.getLogger(__name__)
28

    
29
NOCACHE = "CHIRP_NOCACHE" in os.environ
30

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

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

    
38
LOCK = threading.Lock()
39
COMMAND_RESP_BUFSIZE = 8
40
LAST_BAUD = 9600
41
LAST_DELIMITER = ("\r", " ")
42

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

    
47

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

    
52
    start = time.time()
53

    
54
    LOCK.acquire()
55

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

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

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

    
70
    if result.endswith(LAST_DELIMITER[0]):
71
        LOG.debug("RADIO->PC: %s" % result.strip())
72
        result = result[:-1]
73
    else:
74
        LOG.error("Giving up")
75

    
76
    LOCK.release()
77

    
78
    return result.strip()
79

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

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

    
90
    for i in bauds:
91
        curj = 0
92
        for j in command_delimiters:
93
            curj += 1
94
            LAST_DELIMITER = j
95
            LOG.info("Trying ID at baud %i with delimiter check #%i" % (i, curj))
96
            ser.setBaudrate(i)
97
            ser.write(LAST_DELIMITER[0])
98
            ser.read(25)
99
            resp = command(ser, "ID")
100

    
101
            # most kenwood radios
102
            if " " in resp:
103
                LAST_BAUD = i
104
                return resp.split(" ")[1]
105

    
106
            # TS-2000
107
            if "ID019" == resp:
108
                LAST_BAUD = i
109
                return "TS-2000"
110

    
111
    raise errors.RadioError("No response from radio")
112

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

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

    
128

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

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

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

    
143
        self._memcache = {}
144

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
215
        return mem
216

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
324

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

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

    
338
        return KenwoodLiveRadio.set_memory(self, memory)
339

    
340

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

    
346
    _kenwood_split = True
347

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

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

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

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

    
416
        return spec
417

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

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

    
439
        return mem
440

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

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

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

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

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

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

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

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

    
522
        return top
523

    
524

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

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

    
535

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

    
541
    _kenwood_split = True
542

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

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

    
579
        return spec
580

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

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

    
602
        return mem
603

    
604

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

    
610
    mem_upper_limit = 200  # Will be updated
611

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

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

    
642
        return spec
643

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

    
657
        return mem
658

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

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

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

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

    
686
        return True
687

    
688
    def _detect_split(self):
689
        return 50
690

    
691

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

    
698

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

    
704

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

    
710

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

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

    
725

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

    
728

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

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

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

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

    
779

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

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

    
786

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

    
792
    _upper = 399
793
    _kenwood_split = True
794

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

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

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

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

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

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

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

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

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

    
854
        return mem
855

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

    
880
        return spec
881

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

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

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

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

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

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

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

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

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

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

    
972
        return top
973

    
974

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

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

    
994

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

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

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

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

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

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

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

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

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

    
1056
        return mem
1057

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

    
1078
        return spec
1079

    
1080

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

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

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

    
1113
        return mem
1114

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

    
1137
        return spec
1138

    
1139

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

    
1145

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

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

    
1160

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

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

    
1175

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

    
1181
    _kenwood_valid_tones = list(THK2_TONES)
1182

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

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

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

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

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

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

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

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

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

    
1260

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

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

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

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

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

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

    
1295

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

    
1302

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

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

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

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

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

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