Project

General

Profile

Bug #10340 » th_uvf8d.py

3201a3b5 - Dan Smith, 02/03/2023 04:42 AM

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

    
17
"""TYT TH-UVF8D radio management module"""
18

    
19
# TODO: support FM Radio memories
20
# TODO: support bank B (another 128 memories)
21
# TODO: [setting] Battery Save
22
# TODO: [setting] Tail Eliminate
23
# TODO: [setting] Tail Mode
24

    
25
import struct
26
import logging
27

    
28
from chirp import chirp_common, bitwise, errors, directory, memmap, util
29
from chirp.settings import RadioSetting, RadioSettingGroup, \
30
    RadioSettingValueInteger, RadioSettingValueList, \
31
    RadioSettingValueBoolean, RadioSettingValueString, \
32
    RadioSettings
33

    
34
LOG = logging.getLogger(__name__)
35

    
36

    
37
def uvf8d_identify(radio):
38
    """Do identify handshake with TYT TH-UVF8D"""
39
    try:
40
        radio.pipe.write(b"\x02PROGRAM")
41
        ack = radio.pipe.read(2)
42
        if ack != b"PG":
43
            raise errors.RadioError("Radio did not ACK first command: %x" %
44
                                    ord(ack))
45
    except:
46
        raise errors.RadioError("Unable to communicate with the radio")
47

    
48
    radio.pipe.write(b"\x02")
49
    ident = radio.pipe.read(32)
50
    radio.pipe.write(b"A")
51
    r = radio.pipe.read(1)
52
    if r != b"A":
53
        raise errors.RadioError("Ack failed")
54
    return ident
55

    
56

    
57
def tyt_uvf8d_download(radio):
58
    data = uvf8d_identify(radio)
59
    for i in range(0, 0x4000, 0x20):
60
        msg = struct.pack(">cHb", b"R", i, 0x20)
61
        radio.pipe.write(msg)
62
        block = radio.pipe.read(0x20 + 4)
63
        if len(block) != (0x20 + 4):
64
            raise errors.RadioError("Radio sent a short block")
65
        radio.pipe.write(b"A")
66
        ack = radio.pipe.read(1)
67
        if ack != b"A":
68
            raise errors.RadioError("Radio NAKed block")
69
        data += block[4:]
70

    
71
        if radio.status_fn:
72
            status = chirp_common.Status()
73
            status.cur = i
74
            status.max = 0x4000
75
            status.msg = "Cloning from radio"
76
            radio.status_fn(status)
77

    
78
    radio.pipe.write(b"ENDR")
79

    
80
    return memmap.MemoryMapBytes(data)
81

    
82

    
83
def tyt_uvf8d_upload(radio):
84
    """Upload to TYT TH-UVF8D"""
85
    data = uvf8d_identify(radio)
86

    
87
    radio.pipe.timeout = 1
88

    
89
    if data != radio._mmap[:32]:
90
        raise errors.RadioError("Model mismatch: \n%s\n%s" %
91
                                (util.hexprint(data),
92
                                 util.hexprint(radio._mmap[:32])))
93

    
94
    for i in range(0, 0x4000, 0x20):
95
        addr = i + 0x20
96
        msg = struct.pack(">cHb", b"W", i, 0x20)
97
        msg += radio._mmap[addr:(addr + 0x20)]
98

    
99
        radio.pipe.write(msg)
100
        ack = radio.pipe.read(1)
101
        if ack != b"A":
102
            raise errors.RadioError("Radio did not ack block %i" % i)
103

    
104
        if radio.status_fn:
105
            status = chirp_common.Status()
106
            status.cur = i
107
            status.max = 0x4000
108
            status.msg = "Cloning to radio"
109
            radio.status_fn(status)
110

    
111
    # End of clone?
112
    radio.pipe.write(b"ENDW")
113

    
114
    # Checksum?
115
    final_data = radio.pipe.read(3)
116

    
117
# these require working desktop software
118
# TODO: DTMF features (ID, delay, speed, kill, etc.)
119

    
120
# TODO: Display Name
121

    
122

    
123
UVF8D_MEM_FORMAT = """
124
struct memory {
125
  lbcd rx_freq[4];
126
  lbcd tx_freq[4];
127
  lbcd rx_tone[2];
128
  lbcd tx_tone[2];
129

    
130
  u8 apro:4,
131
     rpt_md:2,
132
     unknown1:2;
133
  u8 bclo:2,
134
     wideband:1,
135
     ishighpower:1,
136
     unknown21:1,
137
     vox:1,
138
     pttid:2;
139
  u8 unknown3:8;
140

    
141
  u8 unknown4:6,
142
     duplex:2;
143

    
144
  lbcd offset[4];
145

    
146
  char unknown5[4];
147

    
148
  char name[7];
149

    
150
  char unknown6[1];
151
};
152

    
153
struct fm_broadcast_memory {
154
  lbcd freq[3];
155
  u8 unknown;
156
};
157

    
158
struct enable_flags {
159
  bit flags[8];
160
};
161

    
162
#seekto 0x0020;
163
struct memory channels[128];
164

    
165
#seekto 0x2020;
166
struct memory vfo1;
167
struct memory vfo2;
168

    
169
#seekto 0x2060;
170
struct {
171
  u8 unknown2060:4,
172
     tot:4;
173
  u8 unknown2061;
174
  u8 squelch;
175
  u8 unknown2063:4,
176
     vox_level:4;
177
  u8 tuning_step;
178
  char unknown12;
179
  u8 lamp_t;
180
  char unknown11;
181
  u8 unknown2068;
182
  u8 ani:1,
183
     scan_mode:2,
184
     unknown2069:2,
185
     beep:1,
186
     tx_sel:1,
187
     roger:1;
188
  u8 light:2,
189
     led:2,
190
     unknown206a:1,
191
     autolk:1,
192
     unknown206ax:2;
193
  u8 unknown206b:1,
194
     b_display:2,
195
     a_display:2,
196
     ab_switch:1,
197
     dwait:1,
198
     mode:1;
199
  u8 dw:1,
200
     unknown206c:6,
201
     voice:1;
202
  u8 unknown206d:2,
203
     rxsave:2,
204
     opnmsg:2,
205
     lock_mode:2;
206
  u8 a_work_area:1,
207
     b_work_area:1,
208
     unknown206ex:6;
209
  u8 a_channel;
210
  u8 b_channel;
211
  u8 pad3[15];
212
  char ponmsg[7];
213
} settings;
214

    
215
#seekto 0x2E60;
216
struct enable_flags enable[16];
217
struct enable_flags skip[16];
218

    
219
#seekto 0x2FA0;
220
struct fm_broadcast_memory fm_current;
221

    
222
#seekto 0x2FA8;
223
struct fm_broadcast_memory fm_memories[20];
224
"""
225

    
226
THUVF8D_DUPLEX = ["", "-", "+"]
227
THUVF8D_CHARSET = "".join([chr(ord("0") + x) for x in range(0, 10)] +
228
                          [" -*+"] +
229
                          [chr(ord("A") + x) for x in range(0, 26)] +
230
                          ["_/"])
231
TXSEL_LIST = ["EDIT", "BUSY"]
232
LED_LIST = ["Off", "Auto", "On"]
233
MODE_LIST = ["Memory", "VFO"]
234
AB_LIST = ["A", "B"]
235
DISPLAY_LIST = ["Channel", "Frequency", "Name"]
236
LIGHT_LIST = ["Purple", "Orange", "Blue"]
237
RPTMD_LIST = ["Off", "Reverse", "Talkaround"]
238
VOX_LIST = ["1", "2", "3", "4", "5", "6", "7", "8"]
239
WIDEBAND_LIST = ["Narrow", "Wide"]
240
TOT_LIST = ["Off", "30s", "60s", "90s", "120s", "150s", "180s", "210s",
241
            "240s", "270s"]
242
SCAN_MODE_LIST = ["Time", "Carry", "Seek"]
243
OPNMSG_LIST = ["Off", "DC (Battery)", "Message"]
244

    
245
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5),
246
                chirp_common.PowerLevel("Low", watts=0.5),
247
                ]
248

    
249
PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
250
BCLO_LIST = ["Off", "Wave", "Call"]
251
APRO_LIST = ["Off", "Compander", "Scramble 1", "Scramble 2", "Scramble 3",
252
             "Scramble 4", "Scramble 5", "Scramble 6", "Scramble 7",
253
             "Scramble 8"]
254
LOCK_MODE_LIST = ["PTT", "Key", "Key+S", "All"]
255

    
256
TUNING_STEPS_LIST = ["2.5", "5.0", "6.25", "10.0", "12.5",
257
                     "25.0", "50.0", "100.0"]
258
BACKLIGHT_TIMEOUT_LIST = ["1s", "2s", "3s", "4s", "5s",
259
                          "6s", "7s", "8s", "9s", "10s"]
260

    
261
SPECIALS = {
262
    "VFO1": -2,
263
    "VFO2": -1}
264

    
265

    
266
@directory.register
267
class TYTUVF8DRadio(chirp_common.CloneModeRadio):
268
    VENDOR = "TYT"
269
    MODEL = "TH-UVF8D"
270
    BAUD_RATE = 9600
271
    NEEDS_COMPAT_SERIAL = False
272

    
273
    def get_features(self):
274
        rf = chirp_common.RadioFeatures()
275
        rf.memory_bounds = (1, 128)
276
        rf.has_bank = False
277
        rf.has_ctone = True
278
        rf.has_tuning_step = False
279
        rf.has_cross = False
280
        rf.has_rx_dtcs = True
281
        rf.has_settings = True
282
        # it may actually be supported, but I haven't tested
283
        rf.can_odd_split = False
284
        rf.valid_duplexes = THUVF8D_DUPLEX
285
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
286
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-"
287
        rf.valid_bands = [(136000000, 174000000),
288
                          (400000000, 520000000)]
289
        rf.valid_skips = ["", "S"]
290
        rf.valid_power_levels = POWER_LEVELS
291
        rf.valid_modes = ["FM", "NFM"]
292
        rf.valid_special_chans = list(SPECIALS.keys())
293
        rf.valid_name_length = 7
294
        return rf
295

    
296
    def sync_in(self):
297
        self._mmap = tyt_uvf8d_download(self)
298
        self.process_mmap()
299

    
300
    def sync_out(self):
301
        tyt_uvf8d_upload(self)
302

    
303
    @classmethod
304
    def match_model(cls, filedata, filename):
305
        return filedata.startswith(b"TYT-F10\x00")
306

    
307
    def process_mmap(self):
308
        self._memobj = bitwise.parse(UVF8D_MEM_FORMAT, self._mmap)
309

    
310
    def _decode_tone(self, toneval):
311
        pol = "N"
312
        rawval = (toneval[1].get_bits(0xFF) << 8) | toneval[0].get_bits(0xFF)
313

    
314
        if toneval[0].get_bits(0xFF) == 0xFF:
315
            mode = ""
316
            val = 0
317
        elif toneval[1].get_bits(0xC0) == 0xC0:
318
            mode = "DTCS"
319
            val = int("%x" % (rawval & 0x3FFF))
320
            pol = "R"
321
        elif toneval[1].get_bits(0x80):
322
            mode = "DTCS"
323
            val = int("%x" % (rawval & 0x3FFF))
324
        else:
325
            mode = "Tone"
326
            val = int(toneval) / 10.0
327

    
328
        return mode, val, pol
329

    
330
    def _encode_tone(self, _toneval, mode, val, pol):
331
        toneval = 0
332
        if mode == "Tone":
333
            toneval = int("%i" % (val * 10), 16)
334
        elif mode == "DTCS":
335
            toneval = int("%i" % val, 16)
336
            toneval |= 0x8000
337
            if pol == "R":
338
                toneval |= 0x4000
339
        else:
340
            toneval = 0xFFFF
341

    
342
        _toneval[0].set_raw(toneval & 0xFF)
343
        _toneval[1].set_raw((toneval >> 8) & 0xFF)
344

    
345
    def get_raw_memory(self, number):
346
        return repr(self._memobj.channels[number - 1])
347

    
348
    def _get_memobjs(self, number):
349
        if isinstance(number, str):
350
            return (getattr(self._memobj, number.lower()), None)
351
        elif number < 0:
352
            for k, v in SPECIALS.items():
353
                if number == v:
354
                    return (getattr(self._memobj, k.lower()), None)
355
        else:
356
            return (self._memobj.channels[number - 1],
357
                    None)
358

    
359
    def get_memory(self, number):
360
        _mem, _name = self._get_memobjs(number)
361

    
362
        mem = chirp_common.Memory()
363

    
364
        if isinstance(number, str):
365
            mem.number = SPECIALS[number]
366
            mem.extd_number = number
367
        else:
368
            mem.number = number
369

    
370
        if _mem.get_raw().startswith("\xFF\xFF\xFF\xFF"):
371
            mem.empty = True
372
            return mem
373

    
374
        if isinstance(number, int):
375
            e = self._memobj.enable[(number - 1) / 8]
376
            enabled = e.flags[7 - ((number - 1) % 8)]
377
            s = self._memobj.skip[(number - 1) / 8]
378
            dont_skip = s.flags[7 - ((number - 1) % 8)]
379
        else:
380
            enabled = True
381
            dont_skip = True
382

    
383
        if not enabled:
384
            mem.empty = True
385
            return mem
386

    
387
        mem.freq = int(_mem.rx_freq) * 10
388

    
389
        mem.duplex = THUVF8D_DUPLEX[_mem.duplex]
390
        mem.offset = int(_mem.offset) * 10
391

    
392
        txmode, txval, txpol = self._decode_tone(_mem.tx_tone)
393
        rxmode, rxval, rxpol = self._decode_tone(_mem.rx_tone)
394

    
395
        chirp_common.split_tone_decode(mem,
396
                                       (txmode, txval, txpol),
397
                                       (rxmode, rxval, rxpol))
398

    
399
        mem.name = str(_mem.name).rstrip('\xFF ')
400

    
401
        if dont_skip:
402
            mem.skip = ''
403
        else:
404
            mem.skip = 'S'
405

    
406
        mem.mode = _mem.wideband and "FM" or "NFM"
407
        mem.power = POWER_LEVELS[1 - _mem.ishighpower]
408

    
409
        mem.extra = RadioSettingGroup("extra", "Extra Settings")
410

    
411
        rs = RadioSetting("pttid", "PTT ID",
412
                          RadioSettingValueList(PTTID_LIST,
413
                                                PTTID_LIST[_mem.pttid]))
414
        mem.extra.append(rs)
415

    
416
        rs = RadioSetting("vox", "VOX",
417
                          RadioSettingValueBoolean(_mem.vox))
418
        mem.extra.append(rs)
419

    
420
        rs = RadioSetting("bclo", "Busy Channel Lockout",
421
                          RadioSettingValueList(BCLO_LIST,
422
                                                BCLO_LIST[_mem.bclo]))
423
        mem.extra.append(rs)
424

    
425
        rs = RadioSetting("apro", "APRO",
426
                          RadioSettingValueList(APRO_LIST,
427
                                                APRO_LIST[_mem.apro]))
428
        mem.extra.append(rs)
429

    
430
        rs = RadioSetting("rpt_md", "Repeater Mode",
431
                          RadioSettingValueList(RPTMD_LIST,
432
                                                RPTMD_LIST[_mem.rpt_md]))
433
        mem.extra.append(rs)
434

    
435
        return mem
436

    
437
    def set_memory(self, mem):
438
        _mem, _name = self._get_memobjs(mem.number)
439

    
440
        e = self._memobj.enable[(mem.number - 1) / 8]
441
        s = self._memobj.skip[(mem.number - 1) / 8]
442
        if mem.empty:
443
            _mem.set_raw("\xFF" * 32)
444
            e.flags[7 - ((mem.number - 1) % 8)] = False
445
            s.flags[7 - ((mem.number - 1) % 8)] = False
446
            return
447
        else:
448
            e.flags[7 - ((mem.number - 1) % 8)] = True
449

    
450
        if _mem.get_raw() == ("\xFF" * 32):
451
            LOG.debug("Initializing empty memory")
452
            _mem.set_raw("\x00" * 32)
453

    
454
        _mem.rx_freq = mem.freq / 10
455
        if mem.duplex == "-":
456
            _mem.tx_freq = (mem.freq - mem.offset) / 10
457
        elif mem.duplex == "+":
458
            _mem.tx_freq = (mem.freq + mem.offset) / 10
459
        else:
460
            _mem.tx_freq = mem.freq / 10
461

    
462
        _mem.duplex = THUVF8D_DUPLEX.index(mem.duplex)
463
        _mem.offset = mem.offset / 10
464

    
465
        (txmode, txval, txpol), (rxmode, rxval, rxpol) = \
466
            chirp_common.split_tone_encode(mem)
467

    
468
        self._encode_tone(_mem.tx_tone, txmode, txval, txpol)
469
        self._encode_tone(_mem.rx_tone, rxmode, rxval, rxpol)
470

    
471
        _mem.name = mem.name.rstrip(' ').ljust(7, "\xFF")
472

    
473
        flag_index = 7 - ((mem.number - 1) % 8)
474
        s.flags[flag_index] = (mem.skip == "")
475
        _mem.wideband = mem.mode == "FM"
476
        _mem.ishighpower = mem.power == POWER_LEVELS[0]
477

    
478
        for element in mem.extra:
479
            setattr(_mem, element.get_name(), element.value)
480

    
481
    def get_settings(self):
482
        _settings = self._memobj.settings
483

    
484
        group = RadioSettingGroup("basic", "Basic")
485
        top = RadioSettings(group)
486

    
487
        group.append(RadioSetting(
488
                "mode", "Mode",
489
                RadioSettingValueList(
490
                    MODE_LIST, MODE_LIST[_settings.mode])))
491

    
492
        group.append(RadioSetting(
493
                "ab_switch", "A/B",
494
                RadioSettingValueList(
495
                    AB_LIST, AB_LIST[_settings.ab_switch])))
496

    
497
        group.append(RadioSetting(
498
                "a_channel", "A Selected Memory",
499
                RadioSettingValueInteger(1, 128, _settings.a_channel + 1)))
500

    
501
        group.append(RadioSetting(
502
                "b_channel", "B Selected Memory",
503
                RadioSettingValueInteger(1, 128, _settings.b_channel + 1)))
504

    
505
        group.append(RadioSetting(
506
                "a_display", "A Channel Display",
507
                RadioSettingValueList(
508
                    DISPLAY_LIST, DISPLAY_LIST[_settings.a_display])))
509
        group.append(RadioSetting(
510
                "b_display", "B Channel Display",
511
                RadioSettingValueList(
512
                    DISPLAY_LIST, DISPLAY_LIST[_settings.b_display])))
513
        group.append(RadioSetting(
514
                "tx_sel", "Priority Transmit",
515
                RadioSettingValueList(
516
                    TXSEL_LIST, TXSEL_LIST[_settings.tx_sel])))
517
        group.append(RadioSetting(
518
                "vox_level", "VOX Level",
519
                RadioSettingValueList(
520
                    VOX_LIST, VOX_LIST[_settings.vox_level])))
521

    
522
        group.append(RadioSetting(
523
                "squelch", "Squelch Level",
524
                RadioSettingValueInteger(0, 9, _settings.squelch)))
525

    
526
        group.append(RadioSetting(
527
                "dwait", "Dual Wait",
528
                RadioSettingValueBoolean(_settings.dwait)))
529

    
530
        group.append(RadioSetting(
531
                "led", "LED Mode",
532
                RadioSettingValueList(LED_LIST, LED_LIST[_settings.led])))
533

    
534
        group.append(RadioSetting(
535
                "light", "Light Color",
536
                RadioSettingValueList(
537
                    LIGHT_LIST, LIGHT_LIST[_settings.light])))
538

    
539
        group.append(RadioSetting(
540
                "beep", "Beep",
541
                RadioSettingValueBoolean(_settings.beep)))
542

    
543
        group.append(RadioSetting(
544
                "ani", "ANI",
545
                RadioSettingValueBoolean(_settings.ani)))
546

    
547
        group.append(RadioSetting(
548
                "tot", "Timeout Timer",
549
                RadioSettingValueList(TOT_LIST, TOT_LIST[_settings.tot])))
550

    
551
        group.append(RadioSetting(
552
                "roger", "Roger Beep",
553
                RadioSettingValueBoolean(_settings.roger)))
554

    
555
        group.append(RadioSetting(
556
                "dw", "Dual Watch",
557
                RadioSettingValueBoolean(_settings.dw)))
558

    
559
        group.append(RadioSetting(
560
                "rxsave", "RX Save",
561
                RadioSettingValueBoolean(_settings.rxsave)))
562

    
563
        def _filter(name):
564
            return str(name).rstrip("\xFF").rstrip()
565

    
566
        group.append(RadioSetting(
567
                "ponmsg", "Power-On Message",
568
                RadioSettingValueString(0, 7, _filter(_settings.ponmsg))))
569

    
570
        group.append(RadioSetting(
571
                "scan_mode", "Scan Mode",
572
                RadioSettingValueList(
573
                    SCAN_MODE_LIST, SCAN_MODE_LIST[_settings.scan_mode])))
574

    
575
        group.append(RadioSetting(
576
                "autolk", "Auto Lock",
577
                RadioSettingValueBoolean(_settings.autolk)))
578

    
579
        group.append(RadioSetting(
580
                "lock_mode", "Keypad Lock Mode",
581
                RadioSettingValueList(
582
                    LOCK_MODE_LIST, LOCK_MODE_LIST[_settings.lock_mode])))
583

    
584
        group.append(RadioSetting(
585
                "voice", "Voice Prompt",
586
                RadioSettingValueBoolean(_settings.voice)))
587

    
588
        group.append(RadioSetting(
589
                "opnmsg", "Opening Message",
590
                RadioSettingValueList(
591
                    OPNMSG_LIST, OPNMSG_LIST[_settings.opnmsg])))
592

    
593
        group.append(RadioSetting(
594
                "tuning_step", "Tuning Step",
595
                RadioSettingValueList(
596
                    TUNING_STEPS_LIST,
597
                    TUNING_STEPS_LIST[_settings.tuning_step])))
598

    
599
        group.append(RadioSetting(
600
                "lamp_t", "Backlight Timeout",
601
                RadioSettingValueList(
602
                    BACKLIGHT_TIMEOUT_LIST,
603
                    BACKLIGHT_TIMEOUT_LIST[_settings.lamp_t])))
604

    
605
        group.append(RadioSetting(
606
                "a_work_area", "A Work Area",
607
                RadioSettingValueList(
608
                    AB_LIST, AB_LIST[_settings.a_work_area])))
609

    
610
        group.append(RadioSetting(
611
                "b_work_area", "B Work Area",
612
                RadioSettingValueList(
613
                    AB_LIST, AB_LIST[_settings.b_work_area])))
614

    
615
        return top
616

    
617
        group.append(RadioSetting(
618
                "disnm", "Display Name",
619
                RadioSettingValueBoolean(_settings.disnm)))
620

    
621
        return group
622

    
623
    def set_settings(self, settings):
624
        _settings = self._memobj.settings
625

    
626
        for element in settings:
627
            if element.get_name() == 'rxsave':
628
                if bool(element.value.get_value()):
629
                    _settings.rxsave = 3
630
                else:
631
                    _settings.rxsave = 0
632
                continue
633
            if element.get_name().endswith('_channel'):
634
                LOG.debug(element.value, type(element.value))
635
                setattr(_settings, element.get_name(), int(element.value) - 1)
636
                continue
637
            if not isinstance(element, RadioSetting):
638
                self.set_settings(element)
639
                continue
640
            setattr(_settings, element.get_name(), element.value)
(2-2/6)