Project

General

Profile

Bug #867 ยป kenwood_live.py

D7 and D700 tone list fix - Tom Hayward, 05/14/2013 12:28 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

    
21
NOCACHE = os.environ.has_key("CHIRP_NOCACHE")
22

    
23
if __name__ == "__main__":
24
    import sys
25
    sys.path.insert(0, "..")
26

    
27
from chirp import chirp_common, errors, directory, util
28
from chirp.settings import RadioSetting, RadioSettingGroup, \
29
    RadioSettingValueInteger, RadioSettingValueBoolean, \
30
    RadioSettingValueString, RadioSettingValueList
31

    
32
DEBUG = True
33

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

    
39
THF6_MODES = ["FM", "WFM", "AM", "LSB", "USB", "CW"]
40

    
41
LOCK = threading.Lock()
42

    
43
def command(ser, cmd, *args):
44
    """Send @cmd to radio via @ser"""
45
    global LOCK
46

    
47
    start = time.time()
48

    
49
    LOCK.acquire()
50
    if args:
51
        cmd += " " + " ".join(args)
52
    if DEBUG:
53
        print "PC->RADIO: %s" % cmd
54
    ser.write(cmd + "\r")
55

    
56
    result = ""
57
    while not result.endswith("\r"):
58
        result += ser.read(8)
59
        if (time.time() - start) > 0.5:
60
            print "Timeout waiting for data"
61
            break
62

    
63
    if DEBUG:
64
        print "D7->PC: %s" % result.strip()
65

    
66
    LOCK.release()
67

    
68
    return result.strip()
69

    
70
LAST_BAUD = 9600
71
def get_id(ser):
72
    """Get the ID of the radio attached to @ser"""
73
    global LAST_BAUD
74
    bauds = [9600, 19200, 38400, 57600]
75
    bauds.remove(LAST_BAUD)
76
    bauds.insert(0, LAST_BAUD)
77

    
78
    for i in bauds:
79
        print "Trying ID at baud %i" % i
80
        ser.setBaudrate(i)
81
        ser.write("\r")
82
        ser.read(25)
83
        resp = command(ser, "ID")
84
        if " " in resp:
85
            LAST_BAUD = i
86
            return resp.split(" ")[1]
87

    
88
    raise errors.RadioError("No response from radio")
89

    
90
def get_tmode(tone, ctcss, dcs):
91
    """Get the tone mode based on the values of the tone, ctcss, dcs"""
92
    if dcs and int(dcs) == 1:
93
        return "DTCS"
94
    elif int(ctcss):
95
        return "TSQL"
96
    elif int(tone):
97
        return "Tone"
98
    else:
99
        return ""
100

    
101
def iserr(result):
102
    """Returns True if the @result from a radio is an error"""
103
    return result in ["N", "?"]
104

    
105
class KenwoodLiveRadio(chirp_common.LiveRadio):
106
    """Base class for all live-mode kenwood radios"""
107
    BAUD_RATE = 9600
108
    VENDOR = "Kenwood"
109
    MODEL = ""
110

    
111
    _vfo = 0
112
    _upper = 200
113
    _kenwood_split = False
114
    _kenwood_valid_tones = list(chirp_common.TONES)
115

    
116
    def __init__(self, *args, **kwargs):
117
        chirp_common.LiveRadio.__init__(self, *args, **kwargs)
118

    
119
        self.__memcache = {}
120

    
121
        if self.pipe:
122
            self.pipe.setTimeout(0.1)
123
            radio_id = get_id(self.pipe)
124
            if radio_id != self.MODEL.split(" ")[0]:
125
                raise Exception("Radio reports %s (not %s)" % (radio_id,
126
                                                               self.MODEL))
127

    
128
            command(self.pipe, "AI", "0")
129

    
130
    def _cmd_get_memory(self, number):
131
        return "MR", "%i,0,%03i" % (self._vfo, number)
132

    
133
    def _cmd_get_memory_name(self, number):
134
        return "MNA", "%i,%03i" % (self._vfo, number)
135

    
136
    def _cmd_get_split(self, number):
137
        return "MR", "%i,1,%03i" % (self._vfo, number)
138

    
139
    def _cmd_set_memory(self, number, spec):
140
        if spec:
141
            spec = "," + spec
142
        return "MW", "%i,0,%03i%s" % (self._vfo, number, spec)
143

    
144
    def _cmd_set_memory_name(self, number, name):
145
        return "MNA", "%i,%03i,%s" % (self._vfo, number, name)
146

    
147
    def _cmd_set_split(self, number, spec):
148
        return "MW", "%i,1,%03i,%s" % (self._vfo, number, spec)
149

    
150
    def get_raw_memory(self, number):
151
        return command(self.pipe, *self._cmd_get_memory(number))
152

    
153
    def get_memory(self, number):
154
        if number < 0 or number > self._upper:
155
            raise errors.InvalidMemoryLocation( \
156
                "Number must be between 0 and %i" % self._upper)
157
        if self.__memcache.has_key(number) and not NOCACHE:
158
            return self.__memcache[number]
159

    
160
        result = command(self.pipe, *self._cmd_get_memory(number))
161
        if result == "N" or result == "E":
162
            mem = chirp_common.Memory()
163
            mem.number = number
164
            mem.empty = True
165
            self.__memcache[mem.number] = mem
166
            return mem
167
        elif " " not in result:
168
            print "Not sure what to do with this: `%s'" % result
169
            raise errors.RadioError("Unexpected result returned from radio")
170

    
171
        value = result.split(" ")[1]
172
        spec = value.split(",")
173

    
174
        mem = self._parse_mem_spec(spec)
175
        self.__memcache[mem.number] = mem
176

    
177
        result = command(self.pipe, *self._cmd_get_memory_name(number))
178
        if " " in result:
179
            value = result.split(" ", 1)[1]
180
            if value.count(",") == 2:
181
                _zero, _loc, mem.name = value.split(",")
182
            else:
183
                _loc, mem.name = value.split(",")
184
 
185
        if mem.duplex == "" and self._kenwood_split:
186
            result = command(self.pipe, *self._cmd_get_split(number))
187
            if " " in result:
188
                value = result.split(" ", 1)[1]
189
                self._parse_split_spec(mem, value.split(","))
190

    
191
        return mem
192

    
193
    def _make_mem_spec(self, mem):
194
        pass
195

    
196
    def _parse_mem_spec(self, spec):
197
        pass
198

    
199
    def _parse_split_spec(self, mem, spec):
200
        mem.duplex = "split"
201
        mem.offset = int(spec[2])
202

    
203
    def _make_split_spec(self, mem):
204
        return ("%011i" % mem.offset, "0")
205

    
206
    def set_memory(self, memory):
207
        if memory.number < 0 or memory.number > self._upper:
208
            raise errors.InvalidMemoryLocation( \
209
                "Number must be between 0 and %i" % self._upper)
210

    
211
        spec = self._make_mem_spec(memory)
212
        spec = ",".join(spec)
213
        r1 = command(self.pipe, *self._cmd_set_memory(memory.number, spec))
214
        if not iserr(r1):
215
            time.sleep(0.5)
216
            r2 = command(self.pipe, *self._cmd_set_memory_name(memory.number,
217
                                                               memory.name))
218
            if not iserr(r2):
219
                memory.name = memory.name.rstrip()
220
                self.__memcache[memory.number] = memory
221
            else:
222
                raise errors.InvalidDataError("Radio refused name %i: %s" %\
223
                                                  (memory.number,
224
                                                   repr(memory.name)))
225
        else:
226
            raise errors.InvalidDataError("Radio refused %i" % memory.number)
227

    
228
        if memory.duplex == "split" and self._kenwood_split: 
229
            spec = ",".join(self._make_split_spec(memory))
230
            result = command(self.pipe, *self._cmd_set_split(memory.number,
231
                                                             spec))
232
            if iserr(result):
233
                raise errors.InvalidDataError("Radio refused %i" % \
234
                                                  memory.number)
235

    
236
    def erase_memory(self, number):
237
        if not self.__memcache.has_key(number):
238
            return
239

    
240
        resp = command(self.pipe, *self._cmd_set_memory(number, ""))
241
        if iserr(resp):
242
            raise errors.RadioError("Radio refused delete of %i" % number)
243
        del self.__memcache[number]
244

    
245
TH_D7_SETTINGS = {
246
    "BAL"  : ["4:0", "3:1", "2:2", "1:3", "0:4"],
247
    "BEP"  : ["Off", "Key", "Key+Data", "All"],
248
    "BEPT" : ["Off", "Mine", "All New"], # D700 has fourth "All"
249
    "DS"   : ["Data Band", "Both Bands"],
250
    "DTB"  : ["A", "B"],
251
    "DTBA" : ["A", "B", "A:TX/B:RX"], # D700 has fourth A:RX/B:TX
252
    "DTX"  : ["Manual", "PTT", "Auto"],
253
    "ICO"  : ["Kenwood", "Runner", "House", "Tent", "Boat", "SSTV",
254
              "Plane", "Speedboat", "Car", "Bicycle"],
255
    "MNF"  : ["Name", "Frequency"],
256
    "PKSA" : ["1200", "9600"],
257
    "POSC" : ["Off Duty", "Enroute", "In Service", "Returning",
258
              "Committed", "Special", "Priority", "Emergency"],
259
    "PT"   : ["100ms", "200ms", "500ms", "750ms", "1000ms", "1500ms", "2000ms"],
260
    "SCR"  : ["Time", "Carrier", "Seek"],
261
    "SV"   : ["Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s",
262
              "2s", "3s", "4s", "5s"],
263
    "TEMP" : ["F", "C"],
264
    "TXI"  : ["30sec", "1min", "2min", "3min", "4min", "5min",
265
              "10min", "20min", "30min"],
266
    "UNIT" : ["English", "Metric"],
267
    "WAY"  : ["Off", "6 digit NMEA", "7 digit NMEA", "8 digit NMEA",
268
              "9 digit NMEA", "6 digit Magellan", "DGPS"],
269
}
270

    
271
class KenwoodOldLiveRadio(KenwoodLiveRadio):
272
    _kenwood_valid_tones = list(chirp_common.OLD_TONES)
273

    
274
    def set_memory(self, memory):
275
        supported_tones = list(chirp_common.OLD_TONES)
276
        supported_tones.remove(69.3)
277
        if memory.rtone not in supported_tones:
278
            raise errors.UnsupportedToneError("This radio does not support " +
279
                                              "tone %.1fHz" % memory.rtone)
280
        if memory.ctone not in supported_tones:
281
            raise errors.UnsupportedToneError("This radio does not support " +
282
                                              "tone %.1fHz" % memory.ctone)
283

    
284
        return KenwoodLiveRadio.set_memory(self, memory)
285

    
286
@directory.register
287
class THD7Radio(KenwoodOldLiveRadio):
288
    """Kenwood TH-D7"""
289
    MODEL = "TH-D7"
290

    
291
    _kenwood_split = True
292

    
293
    def get_features(self):
294
        rf = chirp_common.RadioFeatures()
295
        rf.has_settings = True
296
        rf.has_dtcs = False
297
        rf.has_dtcs_polarity = False
298
        rf.has_bank = False
299
        rf.has_mode = True
300
        rf.has_tuning_step = False
301
        rf.can_odd_split = True
302
        rf.valid_duplexes = ["", "-", "+", "split"]
303
        rf.valid_modes = MODES.values()
304
        rf.valid_tmodes = ["", "Tone", "TSQL"]
305
        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
306
        rf.valid_name_length = 7
307
        rf.memory_bounds = (1, self._upper)
308
        return rf
309

    
310
    def _make_mem_spec(self, mem):
311
        if mem.duplex in " -+":
312
            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
313
            offset = mem.offset
314
        else:
315
            duplex = 0
316
            offset = 0
317
        
318
        spec = ( \
319
            "%011i" % mem.freq,
320
            "%X" % STEPS.index(mem.tuning_step),
321
            "%i" % duplex,
322
            "0",
323
            "%i" % (mem.tmode == "Tone"),
324
            "%i" % (mem.tmode == "TSQL"),
325
            "", # DCS Flag
326
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
327
            "", # DCS Code
328
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
329
            "%09i" % offset,
330
            "%i" % util.get_dict_rev(MODES, mem.mode),
331
            "%i" % ((mem.skip == "S") and 1 or 0))
332

    
333
        return spec
334

    
335
    def _parse_mem_spec(self, spec):
336
        mem = chirp_common.Memory()
337

    
338
        mem.number = int(spec[2])
339
        mem.freq = int(spec[3], 10)
340
        mem.tuning_step = STEPS[int(spec[4], 16)]
341
        mem.duplex = DUPLEX[int(spec[5])]
342
        mem.tmode = get_tmode(spec[7], spec[8], spec[9])
343
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
344
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
345
        if spec[11] and spec[11].isdigit():
346
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
347
        else:
348
            print "Unknown or invalid DCS: %s" % spec[11]
349
        if spec[13]:
350
            mem.offset = int(spec[13])
351
        else:
352
            mem.offset = 0
353
        mem.mode = MODES[int(spec[14])]
354
        mem.skip = int(spec[15]) and "S" or ""
355

    
356
        return mem
357

    
358
    def _kenwood_get(self, cmd):
359
        resp = command(self.pipe, cmd)
360
        if " " in resp:
361
            return resp.split(" ", 1)
362
        else:
363
            raise errors.RadioError("Radio refused to return %s" % cmd)
364

    
365
    def _kenwood_set(self, cmd, value):
366
        resp = command(self.pipe, cmd, value)
367
        if " " in resp:
368
            return
369
        raise errors.RadioError("Radio refused to set %s" % cmd)
370

    
371
    def _kenwood_get_bool(self, cmd):
372
        _cmd, result = self._kenwood_get(cmd)
373
        return result == "1"
374

    
375
    def _kenwood_set_bool(self, cmd, value):
376
        return self._kenwood_set(cmd, str(int(value)))
377

    
378
    def _kenwood_get_int(self, cmd):
379
        _cmd, result = self._kenwood_get(cmd)
380
        return int(result)
381

    
382
    def _kenwood_set_int(self, cmd, value, digits=1):
383
        return self._kenwood_set(cmd, ("%%0%ii" % digits) % value)
384
    
385
    def get_settings(self):
386
        aux = RadioSettingGroup("aux", "Aux")
387
        tnc = RadioSettingGroup("tnc", "TNC")
388
        save = RadioSettingGroup("save", "Save")
389
        display = RadioSettingGroup("display", "Display")
390
        dtmf = RadioSettingGroup("dtmf", "DTMF")
391
        radio = RadioSettingGroup("radio", "Radio",
392
                                  aux, tnc, save, display, dtmf)
393
        sky = RadioSettingGroup("sky", "SkyCommand")
394
        aprs = RadioSettingGroup("aprs", "APRS")
395
        top = RadioSettingGroup("top", "All Settings", radio, aprs, sky)
396

    
397
        bools = [("AMR", aprs, "APRS Message Auto-Reply"),
398
                 ("AIP", aux, "Advanced Intercept Point"),
399
                 ("ARO", aux, "Automatic Repeater Offset"),
400
                 ("BCN", aprs, "Beacon"),
401
                 ("CH", radio, "Channel Mode Display"),
402
                 #("DIG", aprs, "APRS Digipeater"),
403
                 ("DL", all, "Dual"),
404
                 ("LK", all, "Lock"),
405
                 ("LMP", all, "Lamp"),
406
                 ("TSP", dtmf, "DTMF Fast Transmission"),
407
                 ("TXH", dtmf, "TX Hold"),
408
                 ]
409

    
410
        for setting, group, name in bools:
411
            value = self._kenwood_get_bool(setting)
412
            rs = RadioSetting(setting, name,
413
                              RadioSettingValueBoolean(value))
414
            group.append(rs)
415

    
416
        lists = [("BAL", all, "Balance"),
417
                 ("BEP", aux, "Beep"),
418
                 ("BEPT", aprs, "APRS Beep"),
419
                 ("DS", tnc, "Data Sense"),
420
                 ("DTB", tnc, "Data Band"),
421
                 ("DTBA", aprs, "APRS Data Band"),
422
                 ("DTX", aprs, "APRS Data TX"),
423
                 #("ICO", aprs, "APRS Icon"),
424
                 ("MNF", all, "Memory Display Mode"),
425
                 ("PKSA", aprs, "APRS Packet Speed"),
426
                 ("POSC", aprs, "APRS Position Comment"),
427
                 ("PT", dtmf, "DTMF Speed"),
428
                 ("SV", save, "Battery Save"),
429
                 ("TEMP", aprs, "APRS Temperature Units"),
430
                 ("TXI", aprs, "APRS Transmit Interval"),
431
                 #("UNIT", aprs, "APRS Display Units"),
432
                 ("WAY", aprs, "Waypoint Mode"),
433
                 ]
434

    
435
        for setting, group, name in lists:
436
            value = self._kenwood_get_int(setting)
437
            options = TH_D7_SETTINGS[setting]
438
            rs = RadioSetting(setting, name,
439
                              RadioSettingValueList(options,
440
                                                    options[value]))
441
            group.append(rs)
442

    
443
        ints = [("CNT", display, "Contrast", 1, 16),
444
                ]
445
        for setting, group, name, minv, maxv in ints:
446
            value = self._kenwood_get_int(setting)
447
            rs = RadioSetting(setting, name,
448
                              RadioSettingValueInteger(minv, maxv, value))
449
            group.append(rs)
450

    
451
        strings = [("MES", display, "Power-on Message", 8),
452
                   ("MYC", aprs, "APRS Callsign", 8),
453
                   ("PP", aprs, "APRS Path", 32),
454
                   ("SCC", sky, "SkyCommand Callsign", 8),
455
                   ("SCT", sky, "SkyCommand To Callsign", 8),
456
                   #("STAT", aprs, "APRS Status Text", 32),
457
                   ]
458
        for setting, group, name, length in strings:
459
            _cmd, value = self._kenwood_get(setting)
460
            rs = RadioSetting(setting, name,
461
                              RadioSettingValueString(0, length, value))
462
            group.append(rs)
463

    
464
        return top
465

    
466
    def set_settings(self, settings):
467
        for element in settings:
468
            if not element.changed():
469
                continue
470
            if isinstance(element.value, RadioSettingValueBoolean):
471
                self._kenwood_set_bool(element.get_name(), element.value)
472
            elif isinstance(element.value, RadioSettingValueList):
473
                options = TH_D7_SETTINGS[element.get_name()]
474
                self._kenwood_set_int(element.get_name(),
475
                                      options.index(str(element.value)))
476
            elif isinstance(element.value, RadioSettingValueInteger):
477
                if element.value.get_max() > 9:
478
                    digits = 2
479
                else:
480
                    digits = 1
481
                self._kenwood_set_int(element.get_name(), element.value, digits)
482
            elif isinstance(element.value, RadioSettingValueString):
483
                self._kenwood_set(element.get_name(), str(element.value))
484
            else:
485
                print "Unknown type %s" % element.value
486

    
487
@directory.register
488
class THD7GRadio(THD7Radio):
489
    """Kenwood TH-D7G"""
490
    MODEL = "TH-D7G"
491

    
492
@directory.register
493
class TMD700Radio(KenwoodOldLiveRadio):
494
    """Kenwood TH-D700"""
495
    MODEL = "TM-D700"
496

    
497
    _kenwood_split = True
498

    
499
    def get_features(self):
500
        rf = chirp_common.RadioFeatures()
501
        rf.has_dtcs = True
502
        rf.has_dtcs_polarity = False
503
        rf.has_bank = False
504
        rf.has_mode = False
505
        rf.has_tuning_step = False
506
        rf.can_odd_split = True
507
        rf.valid_duplexes = ["", "-", "+", "split"]
508
        rf.valid_modes = ["FM"]
509
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
510
        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
511
        rf.valid_name_length = 8
512
        rf.memory_bounds = (1, self._upper)
513
        return rf
514

    
515
    def _make_mem_spec(self, mem):
516
        if mem.duplex in " -+":
517
            duplex = util.get_dict_rev(DUPLEX, mem.duplex)
518
        else:
519
            duplex = 0
520
        spec = ( \
521
            "%011i" % mem.freq,
522
            "%X" % STEPS.index(mem.tuning_step),
523
            "%i" % duplex,
524
            "0",
525
            "%i" % (mem.tmode == "Tone"),
526
            "%i" % (mem.tmode == "TSQL"),
527
            "%i" % (mem.tmode == "DTCS"),
528
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
529
            "%03i0" % (chirp_common.DTCS_CODES.index(mem.dtcs) + 1),
530
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
531
            "%09i" % mem.offset,
532
            "%i" % util.get_dict_rev(MODES, mem.mode),
533
            "%i" % ((mem.skip == "S") and 1 or 0))
534

    
535
        return spec
536

    
537
    def _parse_mem_spec(self, spec):
538
        mem = chirp_common.Memory()
539

    
540
        mem.number = int(spec[2])
541
        mem.freq = int(spec[3])
542
        mem.tuning_step = STEPS[int(spec[4], 16)]
543
        mem.duplex = DUPLEX[int(spec[5])]
544
        mem.tmode = get_tmode(spec[7], spec[8], spec[9])
545
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
546
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
547
        if spec[11] and spec[11].isdigit():
548
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1]
549
        else:
550
            print "Unknown or invalid DCS: %s" % spec[11]
551
        if spec[13]:
552
            mem.offset = int(spec[13])
553
        else:
554
            mem.offset = 0
555
        mem.mode = MODES[int(spec[14])]
556
        mem.skip = int(spec[15]) and "S" or ""
557

    
558
        return mem
559

    
560
@directory.register
561
class TMV7Radio(KenwoodOldLiveRadio):
562
    """Kenwood TM-V7"""
563
    MODEL = "TM-V7"
564

    
565
    mem_upper_limit = 200 # Will be updated
566

    
567
    def get_features(self):
568
        rf = chirp_common.RadioFeatures()
569
        rf.has_dtcs = False
570
        rf.has_dtcs_polarity = False
571
        rf.has_bank = False
572
        rf.has_mode = False
573
        rf.has_tuning_step = False
574
        rf.valid_modes = ["FM"]
575
        rf.valid_tmodes = ["", "Tone", "TSQL"]
576
        rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC
577
        rf.valid_name_length = 7
578
        rf.has_sub_devices = True
579
        rf.memory_bounds = (1, self._upper)
580
        return rf
581

    
582
    def _make_mem_spec(self, mem):
583
        spec = ( \
584
            "%011i" % mem.freq,
585
            "%X" % STEPS.index(mem.tuning_step),
586
            "%i" % util.get_dict_rev(DUPLEX, mem.duplex),
587
            "0",
588
            "%i" % (mem.tmode == "Tone"),
589
            "%i" % (mem.tmode == "TSQL"),
590
            "0",
591
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1),
592
            "000",
593
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1),
594
            "",
595
            "0")
596

    
597
        return spec
598

    
599
    def _parse_mem_spec(self, spec):
600
        mem = chirp_common.Memory()
601
        mem.number = int(spec[2])
602
        mem.freq = int(spec[3])
603
        mem.tuning_step = STEPS[int(spec[4], 16)]
604
        mem.duplex = DUPLEX[int(spec[5])]
605
        if int(spec[7]):
606
            mem.tmode = "Tone"
607
        elif int(spec[8]):
608
            mem.tmode = "TSQL"
609
        mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1]
610
        mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1]
611

    
612
        return mem
613

    
614
    def get_sub_devices(self):
615
        return [TMV7RadioVHF(self.pipe), TMV7RadioUHF(self.pipe)]
616

    
617
    def __test_location(self, loc):
618
        mem = self.get_memory(loc)
619
        if not mem.empty:
620
            # Memory was not empty, must be valid
621
            return True
622

    
623
        # Mem was empty (or invalid), try to set it
624
        if self._vfo == 0:
625
            mem.freq = 144000000
626
        else:
627
            mem.freq = 440000000
628
        mem.empty = False
629
        try:
630
            self.set_memory(mem)
631
        except Exception:
632
            # Failed, so we're past the limit
633
            return False
634

    
635
        # Erase what we did
636
        try:
637
            self.erase_memory(loc)
638
        except Exception:
639
            pass # V7A Can't delete just yet
640

    
641
        return True
642

    
643
    def _detect_split(self):
644
        return 50
645

    
646
class TMV7RadioSub(TMV7Radio):
647
    """Base class for the TM-V7 sub devices"""
648
    def __init__(self, pipe):
649
        TMV7Radio.__init__(self, pipe)
650
        self._detect_split()
651

    
652
class TMV7RadioVHF(TMV7RadioSub):
653
    """TM-V7 VHF subdevice"""
654
    VARIANT = "VHF"
655
    _vfo = 0
656

    
657
class TMV7RadioUHF(TMV7RadioSub):
658
    """TM-V7 UHF subdevice"""
659
    VARIANT = "UHF"
660
    _vfo = 1
661

    
662
@directory.register
663
class TMG707Radio(TMV7Radio):
664
    """Kenwood TM-G707"""
665
    MODEL = "TM-G707"
666
    
667
    def get_features(self):
668
        rf = TMV7Radio.get_features(self)
669
        rf.has_sub_devices = False
670
        rf.memory_bounds = (1, 180)
671
        rf.valid_bands = [(144000000, 148000000),
672
                          (430000000, 450000000)]
673
        return rf
674

    
675
THF6A_STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
676
               100.0]
677

    
678
THF6A_DUPLEX = dict(DUPLEX)
679
THF6A_DUPLEX[3] = "split"
680

    
681
@directory.register
682
class THF6ARadio(KenwoodLiveRadio):
683
    """Kenwood TH-F6"""
684
    MODEL = "TH-F6"
685

    
686
    _upper = 399
687
    _kenwood_split = True
688

    
689
    def get_features(self):
690
        rf = chirp_common.RadioFeatures()
691
        rf.has_dtcs_polarity = False
692
        rf.has_bank = False
693
        rf.can_odd_split = True
694
        rf.valid_modes = list(THF6_MODES)
695
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
696
        rf.valid_tuning_steps = list(THF6A_STEPS)
697
        rf.valid_bands = [(1000, 1300000000)]
698
        rf.valid_skips = ["", "S"]
699
        rf.valid_duplexes = THF6A_DUPLEX.values()
700
        rf.valid_characters = chirp_common.CHARSET_ASCII
701
        rf.valid_name_length = 8
702
        rf.memory_bounds = (0, self._upper)
703
        return rf
704

    
705
    def _cmd_set_memory(self, number, spec):
706
        if spec:
707
            spec = "," + spec
708
        return "MW", "0,%03i%s" % (number, spec)
709

    
710
    def _cmd_get_memory(self, number):
711
        return "MR", "0,%03i" % number
712

    
713
    def _cmd_get_memory_name(self, number):
714
        return "MNA", "%03i" % number
715

    
716
    def _cmd_set_memory_name(self, number, name):
717
        return "MNA", "%03i,%s" % (number, name)
718

    
719
    def _cmd_get_split(self, number):
720
        return "MR", "1,%03i" % number
721

    
722
    def _cmd_set_split(self, number, spec):
723
        return "MW", "1,%03i,%s" % (number, spec)
724

    
725
    def _parse_mem_spec(self, spec):
726
        mem = chirp_common.Memory()
727

    
728
        mem.number = int(spec[1])
729
        mem.freq = int(spec[2])
730
        mem.tuning_step = THF6A_STEPS[int(spec[3], 16)]
731
        mem.duplex = THF6A_DUPLEX[int(spec[4])]
732
        mem.tmode = get_tmode(spec[6], spec[7], spec[8])
733
        mem.rtone = self._kenwood_valid_tones[int(spec[9])]
734
        mem.ctone = self._kenwood_valid_tones[int(spec[10])]
735
        if spec[11] and spec[11].isdigit():
736
            mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
737
        else:
738
            print "Unknown or invalid DCS: %s" % spec[11]
739
        if spec[12]:
740
            mem.offset = int(spec[12])
741
        else:
742
            mem.offset = 0
743
        mem.mode = THF6_MODES[int(spec[13])]
744
        if spec[14] == "1":
745
            mem.skip = "S"
746

    
747
        return mem
748

    
749
    def _make_mem_spec(self, mem):
750
        if mem.duplex in " +-":
751
            duplex = util.get_dict_rev(THF6A_DUPLEX, mem.duplex)
752
            offset = mem.offset
753
        elif mem.duplex == "split":
754
            duplex = 0
755
            offset = 0
756
        else:
757
            print "Bug: unsupported duplex `%s'" % mem.duplex
758
        spec = ( \
759
            "%011i" % mem.freq,
760
            "%X" % THF6A_STEPS.index(mem.tuning_step),
761
            "%i" % duplex,
762
            "0",
763
            "%i" % (mem.tmode == "Tone"),
764
            "%i" % (mem.tmode == "TSQL"),
765
            "%i" % (mem.tmode == "DTCS"),
766
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
767
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
768
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
769
            "%09i" % offset,
770
            "%i" % (THF6_MODES.index(mem.mode)),
771
            "%i" % (mem.skip == "S"))
772

    
773
        return spec
774

    
775
@directory.register
776
class THF7ERadio(THF6ARadio):
777
    """Kenwood TH-F7"""
778
    MODEL = "TH-F7"
779

    
780
D710_DUPLEX = ["", "+", "-", "split"]
781
D710_MODES = ["FM", "NFM", "AM"]
782
D710_SKIP = ["", "S"]
783
D710_STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
784
D710_TONES = list(chirp_common.TONES)
785
D710_TONES.remove(159.8)
786
D710_TONES.remove(165.5)
787
D710_TONES.remove(171.3)
788
D710_TONES.remove(177.3)
789
D710_TONES.remove(183.5)
790
D710_TONES.remove(189.9)
791
D710_TONES.remove(196.6)
792
D710_TONES.remove(199.5)
793

    
794
@directory.register
795
class TMD710Radio(KenwoodLiveRadio):
796
    """Kenwood TM-D710"""
797
    MODEL = "TM-D710"
798
    
799
    _upper = 999
800
    _kenwood_valid_tones = list(D710_TONES)
801

    
802
    def get_features(self):
803
        rf = chirp_common.RadioFeatures()
804
        rf.can_odd_split = True
805
        rf.has_dtcs_polarity = False
806
        rf.has_bank = False
807
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
808
        rf.valid_modes = D710_MODES
809
        rf.valid_duplexes = D710_DUPLEX
810
        rf.valid_tuning_steps = D710_STEPS
811
        rf.valid_characters = chirp_common.CHARSET_ASCII.replace(',','')
812
        rf.valid_name_length = 8
813
        rf.valid_skips = D710_SKIP
814
        rf.memory_bounds = (0, 999)
815
        return rf
816

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

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

    
823
    def _cmd_set_memory(self, number, spec):
824
        return "ME", "%03i,%s" % (number, spec)
825

    
826
    def _cmd_set_memory_name(self, number, name):
827
        return "MN", "%03i,%s" % (number, name)
828

    
829
    def _parse_mem_spec(self, spec):
830
        mem = chirp_common.Memory()
831

    
832
        mem.number = int(spec[0])
833
        mem.freq = int(spec[1])
834
        mem.tuning_step = D710_STEPS[int(spec[2], 16)]
835
        mem.duplex = D710_DUPLEX[int(spec[3])]
836
        # Reverse
837
        if int(spec[5]):
838
            mem.tmode = "Tone"
839
        elif int(spec[6]):
840
            mem.tmode = "TSQL"
841
        elif int(spec[7]):
842
            mem.tmode = "DTCS"
843
        mem.rtone = self._kenwood_valid_tones[int(spec[8])]
844
        mem.ctone = self._kenwood_valid_tones[int(spec[9])]
845
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
846
        mem.offset = int(spec[11])
847
        mem.mode = D710_MODES[int(spec[12])]
848
        # TX Frequency
849
        if int(spec[13]):
850
            mem.duplex = "split"
851
            mem.offset = int(spec[13])
852
        # Unknown
853
        mem.skip = D710_SKIP[int(spec[15])] # Memory Lockout
854

    
855
        return mem
856

    
857
    def _make_mem_spec(self, mem):
858
        spec = ( \
859
            "%010i" % mem.freq,
860
            "%X" % D710_STEPS.index(mem.tuning_step),
861
            "%i" % (0 if mem.duplex == "split" else \
862
                        D710_DUPLEX.index(mem.duplex)),
863
            "0", # Reverse
864
            "%i" % (mem.tmode == "Tone" and 1 or 0),
865
            "%i" % (mem.tmode == "TSQL" and 1 or 0),
866
            "%i" % (mem.tmode == "DTCS" and 1 or 0),
867
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
868
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
869
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
870
            "%08i" % (0 if mem.duplex == "split" else mem.offset), # Offset
871
            "%i" % D710_MODES.index(mem.mode),
872
            "%010i" % (mem.offset if mem.duplex == "split" else 0), # TX Freq
873
            "0", # Unknown
874
            "%i" % D710_SKIP.index(mem.skip), # Memory Lockout
875
            )
876

    
877
        return spec
878

    
879
@directory.register
880
class THD72Radio(TMD710Radio):
881
    """Kenwood TH-D72"""
882
    MODEL = "TH-D72 (live mode)"
883
    HARDWARE_FLOW = sys.platform == "darwin" # only OS X driver needs hw flow
884

    
885
    def _parse_mem_spec(self, spec):
886
        mem = chirp_common.Memory()
887

    
888
        mem.number = int(spec[0])
889
        mem.freq = int(spec[1])
890
        mem.tuning_step = D710_STEPS[int(spec[2], 16)]
891
        mem.duplex = D710_DUPLEX[int(spec[3])]
892
        # Reverse
893
        if int(spec[5]):
894
            mem.tmode = "Tone"
895
        elif int(spec[6]):
896
            mem.tmode = "TSQL"
897
        elif int(spec[7]):
898
            mem.tmode = "DTCS"
899
        mem.rtone = self._kenwood_valid_tones[int(spec[9])]
900
        mem.ctone = self._kenwood_valid_tones[int(spec[10])]
901
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])]
902
        mem.offset = int(spec[13])
903
        mem.mode = D710_MODES[int(spec[14])]
904
        # TX Frequency
905
        if int(spec[15]):
906
            mem.duplex = "split"
907
            mem.offset = int(spec[15])
908
        # Lockout
909
        mem.skip = D710_SKIP[int(spec[17])] # Memory Lockout
910

    
911
        return mem
912

    
913
    def _make_mem_spec(self, mem):
914
        spec = ( \
915
            "%010i" % mem.freq,
916
            "%X" % D710_STEPS.index(mem.tuning_step),
917
            "%i" % (0 if mem.duplex == "split" else \
918
                        D710_DUPLEX.index(mem.duplex)),
919
            "0", # Reverse
920
            "%i" % (mem.tmode == "Tone" and 1 or 0),
921
            "%i" % (mem.tmode == "TSQL" and 1 or 0),
922
            "%i" % (mem.tmode == "DTCS" and 1 or 0),
923
            "0",
924
            "%02i" % (self._kenwood_valid_tones.index(mem.rtone)),
925
            "%02i" % (self._kenwood_valid_tones.index(mem.ctone)),
926
            "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)),
927
            "0",
928
            "%08i" % (0 if mem.duplex == "split" else mem.offset), # Offset
929
            "%i" % D710_MODES.index(mem.mode),
930
            "%010i" % (mem.offset if mem.duplex == "split" else 0), # TX Freq
931
            "0", # Unknown
932
            "%i" % D710_SKIP.index(mem.skip), # Memory Lockout
933
            )
934

    
935
        return spec
936

    
937
@directory.register
938
class TMV71Radio(TMD710Radio):
939
    """Kenwood TM-V71"""
940
    MODEL = "TM-V71"
941

    
942
THK2_DUPLEX = ["", "+", "-"]
943
THK2_MODES = ["FM", "NFM"]
944
THK2_TONES = list(chirp_common.TONES)
945
THK2_TONES.remove(159.8) # ??
946
THK2_TONES.remove(165.5) # ??
947
THK2_TONES.remove(171.3) # ??
948
THK2_TONES.remove(177.3) # ??
949
THK2_TONES.remove(183.5) # ??
950
THK2_TONES.remove(189.9) # ??
951
THK2_TONES.remove(196.6) # ??
952
THK2_TONES.remove(199.5) # ??
953

    
954
THK2_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/"
955

    
956
@directory.register
957
class THK2Radio(KenwoodLiveRadio):
958
    """Kenwood TH-K2"""
959
    MODEL = "TH-K2"
960

    
961
    _kenwood_valid_tones = list(THK2_TONES)
962

    
963
    def get_features(self):
964
        rf = chirp_common.RadioFeatures()
965
        rf.can_odd_split = False
966
        rf.has_dtcs_polarity = False
967
        rf.has_bank = False
968
        rf.has_tuning_step = False
969
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
970
        rf.valid_modes = THK2_MODES
971
        rf.valid_duplexes = THK2_DUPLEX
972
        rf.valid_characters = THK2_CHARS
973
        rf.valid_name_length = 6
974
        rf.valid_bands = [(136000000, 173990000)]
975
        rf.valid_skips = ["", "S"]
976
        rf.valid_tuning_steps = [5.0]
977
        rf.memory_bounds = (1, 50)
978
        return rf
979

    
980
    def _cmd_get_memory(self, number):
981
        return "ME", "%02i" % number
982

    
983
    def _cmd_get_memory_name(self, number):
984
        return "MN", "%02i" % number
985

    
986
    def _cmd_set_memory(self, number, spec):
987
        return "ME", "%02i,%s" % (number, spec)
988

    
989
    def _cmd_set_memory_name(self, number, name):
990
        return "MN", "%02i,%s" % (number, name)
991

    
992
    def _parse_mem_spec(self, spec):
993
        mem = chirp_common.Memory()
994

    
995
        mem.number = int(spec[0])
996
        mem.freq = int(spec[1])
997
        #mem.tuning_step = 
998
        mem.duplex = THK2_DUPLEX[int(spec[3])]
999
        if int(spec[5]):
1000
            mem.tmode = "Tone"
1001
        elif int(spec[6]):
1002
            mem.tmode = "TSQL"
1003
        elif int(spec[7]):
1004
            mem.tmode = "DTCS"
1005
        mem.rtone = self._kenwood_valid_tones[int(spec[8])]
1006
        mem.ctone = self._kenwood_valid_tones[int(spec[9])]
1007
        mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])]
1008
        mem.offset = int(spec[11])
1009
        mem.mode = THK2_MODES[int(spec[12])]
1010
        mem.skip = int(spec[16]) and "S" or ""
1011
        return mem
1012

    
1013
    def _make_mem_spec(self, mem):
1014
        try:
1015
            rti = self._kenwood_valid_tones.index(mem.rtone)
1016
            cti = self._kenwood_valid_tones.index(mem.ctone)
1017
        except ValueError:
1018
            raise errors.UnsupportedToneError()
1019

    
1020
        spec = ( \
1021
            "%010i" % mem.freq,
1022
            "0",
1023
            "%i"    % THK2_DUPLEX.index(mem.duplex),
1024
            "0",
1025
            "%i"    % int(mem.tmode == "Tone"),
1026
            "%i"    % int(mem.tmode == "TSQL"),
1027
            "%i"    % int(mem.tmode == "DTCS"),
1028
            "%02i"  % rti,
1029
            "%02i"  % cti,
1030
            "%03i"  % chirp_common.DTCS_CODES.index(mem.dtcs),
1031
            "%08i"  % mem.offset,
1032
            "%i"    % THK2_MODES.index(mem.mode),
1033
            "0",
1034
            "%010i" % 0,
1035
            "0",
1036
            "%i"    % int(mem.skip == "S")
1037
            )
1038
        return spec
1039
            
1040

    
1041
@directory.register
1042
class TM271Radio(THK2Radio):
1043
    """Kenwood TM-271"""
1044
    MODEL = "TM-271"
1045
    
1046
    def get_features(self):
1047
        rf = chirp_common.RadioFeatures()
1048
        rf.can_odd_split = False
1049
        rf.has_dtcs_polarity = False
1050
        rf.has_bank = False
1051
        rf.has_tuning_step = False
1052
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1053
        rf.valid_modes = THK2_MODES
1054
        rf.valid_duplexes = THK2_DUPLEX
1055
        rf.valid_characters = THK2_CHARS
1056
        rf.valid_name_length = 6
1057
        rf.valid_bands = [(137000000, 173990000)]
1058
        rf.valid_skips = ["", "S"]
1059
        rf.valid_tuning_steps = [5.0]
1060
        rf.memory_bounds = (0, 99)
1061
        return rf
1062

    
1063
    def _cmd_get_memory(self, number):
1064
        return "ME", "%03i" % number
1065

    
1066
    def _cmd_get_memory_name(self, number):
1067
        return "MN", "%03i" % number
1068

    
1069
    def _cmd_set_memory(self, number, spec):
1070
        return "ME", "%03i,%s" % (number, spec)
1071

    
1072
    def _cmd_set_memory_name(self, number, name):
1073
        return "MN", "%03i,%s" % (number, name)
1074

    
1075
def do_test():
1076
    """Dev test"""
1077
    mem = chirp_common.Memory()
1078
    mem.number = 1
1079
    mem.freq = 144000000
1080
    mem.duplex = "split"
1081
    mem.offset = 146000000
1082

    
1083
    tc = THF6ARadio
1084
    class FakeSerial:
1085
        """Faked serial line"""
1086
        buf = ""
1087
        def write(self, buf):
1088
            """Write"""
1089
            self.buf = buf
1090
        def read(self, count):
1091
            """Read"""
1092
            if self.buf[:2] == "ID":
1093
                return "ID %s\r" % tc.MODEL
1094
            return self.buf
1095
        def setTimeout(self, foo):
1096
            """Set Timeout"""
1097
            pass
1098
        def setBaudrate(self, foo):
1099
            """Set Baudrate"""
1100
            pass
1101

    
1102
    radio = tc(FakeSerial())
1103
    radio.set_memory(mem)
1104

    
1105
@directory.register
1106
class TM471Radio(THK2Radio):
1107
    """Kenwood TM-471"""
1108
    MODEL = "TM-471"
1109
    
1110
    def get_features(self):
1111
        rf = chirp_common.RadioFeatures()
1112
        rf.can_odd_split = False
1113
        rf.has_dtcs_polarity = False
1114
        rf.has_bank = False
1115
        rf.has_tuning_step = False
1116
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
1117
        rf.valid_modes = THK2_MODES
1118
        rf.valid_duplexes = THK2_DUPLEX
1119
        rf.valid_characters = THK2_CHARS
1120
        rf.valid_name_length = 6
1121
        rf.valid_bands = [(444000000, 479990000)]
1122
        rf.valid_skips = ["", "S"]
1123
        rf.valid_tuning_steps = [5.0]
1124
        rf.memory_bounds = (0, 99)
1125
        return rf
1126

    
1127
    def _cmd_get_memory(self, number):
1128
        return "ME", "%03i" % number
1129

    
1130
    def _cmd_get_memory_name(self, number):
1131
        return "MN", "%03i" % number
1132

    
1133
    def _cmd_set_memory(self, number, spec):
1134
        return "ME", "%03i,%s" % (number, spec)
1135

    
1136
    def _cmd_set_memory_name(self, number, name):
1137
        return "MN", "%03i,%s" % (number, name)
1138

    
1139
def do_test():
1140
    """Dev test"""
1141
    mem = chirp_common.Memory()
1142
    mem.number = 1
1143
    mem.freq = 440000000
1144
    mem.duplex = "split"
1145
    mem.offset = 442000000
1146

    
1147
    tc = THF6ARadio
1148
    class FakeSerial:
1149
        """Faked serial line"""
1150
        buf = ""
1151
        def write(self, buf):
1152
            """Write"""
1153
            self.buf = buf
1154
        def read(self, count):
1155
            """Read"""
1156
            if self.buf[:2] == "ID":
1157
                return "ID %s\r" % tc.MODEL
1158
            return self.buf
1159
        def setTimeout(self, foo):
1160
            """Set Timeout"""
1161
            pass
1162
        def setBaudrate(self, foo):
1163
            """Set Baudrate"""
1164
            pass
1165

    
1166
    radio = tc(FakeSerial())
1167
    radio.set_memory(mem)
1168

    
1169
if __name__ == "__main__":
1170
    do_test()
1171

    
    (1-1/1)