th9800_draft_fix_1.py

Jim Unroe, 10/11/2021 06:49 pm

Download (27.6 kB)

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

    
18
from chirp import bitwise, chirp_common, directory, errors, util, memmap
19
import struct
20
from chirp.settings import RadioSetting, RadioSettingGroup, \
21
    RadioSettingValueInteger, RadioSettingValueList, \
22
    RadioSettingValueBoolean, RadioSettingValueString, \
23
    RadioSettingValueFloat, InvalidValueError, RadioSettings
24
from chirp.chirp_common import format_freq
25
import os
26
import time
27
import logging
28
from datetime import date
29

    
30
LOG = logging.getLogger(__name__)
31

    
32
TH9800_MEM_FORMAT = """
33
struct mem {
34
  lbcd rx_freq[4];
35
  lbcd tx_freq[4];
36
  lbcd ctcss[2];
37
  lbcd dtcs[2];
38
  u8 power:2,
39
     BeatShift:1,
40
     unknown0a:2,
41
     display:1,     // freq=0, name=1
42
     scan:2;
43
  u8 fmdev:2,       // wide=00, mid=01, narrow=10
44
     scramb:1,
45
     compand:1,
46
     emphasis:1
47
     unknown1a:2,
48
     sqlmode:1;     // carrier, tone
49
  u8 rptmod:2,      // off, -, +
50
     reverse:1,
51
     talkaround:1,
52
     step:4;
53
  u8 dtcs_pol:2,
54
     bclo:2,
55
     unknown3:2,
56
     tmode:2;
57
  lbcd offset[4];
58
  u8 hsdtype:2,     // off, 2-tone, 5-tone, dtmf
59
     unknown5a:1,
60
     am:1,
61
     unknown5b:4;
62
  u8 unknown6[3];
63
  char name[6];
64
  u8 empty[2];
65
};
66

    
67
#seekto 0x%04X;
68
struct mem memory[800];
69

    
70
#seekto 0x%04X;
71
struct {
72
  struct mem lower;
73
  struct mem upper;
74
} scanlimits[5];
75

    
76
#seekto 0x%04X;
77
struct {
78
    u8  unk0xdc20:5,
79
        left_sql:3;
80
    u8  apo;
81
    u8  unk0xdc22:5,
82
        backlight:3;
83
    u8  unk0xdc23;
84
    u8  beep:1,
85
        keylock:1,
86
        pttlock:2,
87
        unk0xdc24_32:2,
88
        hyper_chan:1,
89
        right_func_key:1;
90
    u8  tbst_freq:2,
91
        ani_display:1,
92
        unk0xdc25_4:1
93
        mute_mode:2,
94
        unk0xdc25_10:2;
95
    u8  auto_xfer:1,
96
        auto_contact:1,
97
        unk0xdc26_54:2,
98
        auto_am:1,
99
        unk0xdc26_210:3;
100
    u8  unk0xdc27_76543:5,
101
        scan_mode:1,
102
        unk0xdc27_1:1,
103
        scan_resume:1;
104
    u16 scramb_freq;
105
    u16 scramb_freq1;
106
    u8  exit_delay;
107
    u8  unk0xdc2d;
108
    u8  unk0xdc2e:5,
109
        right_sql:3;
110
    u8  unk0xdc2f:4,
111
        beep_vol:4;
112
    u8  tot;
113
    u8  tot_alert;
114
    u8  tot_rekey;
115
    u8  tot_reset;
116
    u8  unk0xdc34;
117
    u8  unk0xdc35;
118
    u8  unk0xdc36;
119
    u8  unk0xdc37;
120
    u8  p1;
121
    u8  p2;
122
    u8  p3;
123
    u8  p4;
124
} settings;
125

    
126
#seekto 0x%04X;
127
u8  chan_active[128];
128
u8  scan_enable[128];
129
u8  priority[128];
130

    
131
#seekto 0x%04X;
132
struct {
133
    char sn[8];
134
    char model[8];
135
    char code[16];
136
    u8 empty[8];
137
    lbcd prog_yr[2];
138
    lbcd prog_mon;
139
    lbcd prog_day;
140
    u8 empty_10f2c[4];
141
} info;
142

    
143
struct {
144
  lbcd lorx[4];
145
  lbcd hirx[4];
146
  lbcd lotx[4];
147
  lbcd hitx[4];
148
} bandlimits[9];
149

    
150
"""
151

    
152

    
153
BLANK_MEMORY = "\xFF" * 8 + "\x00\x10\x23\x00\xC0\x08\x06\x00" \
154
               "\x00\x00\x76\x00\x00\x00" + "\xFF" * 10
155
DTCS_POLARITY = ["NN", "RN", "NR", "RR"]
156
SCAN_MODES = ["", "S", "P"]
157
MODES = ["WFM", "FM", "NFM"]
158
TMODES = ["", "Tone", "TSQL", "DTCS"]
159
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00),
160
                chirp_common.PowerLevel("Mid2", watts=10.00),
161
                chirp_common.PowerLevel("Mid1", watts=20.00),
162
                chirp_common.PowerLevel("High", watts=50.00)]
163
BUSY_LOCK = ["off", "Carrier", "2 tone"]
164
MICKEYFUNC = ["None", "SCAN", "SQL.OFF", "TCALL", "PPTR", "PRI", "LOW", "TONE",
165
              "MHz", "REV", "HOME", "BAND", "VFO/MR"]
166
SQLPRESET = ["Off", "2", "5", "9", "Full"]
167
BANDS = ["30MHz", "50MHz", "60MHz", "108MHz", "150MHz", "250MHz", "350MHz",
168
         "450MHz", "850MHz"]
169
STEPS = [2.5, 5.0, 6.25, 7.5, 8.33, 10.0, 12.5,
170
         15.0, 20.0, 25.0, 30.0, 50.0, 100.0]
171

    
172

    
173
def isValidDate(month, day, year):
174
    today = date.today()
175

    
176
    monthlist1 = [1, 3, 5, 7, 8, 10, 12] ## monthlist for months with 31 days.
177
    monthlist2 = [4, 6, 9, 11] ## monthlist for months with 30 days.
178
    monthlist3 = [2, ] ## monthlist for month with 28 days.
179

    
180
    if month in monthlist1:
181
        max1 = 31
182
    elif month in monthlist2:
183
        max1 = 30
184
    elif month in monthlist3:
185
        if ((yy % 4) == 0 and (yy % 100) != 0 or (yy % 400) == 0):
186
            max1 = 29
187
        else:
188
            max1 = 28
189
    if(month < 1 or month > 12):
190
        LOG.debug("Invalid 'Last Program Date: Month'")
191
        return False
192
    elif(day < 1 or day > max1):
193
        LOG.debug("Invalid 'Last Program Date: Day'")
194
        return False
195
    elif(year < 2014 or year > today.year):
196
        LOG.debug("Invalid 'Last Program Date: Year'")
197
        return False
198
    return True
199

    
200
class TYTTH9800Base(chirp_common.Radio):
201
    """Base class for TYT TH-9800"""
202
    VENDOR = "TYT"
203

    
204
    def get_features(self):
205
        rf = chirp_common.RadioFeatures()
206
        rf.memory_bounds = (1, 800)
207
        rf.has_bank = False
208
        rf.has_tuning_step = True
209
        rf.valid_tuning_steps = STEPS
210
        rf.can_odd_split = True
211
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
212
        rf.valid_tmodes = TMODES
213
        rf.has_ctone = False
214
        rf.valid_power_levels = POWER_LEVELS
215
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "#*-+"
216
        rf.valid_bands = [(26000000,  33000000),
217
                          (47000000,  54000000),
218
                          (108000000, 180000000),
219
                          (220000000, 260000000),
220
                          (350000000, 399995000),
221
                          (400000000, 512000000),
222
                          (750000000, 950000000)]
223
        rf.valid_skips = SCAN_MODES
224
        rf.valid_modes = MODES + ["AM"]
225
        rf.valid_name_length = 6
226
        rf.has_settings = True
227
        return rf
228

    
229
    def process_mmap(self):
230
        self._memobj = bitwise.parse(
231
            TH9800_MEM_FORMAT %
232
            (self._mmap_offset, self._scanlimits_offset, self._settings_offset,
233
             self._chan_active_offset, self._info_offset), self._mmap)
234

    
235
    def get_active(self, banktype, num):
236
        """get active flag for channel active,
237
        scan enable, or priority banks"""
238
        bank = getattr(self._memobj, banktype)
239
        index = (num - 1) / 8
240
        bitpos = (num - 1) % 8
241
        mask = 2**bitpos
242
        enabled = bank[index] & mask
243
        if enabled:
244
            return True
245
        else:
246
            return False
247

    
248
    def set_active(self, banktype, num, enable=True):
249
        """set active flag for channel active,
250
        scan enable, or priority banks"""
251
        bank = getattr(self._memobj, banktype)
252
        index = (num - 1) / 8
253
        bitpos = (num - 1) % 8
254
        mask = 2**bitpos
255
        if enable:
256
            bank[index] |= mask
257
        else:
258
            bank[index] &= ~mask
259

    
260
    def get_raw_memory(self, number):
261
        return repr(self._memobj.memory[number - 1])
262

    
263
    def get_memory(self, number):
264
        _mem = self._memobj.memory[number - 1]
265
        mem = chirp_common.Memory()
266
        mem.number = number
267

    
268
        mem.empty = not self.get_active("chan_active", number)
269
        if mem.empty:
270
            return mem
271

    
272
        mem.freq = int(_mem.rx_freq) * 10
273

    
274
        txfreq = int(_mem.tx_freq) * 10
275
        if txfreq == mem.freq:
276
            mem.duplex = ""
277
        elif txfreq == 0:
278
            mem.duplex = "off"
279
            mem.offset = 0
280
        elif abs(txfreq - mem.freq) > 70000000:
281
            mem.duplex = "split"
282
            mem.offset = txfreq
283
        elif txfreq < mem.freq:
284
            mem.duplex = "-"
285
            mem.offset = mem.freq - txfreq
286
        elif txfreq > mem.freq:
287
            mem.duplex = "+"
288
            mem.offset = txfreq - mem.freq
289

    
290
        mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_pol]
291

    
292
        mem.tmode = TMODES[int(_mem.tmode)]
293
        mem.ctone = mem.rtone = int(_mem.ctcss) / 10.0
294
        mem.dtcs = int(_mem.dtcs)
295

    
296
        mem.name = str(_mem.name)
297
        mem.name = mem.name.replace("\xFF", " ").rstrip()
298

    
299
        if not self.get_active("scan_enable", number):
300
            mem.skip = "S"
301
        elif self.get_active("priority", number):
302
            mem.skip = "P"
303
        else:
304
            mem.skip = ""
305

    
306
        mem.mode = _mem.am and "AM" or MODES[int(_mem.fmdev)]
307

    
308
        mem.power = POWER_LEVELS[_mem.power]
309
        mem.tuning_step = STEPS[_mem.step]
310

    
311
        mem.extra = RadioSettingGroup("extra", "Extra")
312

    
313
        opts = ["Frequency", "Name"]
314
        display = RadioSetting(
315
                "display", "Display",
316
                RadioSettingValueList(opts, opts[_mem.display]))
317
        mem.extra.append(display)
318

    
319
        bclo = RadioSetting(
320
                "bclo", "Busy Lockout",
321
                RadioSettingValueList(BUSY_LOCK, BUSY_LOCK[_mem.bclo]))
322
        bclo.set_doc("Busy Lockout")
323
        mem.extra.append(bclo)
324

    
325
        emphasis = RadioSetting(
326
                "emphasis", "Emphasis",
327
                RadioSettingValueBoolean(bool(_mem.emphasis)))
328
        emphasis.set_doc("Boosts 300Hz to 2500Hz mic response")
329
        mem.extra.append(emphasis)
330

    
331
        compand = RadioSetting(
332
                "compand", "Compand",
333
                RadioSettingValueBoolean(bool(_mem.compand)))
334
        compand.set_doc("Compress Audio")
335
        mem.extra.append(compand)
336

    
337
        BeatShift = RadioSetting(
338
                "BeatShift", "BeatShift",
339
                RadioSettingValueBoolean(bool(_mem.BeatShift)))
340
        BeatShift.set_doc("Beat Shift")
341
        mem.extra.append(BeatShift)
342

    
343
        TalkAround = RadioSetting(
344
                "talkaround", "Talk Around",
345
                RadioSettingValueBoolean(bool(_mem.talkaround)))
346
        TalkAround.set_doc("Simplex mode when out of range of repeater")
347
        mem.extra.append(TalkAround)
348

    
349
        scramb = RadioSetting(
350
                "scramb", "Scramble",
351
                RadioSettingValueBoolean(bool(_mem.scramb)))
352
        scramb.set_doc("Frequency inversion Scramble")
353
        mem.extra.append(scramb)
354

    
355
        return mem
356

    
357
    def set_memory(self, mem):
358
        _mem = self._memobj.memory[mem.number - 1]
359

    
360
        _prev_active = self.get_active("chan_active", mem.number)
361
        self.set_active("chan_active", mem.number, not mem.empty)
362
        if mem.empty or not _prev_active:
363
            LOG.debug("initializing memory channel %d" % mem.number)
364
            _mem.set_raw(BLANK_MEMORY)
365

    
366
        if mem.empty:
367
            return
368

    
369
        _mem.rx_freq = mem.freq / 10
370
        if mem.duplex == "split":
371
            _mem.tx_freq = mem.offset / 10
372
        elif mem.duplex == "-":
373
            _mem.tx_freq = (mem.freq - mem.offset) / 10
374
        elif mem.duplex == "+":
375
            _mem.tx_freq = (mem.freq + mem.offset) / 10
376
        elif mem.duplex == "off":
377
            _mem.tx_freq = 0
378
            _mem.offset = 0
379
        else:
380
            _mem.tx_freq = mem.freq / 10
381

    
382
        _mem.tmode = TMODES.index(mem.tmode)
383
        if mem.tmode == "TSQL" or mem.tmode == "DTCS":
384
            _mem.sqlmode = 1
385
        else:
386
            _mem.sqlmode = 0
387
        _mem.ctcss = mem.rtone * 10
388
        _mem.dtcs = mem.dtcs
389
        _mem.dtcs_pol = DTCS_POLARITY.index(mem.dtcs_polarity)
390

    
391
        _mem.name = mem.name.ljust(6, "\xFF")
392

    
393
        # autoset display to name if filled, else show frequency
394
        if mem.extra:
395
            # mem.extra only seems to be populated when called from edit panel
396
            display = mem.extra["display"]
397
        else:
398
            display = None
399
        if mem.name:
400
            _mem.display = True
401
            if display and not display.changed():
402
                display.value = "Name"
403
        else:
404
            _mem.display = False
405
            if display and not display.changed():
406
                display.value = "Frequency"
407

    
408
        _mem.scan = SCAN_MODES.index(mem.skip)
409
        if mem.skip == "P":
410
            self.set_active("priority", mem.number, True)
411
            self.set_active("scan_enable", mem.number, True)
412
        elif mem.skip == "S":
413
            self.set_active("priority", mem.number, False)
414
            self.set_active("scan_enable", mem.number, False)
415
        elif mem.skip == "":
416
            self.set_active("priority", mem.number, False)
417
            self.set_active("scan_enable", mem.number, True)
418

    
419
        if mem.mode == "AM":
420
            _mem.am = True
421
            _mem.fmdev = 0
422
        else:
423
            _mem.am = False
424
            _mem.fmdev = MODES.index(mem.mode)
425

    
426
        if mem.power:
427
            _mem.power = POWER_LEVELS.index(mem.power)
428
        else:
429
            _mem.power = 0    # low
430
        _mem.step = STEPS.index(mem.tuning_step)
431

    
432
        for setting in mem.extra:
433
            LOG.debug("@set_mem:", setting.get_name(), setting.value)
434
            setattr(_mem, setting.get_name(), setting.value)
435

    
436
    def get_settings(self):
437
        _settings = self._memobj.settings
438
        _info = self._memobj.info
439
        _bandlimits = self._memobj.bandlimits
440
        basic = RadioSettingGroup("basic", "Basic")
441
        info = RadioSettingGroup("info", "Model Info")
442
        top = RadioSettings(basic, info)
443
        basic.append(RadioSetting(
444
                "beep", "Beep",
445
                RadioSettingValueBoolean(_settings.beep)))
446
        basic.append(RadioSetting(
447
                "beep_vol", "Beep Volume",
448
                RadioSettingValueInteger(0, 15, _settings.beep_vol)))
449
        basic.append(RadioSetting(
450
                "keylock", "Key Lock",
451
                RadioSettingValueBoolean(_settings.keylock)))
452
        basic.append(RadioSetting(
453
                "ani_display", "ANI Display",
454
                RadioSettingValueBoolean(_settings.ani_display)))
455
        basic.append(RadioSetting(
456
                "auto_xfer", "Auto Transfer",
457
                RadioSettingValueBoolean(_settings.auto_xfer)))
458
        basic.append(RadioSetting(
459
                "auto_contact", "Auto Contact Always Remind",
460
                RadioSettingValueBoolean(_settings.auto_contact)))
461
        basic.append(RadioSetting(
462
                "auto_am", "Auto AM",
463
                RadioSettingValueBoolean(_settings.auto_am)))
464
        basic.append(RadioSetting(
465
                "left_sql", "Left Squelch",
466
                RadioSettingValueList(
467
                    SQLPRESET, SQLPRESET[_settings.left_sql])))
468
        basic.append(RadioSetting(
469
                "right_sql", "Right Squelch",
470
                RadioSettingValueList(
471
                    SQLPRESET, SQLPRESET[_settings.right_sql])))
472
#      basic.append(RadioSetting("apo", "Auto Power off (0.1h)",
473
#              RadioSettingValueInteger(0, 20, _settings.apo)))
474
        opts = ["Off"] + ["%0.1f" % (t / 10.0) for t in range(1, 21, 1)]
475
        basic.append(RadioSetting(
476
                "apo", "Auto Power off (Hours)",
477
                RadioSettingValueList(opts, opts[_settings.apo])))
478
        opts = ["Off", "1", "2", "3", "Full"]
479
        basic.append(RadioSetting(
480
                "backlight", "Display Backlight",
481
                RadioSettingValueList(opts, opts[_settings.backlight])))
482
        opts = ["Off", "Right", "Left", "Both"]
483
        basic.append(RadioSetting(
484
                "pttlock", "PTT Lock",
485
                RadioSettingValueList(opts, opts[_settings.pttlock])))
486
        opts = ["Manual", "Auto"]
487
        basic.append(RadioSetting(
488
                "hyper_chan", "Hyper Channel",
489
                RadioSettingValueList(opts, opts[_settings.hyper_chan])))
490
        opts = ["Key 1", "Key 2"]
491
        basic.append(RadioSetting(
492
                "right_func_key", "Right Function Key",
493
                RadioSettingValueList(opts, opts[_settings.right_func_key])))
494
        opts = ["1000Hz", "1450Hz", "1750Hz", "2100Hz"]
495
        basic.append(RadioSetting(
496
                "tbst_freq", "Tone Burst Frequency",
497
                RadioSettingValueList(opts, opts[_settings.tbst_freq])))
498
        opts = ["Off", "TX", "RX", "TX RX"]
499
        basic.append(RadioSetting(
500
                "mute_mode", "Mute Mode",
501
                RadioSettingValueList(opts, opts[_settings.mute_mode])))
502
        opts = ["MEM", "MSM"]
503
        scanmode = RadioSetting(
504
                "scan_mode", "Scan Mode",
505
                RadioSettingValueList(opts, opts[_settings.scan_mode]))
506
        scanmode.set_doc("MEM = Normal scan, bypass channels marked skip. "
507
                         " MSM = Scan only channels marked priority.")
508
        basic.append(scanmode)
509
        opts = ["TO", "CO"]
510
        basic.append(RadioSetting(
511
                "scan_resume", "Scan Resume",
512
                RadioSettingValueList(opts, opts[_settings.scan_resume])))
513
        opts = ["%0.1f" % (t / 10.0) for t in range(0, 51, 1)]
514
        basic.append(RadioSetting(
515
                "exit_delay", "Span Transit Exit Delay",
516
                RadioSettingValueList(opts, opts[_settings.exit_delay])))
517
        basic.append(RadioSetting(
518
                "tot", "Time Out Timer (minutes)",
519
                RadioSettingValueInteger(0, 30, _settings.tot)))
520
        basic.append(RadioSetting(
521
                "tot_alert", "Time Out Timer Pre Alert(seconds)",
522
                RadioSettingValueInteger(0, 15, _settings.tot_alert)))
523
        basic.append(RadioSetting(
524
                "tot_rekey", "Time Out Rekey (seconds)",
525
                RadioSettingValueInteger(0, 15, _settings.tot_rekey)))
526
        basic.append(RadioSetting(
527
                "tot_reset", "Time Out Reset(seconds)",
528
                RadioSettingValueInteger(0, 15, _settings.tot_reset)))
529
        basic.append(RadioSetting(
530
                "p1", "P1 Function",
531
                RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p1])))
532
        basic.append(RadioSetting(
533
                "p2", "P2 Function",
534
                RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p2])))
535
        basic.append(RadioSetting(
536
                "p3", "P3 Function",
537
                RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p3])))
538
        basic.append(RadioSetting(
539
                "p4", "P4 Function",
540
                RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p4])))
541
#      opts = ["0", "1"]
542
#      basic.append(RadioSetting("x", "Desc",
543
#            RadioSettingValueList(opts, opts[_settings.x])))
544

    
545
        def _filter(name):
546
            filtered = ""
547
            for char in str(name):
548
                if char in chirp_common.CHARSET_ASCII:
549
                    filtered += char
550
                else:
551
                    filtered += " "
552
            return filtered
553

    
554
        rsvs = RadioSettingValueString(0, 8, _filter(_info.sn))
555
        rsvs.set_mutable(False)
556
        rs = RadioSetting("sn", "Serial Number", rsvs)
557
        info.append(rs)
558

    
559
        rsvs = RadioSettingValueString(0, 8, _filter(_info.model))
560
        rsvs.set_mutable(False)
561
        rs = RadioSetting("model", "Model Name", rsvs)
562
        info.append(rs)
563

    
564
        rsvs = RadioSettingValueString(0, 16, _filter(_info.code))
565
        rsvs.set_mutable(False)
566
        rs = RadioSetting("code", "Model Code", rsvs)
567
        info.append(rs)
568

    
569
        mm = int(_info.prog_mon)
570
        dd = int(_info.prog_day)
571
        yy = int(_info.prog_yr)
572

    
573
        Valid_Date = isValidDate(mm, dd, yy)
574

    
575
        if Valid_Date:
576
            progdate = "%d/%d/%d" % (mm, dd, yy)
577
        else:
578
            progdate = "Invalid"
579
        rsvs = RadioSettingValueString(0, 10, progdate)
580
        rsvs.set_mutable(False)
581
        rs = RadioSetting("progdate", "Last Program Date", rsvs)
582
        info.append(rs)
583

    
584
        # 9 band limits
585
        for i in range(0, 9):
586
            objname = BANDS[i] + "lorx"
587
            objnamepp = BANDS[i] + " Rx Start"
588
            # rsv = RadioSettingValueInteger(0, 100000000,
589
            #              int(_bandlimits[i].lorx))
590
            rsv = RadioSettingValueString(
591
                    0, 10, format_freq(int(_bandlimits[i].lorx)*10))
592
            rsv.set_mutable(False)
593
            rs = RadioSetting(objname, objnamepp, rsv)
594
            info.append(rs)
595
            objname = BANDS[i] + "hirx"
596
            objnamepp = BANDS[i] + " Rx end"
597
            rsv = RadioSettingValueString(
598
                    0, 10, format_freq(int(_bandlimits[i].hirx)*10))
599
            rsv.set_mutable(False)
600
            rs = RadioSetting(objname, objnamepp, rsv)
601
            info.append(rs)
602
            objname = BANDS[i] + "lotx"
603
            objnamepp = BANDS[i] + " Tx Start"
604
            rsv = RadioSettingValueString(
605
                    0, 10, format_freq(int(_bandlimits[i].lotx)*10))
606
            rsv.set_mutable(False)
607
            rs = RadioSetting(objname, objnamepp, rsv)
608
            info.append(rs)
609
            objname = BANDS[i] + "hitx"
610
            objnamepp = BANDS[i] + " Tx end"
611
            rsv = RadioSettingValueString(
612
                    0, 10, format_freq(int(_bandlimits[i].hitx)*10))
613
            rsv.set_mutable(False)
614
            rs = RadioSetting(objname, objnamepp, rsv)
615
            info.append(rs)
616

    
617
        return top
618

    
619
    def set_settings(self, settings):
620
        _settings = self._memobj.settings
621
        _info = self._memobj.info
622
        _bandlimits = self._memobj.bandlimits
623
        for element in settings:
624
            if not isinstance(element, RadioSetting):
625
                self.set_settings(element)
626
                continue
627
            if not element.changed():
628
                continue
629
            try:
630
                setting = element.get_name()
631
                oldval = getattr(_settings, setting)
632
                newval = element.value
633

    
634
                LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval))
635
                setattr(_settings, setting, newval)
636
            except Exception, e:
637
                LOG.debug(element.get_name())
638
                raise
639

    
640

    
641
@directory.register
642
class TYTTH9800File(TYTTH9800Base, chirp_common.FileBackedRadio):
643
    """TYT TH-9800 .dat file"""
644
    MODEL = "TH-9800 File"
645

    
646
    FILE_EXTENSION = "dat"
647

    
648
    _memsize = 69632
649
    _mmap_offset = 0x1100
650
    _scanlimits_offset = 0xC800 + _mmap_offset
651
    _settings_offset = 0xCB20 + _mmap_offset
652
    _chan_active_offset = 0xCB80 + _mmap_offset
653
    _info_offset = 0xfe00 + _mmap_offset
654

    
655
    def __init__(self, pipe):
656
        self.errors = []
657
        self._mmap = None
658

    
659
        if isinstance(pipe, str):
660
            self.pipe = None
661
            self.load_mmap(pipe)
662
        else:
663
            chirp_common.FileBackedRadio.__init__(self, pipe)
664

    
665
    @classmethod
666
    def match_model(cls, filedata, filename):
667
        return len(filedata) == cls._memsize and filename.endswith('.dat')
668

    
669

    
670
def _identify(radio):
671
    """Do identify handshake with TYT"""
672
    try:
673
        radio.pipe.write("\x02PROGRA")
674
        ack = radio.pipe.read(1)
675
        if ack != "A":
676
            util.hexprint(ack)
677
            raise errors.RadioError("Radio did not ACK first command: %x"
678
                                    % ord(ack))
679
    except:
680
        LOG.debug(util.hexprint(ack))
681
        raise errors.RadioError("Unable to communicate with the radio")
682

    
683
    radio.pipe.write("M\x02")
684
    ident = radio.pipe.read(16)
685
    radio.pipe.write("A")
686
    r = radio.pipe.read(1)
687
    if r != "A":
688
        raise errors.RadioError("Ack failed")
689
    return ident
690

    
691

    
692
def _download(radio, memsize=0x10000, blocksize=0x80):
693
    """Download from TYT TH-9800"""
694
    data = _identify(radio)
695
    LOG.info("ident:", util.hexprint(data))
696
    offset = 0x100
697
    for addr in range(offset, memsize, blocksize):
698
        msg = struct.pack(">cHB", "R", addr, blocksize)
699
        radio.pipe.write(msg)
700
        block = radio.pipe.read(blocksize + 4)
701
        if len(block) != (blocksize + 4):
702
            LOG.debug(util.hexprint(block))
703
            raise errors.RadioError("Radio sent a short block")
704
        radio.pipe.write("A")
705
        ack = radio.pipe.read(1)
706
        if ack != "A":
707
            LOG.debug(util.hexprint(ack))
708
            raise errors.RadioError("Radio NAKed block")
709
        data += block[4:]
710

    
711
        if radio.status_fn:
712
            status = chirp_common.Status()
713
            status.cur = addr
714
            status.max = memsize
715
            status.msg = "Cloning from radio"
716
            radio.status_fn(status)
717

    
718
    radio.pipe.write("ENDR")
719

    
720
    return memmap.MemoryMap(data)
721

    
722

    
723
def _upload(radio, memsize=0xF400, blocksize=0x80):
724
    """Upload to TYT TH-9800"""
725
    data = _identify(radio)
726

    
727
    radio.pipe.timeout = 1
728

    
729
    if data != radio._mmap[:radio._mmap_offset]:
730
        raise errors.RadioError(
731
            "Model mis-match: \n%s\n%s" %
732
            (util.hexprint(data),
733
             util.hexprint(radio._mmap[:radio._mmap_offset])))
734
    # in the factory software they update the last program date when
735
    # they upload, So let's do the same
736
    today = date.today()
737
    y = today.year
738
    m = today.month
739
    d = today.day
740
    _info = radio._memobj.info
741

    
742
    ly = int(_info.prog_yr)
743
    lm = int(_info.prog_mon)
744
    ld = int(_info.prog_day)
745

    
746
    Valid_Date = isValidDate(lm, ld, ly)
747

    
748
    if Valid_Date:
749
        LOG.debug("Updating last program date:%d/%d/%d" % (lm, ld, ly))
750
    else:
751
        LOG.debug("Updating last program date: Invalid")
752
    LOG.debug("                  to today:%d/%d/%d" % (m, d, y))
753

    
754
    _info.prog_yr = y
755
    _info.prog_mon = m
756
    _info.prog_day = d
757

    
758
    offset = 0x0100
759
    for addr in range(offset, memsize, blocksize):
760
        mapaddr = addr + radio._mmap_offset - offset
761
        LOG.debug("addr: 0x%04X, mmapaddr: 0x%04X" % (addr, mapaddr))
762
        msg = struct.pack(">cHB", "W", addr, blocksize)
763
        msg += radio._mmap[mapaddr:(mapaddr + blocksize)]
764
        LOG.debug(util.hexprint(msg))
765
        radio.pipe.write(msg)
766
        ack = radio.pipe.read(1)
767
        if ack != "A":
768
            LOG.debug(util.hexprint(ack))
769
            raise errors.RadioError("Radio did not ack block 0x%04X" % addr)
770

    
771
        if radio.status_fn:
772
            status = chirp_common.Status()
773
            status.cur = addr
774
            status.max = memsize
775
            status.msg = "Cloning to radio"
776
            radio.status_fn(status)
777

    
778
    # End of clone
779
    radio.pipe.write("ENDW")
780

    
781
    # Checksum?
782
    final_data = radio.pipe.read(3)
783
    LOG.debug("final:", util.hexprint(final_data))
784

    
785

    
786
@directory.register
787
class TYTTH9800Radio(TYTTH9800Base, chirp_common.CloneModeRadio,
788
                     chirp_common.ExperimentalRadio):
789
    VENDOR = "TYT"
790
    MODEL = "TH-9800"
791
    BAUD_RATE = 38400
792

    
793
    _memsize = 65296
794
    _mmap_offset = 0x0010
795
    _scanlimits_offset = 0xC800 + _mmap_offset
796
    _settings_offset = 0xCB20 + _mmap_offset
797
    _chan_active_offset = 0xCB80 + _mmap_offset
798
    _info_offset = 0xfe00 + _mmap_offset
799

    
800
    @classmethod
801
    def match_model(cls, filedata, filename):
802
        if len(filedata) != cls._memsize:
803
            return False
804
        # TYT set this model for TH-7800 _AND_ TH-9800
805
        if not filedata[0xfe18:0xfe1e] == "TH9800":
806
            return False
807
        # TH-9800 bandlimits differ from TH-7800.  First band is used
808
        # (non-zero).
809
        first_bandlimit = struct.unpack("BBBBBBBBBBBBBBBB",
810
                                        filedata[0xfe40:0xfe50])
811
        if all(v == 0 for v in first_bandlimit):
812
            return False
813
        return True
814

    
815
    @classmethod
816
    def get_prompts(cls):
817
        rp = chirp_common.RadioPrompts()
818
        rp.experimental = (
819
         'This is experimental support for TH-9800 '
820
         'which is still under development.\n'
821
         'Please ensure you have a good backup with OEM software.\n'
822
         'Also please send in bug and enhancement requests!\n'
823
         'You have been warned. Proceed at your own risk!')
824
        return rp
825

    
826
    def sync_in(self):
827
        try:
828
            self._mmap = _download(self)
829
        except Exception, e:
830
            raise errors.RadioError(
831
                    "Failed to communicate with the radio: %s" % e)
832
        self.process_mmap()
833

    
834
    def sync_out(self):
835
        try:
836
            _upload(self)
837
        except Exception, e:
838
            raise errors.RadioError(
839
                    "Failed to communicate with the radio: %s" % e)