Project

General

Profile

Bug #10585 » th7800.py

Dan Smith, 07/04/2023 04:22 PM

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

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

    
30
LOG = logging.getLogger(__name__)
31

    
32
TH7800_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
     clk_sft: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
     unknown3:4,
55
     tmode:2;
56
  lbcd offset[4];
57
  u8 hsdtype:2,     // off, 2-tone, 5-tone, dtmf
58
     unknown5a:1,
59
     am:1,
60
     unknown5b:4;
61
  u8 unknown6[3];
62
  char name[6];
63
  u8 empty[2];
64
};
65

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

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

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

    
122
#seekto 0x%04X;
123
u8  chan_active[128];
124
u8  scan_enable[128];
125
u8  priority[128];
126

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

    
139
struct {
140
  lbcd lorx[4];
141
  lbcd hirx[4];
142
  lbcd lotx[4];
143
  lbcd hitx[4];
144
} bandlimits[9];
145

    
146
"""
147

    
148

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

    
168

    
169
def add_radio_setting(radio_setting_group, mem_field, ui_name, option_map,
170
                      current, doc=None):
171
    setting = RadioSetting(mem_field, ui_name,
172
                           RadioSettingValueMap(option_map, current))
173
    if doc is not None:
174
        setting.set_doc(doc)
175
    radio_setting_group.append(setting)
176

    
177

    
178
def add_radio_bool(radio_setting_group, mem_field, ui_name, current, doc=None):
179
    setting = RadioSetting(mem_field, ui_name,
180
                           RadioSettingValueBoolean(bool(current)))
181
    radio_setting_group.append(setting)
182

    
183

    
184
class TYTTH7800Base(chirp_common.Radio):
185
    """Base class for TYT TH-7800"""
186
    VENDOR = "TYT"
187

    
188
    def get_features(self):
189
        rf = chirp_common.RadioFeatures()
190
        rf.memory_bounds = (1, 800)
191
        rf.has_bank = False
192
        rf.has_tuning_step = True
193
        rf.valid_tuning_steps = STEPS
194
        rf.can_odd_split = True
195
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
196
        rf.valid_tmodes = TMODES
197
        rf.has_ctone = False
198
        rf.valid_power_levels = POWER_LEVELS
199
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "#*-+"
200
        rf.valid_bands = [(108000000, 180000000),
201
                          (350000000, 399995000),
202
                          (400000000, 512000000)]
203
        rf.valid_skips = SCAN_MODES
204
        rf.valid_modes = MODES + ["AM"]
205
        rf.valid_name_length = 6
206
        rf.has_settings = True
207
        return rf
208

    
209
    def process_mmap(self):
210
        self._memobj = bitwise.parse(
211
            TH7800_MEM_FORMAT %
212
            (self._mmap_offset, self._scanlimits_offset, self._settings_offset,
213
             self._chan_active_offset, self._info_offset), self._mmap)
214

    
215
    def get_active(self, banktype, num):
216
        """get active flag for channel active,
217
        scan enable, or priority banks"""
218
        bank = getattr(self._memobj, banktype)
219
        index = (num - 1) // 8
220
        bitpos = (num - 1) % 8
221
        mask = 2**bitpos
222
        enabled = bank[index] & mask
223
        if enabled:
224
            return True
225
        else:
226
            return False
227

    
228
    def set_active(self, banktype, num, enable=True):
229
        """set active flag for channel active,
230
        scan enable, or priority banks"""
231
        bank = getattr(self._memobj, banktype)
232
        index = (num - 1) // 8
233
        bitpos = (num - 1) % 8
234
        mask = 2**bitpos
235
        if enable:
236
            bank[index] |= mask
237
        else:
238
            bank[index] &= ~mask
239

    
240
    def get_raw_memory(self, number):
241
        return repr(self._memobj.memory[number - 1])
242

    
243
    def get_memory(self, number):
244
        _mem = self._memobj.memory[number - 1]
245
        mem = chirp_common.Memory()
246
        mem.number = number
247

    
248
        mem.empty = not self.get_active("chan_active", number)
249
        if mem.empty:
250
            return mem
251

    
252
        mem.freq = int(_mem.rx_freq) * 10
253

    
254
        txfreq = int(_mem.tx_freq) * 10
255
        if txfreq == mem.freq:
256
            mem.duplex = ""
257
        elif txfreq == 0:
258
            mem.duplex = "off"
259
            mem.offset = 0
260
        elif abs(txfreq - mem.freq) > 70000000:
261
            mem.duplex = "split"
262
            mem.offset = txfreq
263
        elif txfreq < mem.freq:
264
            mem.duplex = "-"
265
            mem.offset = mem.freq - txfreq
266
        elif txfreq > mem.freq:
267
            mem.duplex = "+"
268
            mem.offset = txfreq - mem.freq
269

    
270
        mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_pol]
271

    
272
        mem.tmode = TMODES[int(_mem.tmode)]
273
        mem.ctone = mem.rtone = int(_mem.ctcss) / 10.0
274
        mem.dtcs = int(_mem.dtcs)
275

    
276
        mem.name = str(_mem.name)
277
        mem.name = mem.name.replace("\xFF", " ").rstrip()
278

    
279
        if not self.get_active("scan_enable", number):
280
            mem.skip = "S"
281
        elif self.get_active("priority", number):
282
            mem.skip = "P"
283
        else:
284
            mem.skip = ""
285

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

    
288
        mem.power = POWER_LEVELS[_mem.power]
289
        mem.tuning_step = STEPS[_mem.step]
290

    
291
        mem.extra = RadioSettingGroup("extra", "Extra")
292

    
293
        add_radio_setting(mem.extra, "display", "Display",
294
                          zero_indexed_seq_map(["Frequency", "Name"]),
295
                          _mem.display)
296
        add_radio_setting(mem.extra, "hsdtype", "HSD TYPE",
297
                          zero_indexed_seq_map(["OFF", "2TON", "5TON",
298
                                                "DTMF"]),
299
                          _mem.hsdtype)
300
        add_radio_bool(mem.extra, "clk_sft", "CLK-SFT", _mem.clk_sft)
301
        add_radio_bool(mem.extra, "compand", "Compand", _mem.compand,
302
                       doc="Compress Audio")
303
        add_radio_bool(mem.extra, "talkaround", "Talk Around", _mem.talkaround,
304
                       doc="Simplex mode when out of range of repeater")
305

    
306
        add_radio_bool(mem.extra, "scramb", "Scramble", _mem.scramb,
307
                       doc="Frequency inversion Scramble")
308
        return mem
309

    
310
    def set_memory(self, mem):
311
        _mem = self._memobj.memory[mem.number - 1]
312

    
313
        _prev_active = self.get_active("chan_active", mem.number)
314
        self.set_active("chan_active", mem.number, not mem.empty)
315
        if mem.empty or not _prev_active:
316
            LOG.debug("initializing memory channel %d" % mem.number)
317
            _mem.set_raw(BLANK_MEMORY)
318

    
319
        if mem.empty:
320
            return
321

    
322
        _mem.rx_freq = mem.freq / 10
323
        if mem.duplex == "split":
324
            _mem.tx_freq = mem.offset / 10
325
        elif mem.duplex == "-":
326
            _mem.tx_freq = (mem.freq - mem.offset) / 10
327
        elif mem.duplex == "+":
328
            _mem.tx_freq = (mem.freq + mem.offset) / 10
329
        elif mem.duplex == "off":
330
            _mem.tx_freq = 0
331
            _mem.offset = 0
332
        else:
333
            _mem.tx_freq = mem.freq / 10
334

    
335
        _mem.tmode = TMODES.index(mem.tmode)
336
        if mem.tmode == "TSQL" or mem.tmode == "DTCS":
337
            _mem.sqlmode = 1
338
        else:
339
            _mem.sqlmode = 0
340
        _mem.ctcss = mem.rtone * 10
341
        _mem.dtcs = mem.dtcs
342
        _mem.dtcs_pol = DTCS_POLARITY.index(mem.dtcs_polarity)
343

    
344
        _mem.name = mem.name.ljust(6, "\xFF")
345

    
346
        # autoset display to name if filled, else show frequency
347
        if mem.extra:
348
            # mem.extra only seems to be populated when called from edit panel
349
            display = mem.extra["display"]
350
        else:
351
            display = None
352
        if mem.name:
353
            _mem.display = True
354
            if display and not display.changed():
355
                display.value = "Name"
356
        else:
357
            _mem.display = False
358
            if display and not display.changed():
359
                display.value = "Frequency"
360

    
361
        _mem.scan = SCAN_MODES.index(mem.skip)
362
        if mem.skip == "P":
363
            self.set_active("priority", mem.number, True)
364
            self.set_active("scan_enable", mem.number, True)
365
        elif mem.skip == "S":
366
            self.set_active("priority", mem.number, False)
367
            self.set_active("scan_enable", mem.number, False)
368
        elif mem.skip == "":
369
            self.set_active("priority", mem.number, False)
370
            self.set_active("scan_enable", mem.number, True)
371

    
372
        if mem.mode == "AM":
373
            _mem.am = True
374
            _mem.fmdev = 0
375
        else:
376
            _mem.am = False
377
            _mem.fmdev = MODES.index(mem.mode)
378

    
379
        if mem.power:
380
            _mem.power = POWER_LEVELS.index(mem.power)
381
        else:
382
            _mem.power = 0    # low
383
        _mem.step = STEPS.index(mem.tuning_step)
384

    
385
        for setting in mem.extra:
386
            LOG.debug("@set_mem:", setting.get_name(), setting.value)
387
            setattr(_mem, setting.get_name(), setting.value)
388

    
389
    def get_settings(self):
390
        _settings = self._memobj.settings
391
        _info = self._memobj.info
392
        _bandlimits = self._memobj.bandlimits
393
        basic = RadioSettingGroup("basic", "Basic")
394
        info = RadioSettingGroup("info", "Model Info")
395
        top = RadioSettings(basic, info)
396
        add_radio_bool(basic, "beep", "Beep", _settings.beep)
397
        add_radio_bool(basic, "ars", "Auto Repeater Shift", _settings.ars)
398
        add_radio_setting(basic, "keylock", "Key Lock",
399
                          zero_indexed_seq_map(["Manual", "Auto"]),
400
                          _settings.keylock)
401
        add_radio_bool(basic, "auto_am", "Auto AM", _settings.auto_am)
402
        add_radio_setting(basic, "left_sql", "Left Squelch",
403
                          zero_indexed_seq_map(SQLPRESET),
404
                          _settings.left_sql)
405
        add_radio_setting(basic, "right_sql", "Right Squelch",
406
                          zero_indexed_seq_map(SQLPRESET),
407
                          _settings.right_sql)
408
        add_radio_setting(basic, "apo", "Auto Power off (Hours)",
409
                          [("Off", 0), ("0.5", 5), ("1.0", 10), ("1.5", 15),
410
                           ("2.0", 20)],
411
                          _settings.apo)
412
        add_radio_setting(basic, "backlight", "Display Backlight",
413
                          zero_indexed_seq_map(["Off", "1", "2", "3", "Full"]),
414
                          _settings.backlight)
415
        add_radio_setting(basic, "pttlock", "PTT Lock",
416
                          zero_indexed_seq_map(["Off", "Right", "Left",
417
                                                "Both"]),
418
                          _settings.pttlock)
419
        add_radio_setting(basic, "hyper_chan", "Hyper Channel",
420
                          zero_indexed_seq_map(["Manual", "Auto"]),
421
                          _settings.hyper_chan)
422
        add_radio_setting(basic, "right_func_key", "Right Function Key",
423
                          zero_indexed_seq_map(["Key 1", "Key 2"]),
424
                          _settings.right_func_key)
425
        add_radio_setting(basic, "mute_mode", "Mute Mode",
426
                          zero_indexed_seq_map(["Off", "TX", "RX", "TX RX"]),
427
                          _settings.mute_mode)
428
        add_radio_setting(basic, "scan_mode", "Scan Mode",
429
                          zero_indexed_seq_map(["MEM", "MSM"]),
430
                          _settings.scan_mode,
431
                          doc="MEM = Normal scan, bypass channels marked "
432
                          "skip. MSM = Scan only channels marked priority.")
433
        add_radio_setting(basic, "scan_resume", "Scan Resume",
434
                          zero_indexed_seq_map(["Time", "Busy"]),
435
                          _settings.scan_resume)
436
        basic.append(RadioSetting(
437
                "tot", "Time Out Timer (minutes)",
438
                RadioSettingValueInteger(0, 30, _settings.tot)))
439
        add_radio_setting(basic, "p1", "P1 Function",
440
                          zero_indexed_seq_map(MICKEYFUNC),
441
                          _settings.p1)
442
        add_radio_setting(basic, "p2", "P2 Function",
443
                          zero_indexed_seq_map(MICKEYFUNC),
444
                          _settings.p2)
445
        add_radio_setting(basic, "p3", "P3 Function",
446
                          zero_indexed_seq_map(MICKEYFUNC),
447
                          _settings.p3)
448
        add_radio_setting(basic, "p4", "P4 Function",
449
                          zero_indexed_seq_map(MICKEYFUNC),
450
                          _settings.p4)
451

    
452
        def _filter(name):
453
            filtered = ""
454
            for char in str(name):
455
                if char in chirp_common.CHARSET_ASCII:
456
                    filtered += char
457
                else:
458
                    filtered += " "
459
            return filtered
460

    
461
        rsvs = RadioSettingValueString(0, 8, _filter(_info.sn))
462
        rsvs.set_mutable(False)
463
        rs = RadioSetting("sn", "Serial Number", rsvs)
464
        info.append(rs)
465

    
466
        rsvs = RadioSettingValueString(0, 8, _filter(_info.model))
467
        rsvs.set_mutable(False)
468
        rs = RadioSetting("model", "Model Name", rsvs)
469
        info.append(rs)
470

    
471
        rsvs = RadioSettingValueString(0, 16, _filter(_info.code))
472
        rsvs.set_mutable(False)
473
        rs = RadioSetting("code", "Model Code", rsvs)
474
        info.append(rs)
475

    
476
        progdate = "%d/%d/%d" % (_info.prog_mon, _info.prog_day,
477
                                 _info.prog_yr)
478
        rsvs = RadioSettingValueString(0, 10, progdate)
479
        rsvs.set_mutable(False)
480
        rs = RadioSetting("progdate", "Last Program Date", rsvs)
481
        info.append(rs)
482

    
483
        # Band Limits
484
        for i in range(0, len(BANDS)):
485
            rx_start = int(_bandlimits[i].lorx) * 10
486
            if not rx_start == 0:
487
                objname = BANDS[i] + "lorx"
488
                objnamepp = BANDS[i] + " Rx Start"
489
                rsv = RadioSettingValueString(0, 10, format_freq(rx_start))
490
                rsv.set_mutable(False)
491
                rs = RadioSetting(objname, objnamepp, rsv)
492
                info.append(rs)
493

    
494
                rx_end = int(_bandlimits[i].hirx) * 10
495
                objname = BANDS[i] + "hirx"
496
                objnamepp = BANDS[i] + " Rx end"
497
                rsv = RadioSettingValueString(0, 10, format_freq(rx_end))
498
                rsv.set_mutable(False)
499
                rs = RadioSetting(objname, objnamepp, rsv)
500
                info.append(rs)
501

    
502
            tx_start = int(_bandlimits[i].lotx) * 10
503
            if not tx_start == 0:
504
                objname = BANDS[i] + "lotx"
505
                objnamepp = BANDS[i] + " Tx Start"
506
                rsv = RadioSettingValueString(0, 10, format_freq(tx_start))
507
                rsv.set_mutable(False)
508
                rs = RadioSetting(objname, objnamepp, rsv)
509
                info.append(rs)
510

    
511
                tx_end = int(_bandlimits[i].hitx) * 10
512
                objname = BANDS[i] + "hitx"
513
                objnamepp = BANDS[i] + " Tx end"
514
                rsv = RadioSettingValueString(0, 10, format_freq(tx_end))
515
                rsv.set_mutable(False)
516
                rs = RadioSetting(objname, objnamepp, rsv)
517
                info.append(rs)
518
        return top
519

    
520
    def set_settings(self, settings):
521
        _settings = self._memobj.settings
522
        _info = self._memobj.info
523
        _bandlimits = self._memobj.bandlimits
524
        for element in settings:
525
            if not isinstance(element, RadioSetting):
526
                self.set_settings(element)
527
                continue
528
            if not element.changed():
529
                continue
530
            try:
531
                setting = element.get_name()
532
                oldval = getattr(_settings, setting)
533
                newval = element.value
534

    
535
                LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval))
536
                setattr(_settings, setting, newval)
537
            except Exception as e:
538
                LOG.debug(element.get_name())
539
                raise
540

    
541

    
542
@directory.register
543
class TYTTH7800File(TYTTH7800Base, chirp_common.FileBackedRadio):
544
    """TYT TH-7800 .dat file"""
545
    MODEL = "TH-7800 File"
546

    
547
    FILE_EXTENSION = "dat"
548

    
549
    _memsize = 69632
550
    _mmap_offset = 0x1100
551
    _scanlimits_offset = 0xC800 + _mmap_offset
552
    _settings_offset = 0xCB20 + _mmap_offset
553
    _chan_active_offset = 0xCB80 + _mmap_offset
554
    _info_offset = 0xfe00 + _mmap_offset
555

    
556
    def __init__(self, pipe):
557
        self.errors = []
558
        self._mmap = None
559

    
560
        if isinstance(pipe, str):
561
            self.pipe = None
562
            self.load_mmap(pipe)
563
        else:
564
            chirp_common.FileBackedRadio.__init__(self, pipe)
565

    
566
    @classmethod
567
    def match_model(cls, filedata, filename):
568
        return len(filedata) == cls._memsize and filename.endswith('.dat')
569

    
570

    
571
def _identify(radio):
572
    """Do identify handshake with TYT"""
573
    try:
574
        radio.pipe.write(b"\x02SPECPR")
575
        ack = radio.pipe.read(1)
576
        if ack != b"A":
577
            util.hexprint(ack)
578
            raise errors.RadioError("Radio did not ACK first command: %r"
579
                                    % ack)
580
    except:
581
        raise errors.RadioError("Unable to communicate with the radio")
582

    
583
    radio.pipe.write(b"G\x02")
584
    ident = radio.pipe.read(16)
585
    radio.pipe.write(b"A")
586
    r = radio.pipe.read(2)
587
    if r != b"A":
588
        raise errors.RadioError("Ack failed")
589
    return ident
590

    
591

    
592
def _download(radio, memsize=0x10000, blocksize=0x80):
593
    """Download from TYT TH-7800"""
594
    data = _identify(radio)
595
    LOG.info("ident:", util.hexprint(data))
596
    offset = 0x100
597
    for addr in range(offset, memsize, blocksize):
598
        msg = struct.pack(">cHB", b"R", addr, blocksize)
599
        radio.pipe.write(msg)
600
        block = radio.pipe.read(blocksize + 4)
601
        if len(block) != (blocksize + 4):
602
            LOG.debug(util.hexprint(block))
603
            raise errors.RadioError("Radio sent a short block")
604
        radio.pipe.write(b"A")
605
        ack = radio.pipe.read(1)
606
        if ack != b"A":
607
            LOG.debug(util.hexprint(ack))
608
            raise errors.RadioError("Radio NAKed block")
609
        data += block[4:]
610

    
611
        if radio.status_fn:
612
            status = chirp_common.Status()
613
            status.cur = addr
614
            status.max = memsize
615
            status.msg = "Cloning from radio"
616
            radio.status_fn(status)
617

    
618
    radio.pipe.write(b"ENDR")
619

    
620
    return memmap.MemoryMapBytes(data)
621

    
622

    
623
def _upload(radio, memsize=0xF400, blocksize=0x80):
624
    """Upload to TYT TH-7800"""
625
    data = _identify(radio)
626

    
627
    radio.pipe.timeout = 1
628

    
629
    if data != radio._mmap[:radio._mmap_offset]:
630
        raise errors.RadioError(
631
            "Model mismatch: \n%s\n%s" %
632
            (util.hexprint(data),
633
             util.hexprint(radio._mmap[:radio._mmap_offset])))
634
    # in the factory software they update the last program date when
635
    # they upload, So let's do the same
636
    today = date.today()
637
    y = today.year
638
    m = today.month
639
    d = today.day
640
    _info = radio._memobj.info
641

    
642
    ly = _info.prog_yr
643
    lm = _info.prog_mon
644
    ld = _info.prog_day
645
    LOG.debug("Updating last program date:%d/%d/%d" % (lm, ld, ly))
646
    LOG.debug("                  to today:%d/%d/%d" % (m, d, y))
647

    
648
    _info.prog_yr = y
649
    _info.prog_mon = m
650
    _info.prog_day = d
651

    
652
    offset = 0x0100
653
    for addr in range(offset, memsize, blocksize):
654
        mapaddr = addr + radio._mmap_offset - offset
655
        LOG.debug("addr: 0x%04X, mmapaddr: 0x%04X" % (addr, mapaddr))
656
        msg = struct.pack(">cHB", b"W", addr, blocksize)
657
        msg += radio._mmap[mapaddr:(mapaddr + blocksize)]
658
        LOG.debug(util.hexprint(msg))
659
        radio.pipe.write(msg)
660
        ack = radio.pipe.read(1)
661
        if ack != b"A":
662
            LOG.debug(util.hexprint(ack))
663
            raise errors.RadioError("Radio did not ack block 0x%04X" % addr)
664

    
665
        if radio.status_fn:
666
            status = chirp_common.Status()
667
            status.cur = addr
668
            status.max = memsize
669
            status.msg = "Cloning to radio"
670
            radio.status_fn(status)
671

    
672
    # End of clone
673
    radio.pipe.write(b"ENDW")
674

    
675
    # Checksum?
676
    final_data = radio.pipe.read(3)
677
    LOG.debug("final:", util.hexprint(final_data))
678

    
679

    
680
@directory.register
681
class TYTTH7800Radio(TYTTH7800Base, chirp_common.CloneModeRadio,
682
                     chirp_common.ExperimentalRadio):
683
    VENDOR = "TYT"
684
    MODEL = "TH-7800"
685
    BAUD_RATE = 38400
686
    NEEDS_COMPAT_SERIAL = False
687

    
688
    _memsize = 65296
689
    _mmap_offset = 0x0010
690
    _scanlimits_offset = 0xC800 + _mmap_offset
691
    _settings_offset = 0xCB20 + _mmap_offset
692
    _chan_active_offset = 0xCB80 + _mmap_offset
693
    _info_offset = 0xfe00 + _mmap_offset
694

    
695
    @classmethod
696
    def match_model(cls, filedata, filename):
697
        if len(filedata) != cls._memsize:
698
            return False
699
        # TYT used TH9800 as model for TH-7800 _AND_ TH-9800.  Check
700
        # for TH7800 in case they fix it or if users update the model
701
        # in their own radio.
702
        if not (filedata[0xfe18:0xfe1e] == b"TH9800" or
703
                filedata[0xfe18:0xfe1e] == b"TH7800"):
704
            return False
705
        # TH-7800 bandlimits differ from TH-9800.  First band Invalid
706
        # (zero).
707
        first_bandlimit = struct.unpack("BBBBBBBBBBBBBBBB",
708
                                        filedata[0xfe40:0xfe50])
709
        if not all(v == 0 for v in first_bandlimit):
710
            return False
711
        return True
712

    
713
    @classmethod
714
    def get_prompts(cls):
715
        rp = chirp_common.RadioPrompts()
716
        rp.experimental = (
717
         'This is experimental support for TH-7800 '
718
         'which is still under development.\n'
719
         'Please ensure you have a good backup with OEM software.\n'
720
         'Also please send in bug and enhancement requests!\n'
721
         'You have been warned. Proceed at your own risk!')
722
        return rp
723

    
724
    def sync_in(self):
725
        try:
726
            self._mmap = _download(self)
727
        except Exception as e:
728
            raise errors.RadioError(
729
                    "Failed to communicate with the radio: %s" % e)
730
        self.process_mmap()
731

    
732
    def sync_out(self):
733
        try:
734
            _upload(self)
735
        except Exception as e:
736
            raise errors.RadioError(
737
                    "Failed to communicate with the radio: %s" % e)
(3-3/6)