Project

General

Profile

Bug #10312 » kenwood_live.py

Dan Smith, 01/26/2023 03:10 PM

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

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

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

    
27
LOG = logging.getLogger(__name__)
28

    
29
NOCACHE = "CHIRP_NOCACHE" in os.environ
30

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

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

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

    
48

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

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

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

    
67

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

    
72
    start = time.time()
73

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

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

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

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

    
96
    return result.strip()
97

    
98

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

    
103

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

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

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

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

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

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

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

    
153

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

    
165

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

    
170

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

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

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

    
187
        self._memcache = {}
188

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
260
        return mem
261

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
374

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

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

    
388
        return KenwoodLiveRadio.set_memory(self, memory)
389

    
390

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

    
396
    _kenwood_split = True
397

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

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

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

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

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

    
470
        return spec
471

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

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

    
493
        return mem
494

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

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

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

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

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

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

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

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

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

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

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

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

    
604
        return top
605

    
606

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

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

    
617

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

    
623
    _kenwood_split = True
624

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

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

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

    
671
        return spec
672

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

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

    
694
        return mem
695

    
696

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

    
702
    mem_upper_limit = 200  # Will be updated
703

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

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

    
735
        return spec
736

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

    
750
        return mem
751

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

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

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

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

    
779
        return True
780

    
781
    def _detect_split(self):
782
        return 50
783

    
784

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

    
791

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

    
797

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

    
803

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

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

    
818

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

    
821

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

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

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

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

    
872

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

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

    
879

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

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

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

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

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

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

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

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

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

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

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

    
949
        return mem
950

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

    
975
        return spec
976

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

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

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

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

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

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

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

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

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

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

    
1067
        return top
1068

    
1069

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

    
1076

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

    
1082

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

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

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

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

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

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

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

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

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

    
1144
        return mem
1145

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

    
1166
        return spec
1167

    
1168

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

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

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

    
1201
        return mem
1202

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

    
1225
        return spec
1226

    
1227

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

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

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

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

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

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

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

    
1287
        return mem
1288

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

    
1315
        return spec
1316

    
1317

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

    
1323

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

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

    
1338

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

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

    
1344

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

    
1350
    _kenwood_valid_tones = list(KENWOOD_TONES)
1351

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

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

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

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

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

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

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

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

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

    
1429

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

    
1432

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

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

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

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

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

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

    
1467

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

    
1474

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

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

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

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

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

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

    
1509

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

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

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

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

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

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

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

    
1563
        return rf
1564

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

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

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

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

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

    
1695

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

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

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

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

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

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

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

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

    
1753
        return rf
1754

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

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

    
1812
        return mem
1813

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

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