Project

General

Profile

Bug #10312 » kenwood_live.py

Dan Smith, 01/26/2023 02:19 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

    
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
    _upper = 399
886
    _kenwood_split = True
887
    _kenwood_valid_tones = list(KENWOOD_TONES)
888

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

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

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

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

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

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

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

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

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

    
948
        return mem
949

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

    
974
        return spec
975

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

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

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

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

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

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

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

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

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

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

    
1066
        return top
1067

    
1068

    
1069
@directory.register
1070
class THF7ERadio(THF6ARadio):
1071
    """Kenwood TH-F7"""
1072
    MODEL = "TH-F7"
1073

    
1074

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

    
1080

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

    
1086
    _upper = 999
1087
    _kenwood_valid_tones = list(KENWOOD_TONES)
1088

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

    
1104
    def _cmd_get_memory(self, number):
1105
        return "ME", "%03i" % number
1106

    
1107
    def _cmd_get_memory_name(self, number):
1108
        return "MN", "%03i" % number
1109

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

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

    
1116
    def _parse_mem_spec(self, spec):
1117
        mem = chirp_common.Memory()
1118

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

    
1142
        return mem
1143

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

    
1164
        return spec
1165

    
1166

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

    
1173
    def _parse_mem_spec(self, spec):
1174
        mem = chirp_common.Memory()
1175

    
1176
        mem.number = int(spec[0])
1177
        mem.freq = int(spec[1])
1178
        mem.tuning_step = D710_STEPS[int(spec[2], 16)]
1179
        mem.duplex = D710_DUPLEX[int(spec[3])]
1180
        # Reverse
1181
        if int(spec[5]):
1182
            mem.tmode = "Tone"
1183
        elif int(spec[6]):
1184
            mem.tmode = "TSQL"
1185
        elif int(spec[7]):
1186
            mem.tmode = "DTCS"
1187
        mem.rtone = self._kenwood_valid_tones[int(spec[9])]
1188
        mem.ctone = self._kenwood_valid_tones[int(spec[10])]
1189
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
1190
        mem.offset = int(spec[13])
1191
        mem.mode = D710_MODES[int(spec[14])]
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[17])]  # Memory Lockout
1198

    
1199
        return mem
1200

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

    
1223
        return spec
1224

    
1225

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

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

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

    
1249
    def _cmd_get_memory_name(self, number):
1250
        return ''
1251

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

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

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

    
1285
        return mem
1286

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

    
1313
        return spec
1314

    
1315

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

    
1321

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

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

    
1336

    
1337
THK2_DUPLEX = ["", "+", "-"]
1338
THK2_MODES = ["FM", "NFM"]
1339

    
1340
THK2_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/"
1341

    
1342

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

    
1348
    _kenwood_valid_tones = list(KENWOOD_TONES)
1349

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

    
1367
    def _cmd_get_memory(self, number):
1368
        return "ME", "%02i" % number
1369

    
1370
    def _cmd_get_memory_name(self, number):
1371
        return "MN", "%02i" % number
1372

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

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

    
1379
    def _parse_mem_spec(self, spec):
1380
        mem = chirp_common.Memory()
1381

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

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

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

    
1427

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

    
1430

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

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

    
1453
    def _cmd_get_memory(self, number):
1454
        return "ME", "%03i" % number
1455

    
1456
    def _cmd_get_memory_name(self, number):
1457
        return "MN", "%03i" % number
1458

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

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

    
1465

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

    
1472

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

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

    
1495
    def _cmd_get_memory(self, number):
1496
        return "ME", "%03i" % number
1497

    
1498
    def _cmd_get_memory_name(self, number):
1499
        return "MN", "%03i" % number
1500

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

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

    
1507

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

    
1513
    _kenwood_valid_tones = list(KENWOOD_TONES)
1514
    _kenwood_valid_tones.append(1750)
1515

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

    
1533
    def get_features(self):
1534
        rf = chirp_common.RadioFeatures()
1535

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

    
1550
        rf.memory_bounds = (0, self._upper)
1551

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

    
1561
        return rf
1562

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

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

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

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

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

    
1693

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

    
1699
    _kenwood_valid_tones = list(KENWOOD_TONES)
1700
    _kenwood_valid_tones.append(1750)
1701

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

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

    
1722
    def get_features(self):
1723
        rf = chirp_common.RadioFeatures()
1724

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

    
1739
        rf.memory_bounds = (0, self._upper)
1740

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

    
1751
        return rf
1752

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

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

    
1810
        return mem
1811

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

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