Project

General

Profile

New Model #4129 » kenwood_live.py

William McKeehan, 07/13/2018 07:12 AM

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

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

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

    
27
LOG = logging.getLogger(__name__)
28

    
29
NOCACHE = "CHIRP_NOCACHE" in os.environ
30

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

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

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

    
48
LOCK = threading.Lock()
49
COMMAND_RESP_BUFSIZE = 8
50
LAST_BAUD = 9600
51
LAST_DELIMITER = ("\r", " ")
52

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

    
57

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

    
62
    start = time.time()
63

    
64
    LOCK.acquire()
65

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

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

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

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

    
86
    LOCK.release()
87

    
88
    return result.strip()
89

    
90

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

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

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

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

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

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

    
123

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

    
135

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

    
140

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

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

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

    
155
        self._memcache = {}
156

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
227
        return mem
228

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

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

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

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

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

    
247
        spec = self._make_mem_spec(memory)
248
        LOG.debug("set memory: %s" % memory)
249
        spec = ",".join(spec)
250
        r1 = command(self.pipe, *self._cmd_set_memory(memory.number, spec))
251
        if not iserr(r1):
252
            time.sleep(0.5)
253

    
254
            r2 = command(self.pipe, *self._cmd_set_memory_name(memory.number,
255
                                                               memory.name))
256
            if not iserr(r2):
257
                memory.name = memory.name.rstrip()
258
                self._memcache[memory.number] = memory
259
            else:
260
                raise errors.InvalidDataError("Radio refused name %i: %s" %
261
                                              (memory.number,
262
                                               repr(memory.name)))
263
        else:
264
            raise errors.InvalidDataError("Radio refused %i" % memory.number)
265

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

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

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

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

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

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

    
303
    def _kenwood_set_bool(self, cmd, value):
304
        return self._kenwood_set(cmd, str(int(value)))
305

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

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

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

    
338

    
339
class KenwoodOldLiveRadio(KenwoodLiveRadio):
340
    _kenwood_valid_tones = list(chirp_common.OLD_TONES)
341

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

    
352
        return KenwoodLiveRadio.set_memory(self, memory)
353

    
354

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

    
360
    _kenwood_split = True
361

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

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

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

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

    
430
        return spec
431

    
432
    def _parse_mem_spec(self, spec):
433
        mem = chirp_common.Memory()
434

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

    
453
        return mem
454

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

    
467
        top = RadioSettings(main, radio, aprs, sky)
468

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

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

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

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

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

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

    
536
        return top
537

    
538

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

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

    
549

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

    
555
    _kenwood_split = True
556

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

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

    
593
        return spec
594

    
595
    def _parse_mem_spec(self, spec):
596
        mem = chirp_common.Memory()
597

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

    
616
        return mem
617

    
618

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

    
624
    mem_upper_limit = 200  # Will be updated
625

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

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

    
656
        return spec
657

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

    
671
        return mem
672

    
673
    def get_sub_devices(self):
674
        return [TMV7RadioVHF(self.pipe), TMV7RadioUHF(self.pipe)]
675

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

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

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

    
700
        return True
701

    
702
    def _detect_split(self):
703
        return 50
704

    
705

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

    
712

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

    
718

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

    
724

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

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

    
739

    
740
THG71_STEPS = [5, 6.25, 10, 12.5, 15, 20, 25, 30, 50, 100]
741

    
742

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

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

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

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

    
793

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

    
797
THF6A_DUPLEX = dict(DUPLEX)
798
THF6A_DUPLEX[3] = "split"
799

    
800

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

    
806
    _upper = 399
807
    _kenwood_split = True
808
    _kenwood_valid_tones = list(KENWOOD_TONES)
809

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

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

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

    
835
    def _cmd_get_memory_name(self, number):
836
        return "MNA", "%03i" % number
837

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

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

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

    
847
    def _parse_mem_spec(self, spec):
848
        mem = chirp_common.Memory()
849

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

    
869
        return mem
870

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

    
895
        return spec
896

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

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

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

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

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

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

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

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

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

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

    
987
        return top
988

    
989

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

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

    
1000

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

    
1006
    _upper = 999
1007
    _kenwood_valid_tones = list(KENWOOD_TONES)
1008

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

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

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

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

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

    
1036
    def _parse_mem_spec(self, spec):
1037
        mem = chirp_common.Memory()
1038

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

    
1062
        return mem
1063

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

    
1084
        return spec
1085

    
1086

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

    
1093
    def _parse_mem_spec(self, spec):
1094
        mem = chirp_common.Memory()
1095

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

    
1119
        return mem
1120

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

    
1143
        return spec
1144

    
1145

    
1146
D74_MODES = ['WFM', 'FM', 'NFM', 'AM', 'NAM', 'DV', 'USB', 'LSB', 'CW', 'RTTY', 'DIG', 'PKT', 'NCW', 'NCWR', 'CWR', 'P25', 'Auto', 'RTTYR', 'FSK', 'FSKR', 'DMR', 'USER-L', 'USER-U', 'LSB+CW', 'USB+CW', 'RTTY-L', 'RTTY-U', 'N/A']
1147
D74_MODES = ['FM', 'DV', 'AM', 'LSB', 'USB', 'CW', 'NFM', 'DV', 'WFM', 'R-CW']
1148

    
1149
@directory.register
1150
class THD74Radio(THD72Radio):
1151
    """Kenwood TH-D74"""
1152
    MODEL = "TH-D74 (live mode)"
1153
    HARDWARE_FLOW = sys.platform == "darwin"  # only OS X driver needs hw flow
1154

    
1155
    _upper = 300
1156

    
1157
    def get_features(self):
1158
        rf = chirp_common.RadioFeatures()
1159
        rf.can_odd_split = True
1160
        rf.has_dtcs_polarity = False
1161
        rf.has_bank = False
1162
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1163
        rf.valid_modes = D74_MODES
1164
        rf.valid_duplexes = D710_DUPLEX
1165
        rf.valid_tuning_steps = D710_STEPS
1166
        rf.valid_characters = chirp_common.CHARSET_ASCII.replace(',', '')
1167
        #rf.valid_name_length = 8
1168
        rf.valid_skips = D710_SKIP
1169
        rf.memory_bounds = (0, 999)
1170
        return rf
1171

    
1172
    def _parse_mem_spec(self, spec):
1173
        LOG.debug("TH-D74->parse: %s" % spec)
1174
        mem = chirp_common.Memory()
1175

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

    
1199
        return mem
1200

    
1201
    def _make_mem_spec(self, mem):
1202

    
1203
        # 0 	5 kHz
1204
        # 1 	6.25 kHz
1205
        # 2 	8.33 kHz
1206
        # 3 	9 kHz
1207
        # 4 	10 kHz
1208
        # 5 	12.5 kHz
1209
        # 6 	15 kHz
1210
        # 7 	20 kHz
1211
        # 8 	25 kHz
1212
        # 9 	30 kHz
1213
        # A 	50 kHz
1214
        # B 	100 kHz
1215
        spec = ( 
1216
            "%010i" % mem.freq,
1217
            "%010i" % (0 if mem.duplex == "split" else mem.offset),  # Offset
1218
            "%X" % D710_STEPS.index(mem.tuning_step),
1219
            "%i" % (D74_MODES.index(mem.mode) == "NFM" and 8 or 0),
1220
            "%i" % D74_MODES.index(mem.mode),
1221
            "0",  # Fine mode
1222
            "0",  # Fine step mode
1223
            "%i" % (mem.tmode == "Tone" and 1 or 0),
1224
            "%i" % (mem.tmode == "TSQL" and 1 or 0),
1225
            "%i" % (mem.tmode == "DTCS" and 1 or 0),
1226
            "0",
1227
            "0",
1228
            "0",
1229
            "%i" % (0 if mem.duplex == "split" else D710_DUPLEX.index(mem.duplex)),#shift
1230
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
1231
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
1232
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
1233
            "0",
1234
            "CQCQCQ",
1235
            "0",  # Unknown
1236
            "00",  # Unknown
1237
            "0",  # Unknown
1238
            )
1239

    
1240
        return spec
1241

    
1242
    def _cmd_set_memory_name(self, number, name):
1243
        return "MR", "1"
1244
        # return "MNA", "%i,%03i,%s" % (self._vfo, number, name)
1245

    
1246

    
1247
@directory.register
1248
class TMV71Radio(TMD710Radio):
1249
    """Kenwood TM-V71"""
1250
    MODEL = "TM-V71"
1251

    
1252

    
1253
@directory.register
1254
class TMD710GRadio(TMD710Radio):
1255
    """Kenwood TM-D710G"""
1256
    MODEL = "TM-D710G"
1257

    
1258
    @classmethod
1259
    def get_prompts(cls):
1260
        rp = chirp_common.RadioPrompts()
1261
        rp.experimental = ("This radio driver is currently under development, "
1262
                           "and supports the same features as the TM-D710A/E. "
1263
                           "There are no known issues with it, but you should "
1264
                           "proceed with caution.")
1265
        return rp
1266

    
1267

    
1268
THK2_DUPLEX = ["", "+", "-"]
1269
THK2_MODES = ["FM", "NFM"]
1270

    
1271
THK2_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/"
1272

    
1273

    
1274
@directory.register
1275
class THK2Radio(KenwoodLiveRadio):
1276
    """Kenwood TH-K2"""
1277
    MODEL = "TH-K2"
1278

    
1279
    _kenwood_valid_tones = list(KENWOOD_TONES)
1280

    
1281
    def get_features(self):
1282
        rf = chirp_common.RadioFeatures()
1283
        rf.can_odd_split = False
1284
        rf.has_dtcs_polarity = False
1285
        rf.has_bank = False
1286
        rf.has_tuning_step = False
1287
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1288
        rf.valid_modes = THK2_MODES
1289
        rf.valid_duplexes = THK2_DUPLEX
1290
        rf.valid_characters = THK2_CHARS
1291
        rf.valid_name_length = 6
1292
        rf.valid_bands = [(136000000, 173990000)]
1293
        rf.valid_skips = ["", "S"]
1294
        rf.valid_tuning_steps = [5.0]
1295
        rf.memory_bounds = (0, 49)
1296
        return rf
1297

    
1298
    def _cmd_get_memory(self, number):
1299
        return "ME", "%02i" % number
1300

    
1301
    def _cmd_get_memory_name(self, number):
1302
        return "MN", "%02i" % number
1303

    
1304
    def _cmd_set_memory(self, number, spec):
1305
        return "ME", "%02i,%s" % (number, spec)
1306

    
1307
    def _cmd_set_memory_name(self, number, name):
1308
        return "MN", "%02i,%s" % (number, name)
1309

    
1310
    def _parse_mem_spec(self, spec):
1311
        mem = chirp_common.Memory()
1312

    
1313
        mem.number = int(spec[0])
1314
        mem.freq = int(spec[1])
1315
        # mem.tuning_step =
1316
        mem.duplex = THK2_DUPLEX[int(spec[3])]
1317
        if int(spec[5]):
1318
            mem.tmode = "Tone"
1319
        elif int(spec[6]):
1320
            mem.tmode = "TSQL"
1321
        elif int(spec[7]):
1322
            mem.tmode = "DTCS"
1323
        mem.rtone = self._kenwood_valid_tones[int(spec[8])]
1324
        mem.ctone = self._kenwood_valid_tones[int(spec[9])]
1325
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
1326
        mem.offset = int(spec[11])
1327
        mem.mode = THK2_MODES[int(spec[12])]
1328
        mem.skip = int(spec[16]) and "S" or ""
1329
        return mem
1330

    
1331
    def _make_mem_spec(self, mem):
1332
        try:
1333
            rti = self._kenwood_valid_tones.index(mem.rtone)
1334
            cti = self._kenwood_valid_tones.index(mem.ctone)
1335
        except ValueError:
1336
            raise errors.UnsupportedToneError()
1337

    
1338
        spec = (
1339
            "%010i" % mem.freq,
1340
            "0",
1341
            "%i" % THK2_DUPLEX.index(mem.duplex),
1342
            "0",
1343
            "%i" % int(mem.tmode == "Tone"),
1344
            "%i" % int(mem.tmode == "TSQL"),
1345
            "%i" % int(mem.tmode == "DTCS"),
1346
            "%02i" % rti,
1347
            "%02i" % cti,
1348
            "%03i" % chirp_common.DTCS_CODES.index(mem.dtcs),
1349
            "%08i" % mem.offset,
1350
            "%i" % THK2_MODES.index(mem.mode),
1351
            "0",
1352
            "%010i" % 0,
1353
            "0",
1354
            "%i" % int(mem.skip == "S")
1355
            )
1356
        return spec
1357

    
1358

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

    
1361

    
1362
@directory.register
1363
class TM271Radio(THK2Radio):
1364
    """Kenwood TM-271"""
1365
    MODEL = "TM-271"
1366

    
1367
    def get_features(self):
1368
        rf = chirp_common.RadioFeatures()
1369
        rf.can_odd_split = False
1370
        rf.has_dtcs_polarity = False
1371
        rf.has_bank = False
1372
        rf.has_tuning_step = False
1373
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1374
        rf.valid_modes = THK2_MODES
1375
        rf.valid_duplexes = THK2_DUPLEX
1376
        rf.valid_characters = THK2_CHARS
1377
        rf.valid_name_length = 6
1378
        rf.valid_bands = [(137000000, 173990000)]
1379
        rf.valid_skips = ["", "S"]
1380
        rf.valid_tuning_steps = list(TM271_STEPS)
1381
        rf.memory_bounds = (0, 99)
1382
        return rf
1383

    
1384
    def _cmd_get_memory(self, number):
1385
        return "ME", "%03i" % number
1386

    
1387
    def _cmd_get_memory_name(self, number):
1388
        return "MN", "%03i" % number
1389

    
1390
    def _cmd_set_memory(self, number, spec):
1391
        return "ME", "%03i,%s" % (number, spec)
1392

    
1393
    def _cmd_set_memory_name(self, number, name):
1394
        return "MN", "%03i,%s" % (number, name)
1395

    
1396

    
1397
@directory.register
1398
class TM281Radio(TM271Radio):
1399
    """Kenwood TM-281"""
1400
    MODEL = "TM-281"
1401
    # seems that this is a perfect clone of TM271 with just a different model
1402

    
1403

    
1404
@directory.register
1405
class TM471Radio(THK2Radio):
1406
    """Kenwood TM-471"""
1407
    MODEL = "TM-471"
1408

    
1409
    def get_features(self):
1410
        rf = chirp_common.RadioFeatures()
1411
        rf.can_odd_split = False
1412
        rf.has_dtcs_polarity = False
1413
        rf.has_bank = False
1414
        rf.has_tuning_step = False
1415
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1416
        rf.valid_modes = THK2_MODES
1417
        rf.valid_duplexes = THK2_DUPLEX
1418
        rf.valid_characters = THK2_CHARS
1419
        rf.valid_name_length = 6
1420
        rf.valid_bands = [(444000000, 479990000)]
1421
        rf.valid_skips = ["", "S"]
1422
        rf.valid_tuning_steps = [5.0]
1423
        rf.memory_bounds = (0, 99)
1424
        return rf
1425

    
1426
    def _cmd_get_memory(self, number):
1427
        return "ME", "%03i" % number
1428

    
1429
    def _cmd_get_memory_name(self, number):
1430
        return "MN", "%03i" % number
1431

    
1432
    def _cmd_set_memory(self, number, spec):
1433
        return "ME", "%03i,%s" % (number, spec)
1434

    
1435
    def _cmd_set_memory_name(self, number, name):
1436
        return "MN", "%03i,%s" % (number, name)
(4-4/10)