Project

General

Profile

New Model #7599 » ftlx011.py

first dev version of the driver - Pavel Milanes, 07/29/2021 08:47 PM

 
1
# Copyright 2019 Pavel Milanes, CO7WT <pavelmc@gmail.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 2 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 struct
17
import os
18
import logging
19
import time
20

    
21
from time import sleep
22
from chirp import chirp_common, directory, memmap, errors, util, bitwise
23
from textwrap import dedent
24

    
25
LOG = logging.getLogger(__name__)
26

    
27
### SAMPLE MEM DUMP as sent from the radios
28

    
29
# FTL-1011
30
#0x000000  52 f0 16 90 04 08 38 c0  00 00 00 01 00 00 00 ff  |R.....8.........|
31
#0x000010  20 f1 00 20 00 00 00 20  04 47 25 04 47 25 00 00  | .. ... .G%.G%..|
32

    
33
# FTL-2011
34
#0x000000: 50 90 21 40 04 80 fc 40  00 00 00 01 00 00 00 ff  |P.!@...@........|
35
#0x000010: 20 f1 00 0b 00 00 00 0b  14 51 70 14 45 70 00 00  |.........Qp.Ep..|
36

    
37

    
38
MEM_FORMAT = """
39
#seekto 0x000;
40
u8 rid[2];              // Radio Identification?
41
bbcd if[2];             // Radio internal IF (21.40 Mhz)
42
u8 chcount;             // how many channels are programmed
43
u8 unknownB1:1,
44
   unknownB2:1,
45
   unknownB3:1,
46
   monitor:1,           // monitor disable; 1 = disabled
47
   offhook:1,           // offhook disable; 1 = disabled
48
   unknownB6:1,
49
   unknownB7:1,
50
   unknownB8:1;
51
u8 TOT:4,               // TOT in 30 sec steps, 0 = disable / 15 = 7.5min
52
   unknownB:4;
53

    
54
#seekto 0x010;
55
struct {
56
  u8 empty:1,           // channel is empty = 1
57
     notx:1,            // TX inhibit (TX freq is ignored)
58
     tot:1,             // TOT: 0 =  disabled / 1 = enabled
59
     low:1,             // low power; 1 = low / 0 = high
60
     busylock:1,        // PTT carrier lockout on busy channel
61
     unknownA5:1,
62
     unknownA6:1,
63
     unknownA7:1;
64
  u8 chname;
65
  u8 rx_tone[2];      // empty value is \x00\x0B / disabled is \x00\x00
66
  u8 unknown4;
67
  u8 unknown5;
68
  u8 tx_tone[2];      // empty value is \x00\x0B / disabled is \x00\x00
69
  bbcd rx_freq[3];      // RX freq
70
  bbcd tx_freq[3];      // TX freq
71
  u8 unknownA[2];
72
} memory[24];
73

    
74
#seekto 0x0190;
75
char filename[11];
76

    
77
#seekto 0x19C;
78
u8 checksum;
79
"""
80

    
81
MEM_SIZE = 0x019C
82
DTCS_CODES = chirp_common.DTCS_CODES
83
# make a copy of the tones, is not funny to work with this directly
84
TONES = list(chirp_common.TONES)
85
# this old radios has not the full tone ranges in CST
86
invalid_tones = (
87
    69.3,
88
    159.8,
89
    165.5,
90
    171.3,
91
    177.3,
92
    183.5,
93
    189.9,
94
    196.6,
95
    199.5,
96
    206.5,
97
    229.1,
98
    245.1)
99

    
100
# remove invalid tones
101
for tone in invalid_tones:
102
    try:
103
        TONES.remove(tone)
104
    except:
105
        pass
106

    
107

    
108
def _set_serial(radio):
109
    """Set the serial protocol settings"""
110
    radio.pipe.timeout = 10
111
    radio.pipe.parity = "N"
112
    radio.pipe.baudrate = 9600
113

    
114

    
115
def _checksum(data):
116
    """the radio block checksum algorithm"""
117
    cs = 0
118
    for byte in data:
119
            cs += ord(byte)
120

    
121
    return cs % 256
122

    
123

    
124
def _update_cs(radio):
125
    """Update the checksum on the mmap"""
126
    payload = str(radio.get_mmap())[:-1]
127
    cs = _checksum(payload)
128
    radio._mmap[MEM_SIZE - 1] = cs
129

    
130

    
131
def _do_download(radio):
132
    """ The download function """
133
    # Get the whole 413 bytes (0x019D) bytes one at a time with plenty of time
134
    # to get to the user's pace
135

    
136
    # set serial discipline
137
    _set_serial(radio)
138

    
139
    # UI progress
140
    status = chirp_common.Status()
141
    status.cur = 0
142
    status.max = MEM_SIZE
143
    status.msg = " Press A to clone. "
144
    radio.status_fn(status)
145

    
146
    data = ""
147
    for i in range(0, MEM_SIZE):
148
        a = radio.pipe.read(1)
149
        if len(a) == 0:
150
            # error, no received data
151
            if len(data) != 0:
152
                # received some data, not the complete stream
153
                msg = "Just %02i bytes of the %02i received, try again." % \
154
                    (len(data), MEM_SIZE)
155
            else:
156
                # timeout, please retry
157
                msg = "No data received, try again."
158

    
159
            raise errors.RadioError(msg)
160

    
161
        data += a
162
        # UI Update
163
        status.cur = len(data)
164
        radio.status_fn(status)
165

    
166
    if len(data) != MEM_SIZE:
167
        msg = "Incomplete data, we need %02i but got %02i bytes." % \
168
            (MEM_SIZE, len(data))
169
        raise errors.RadioError(msg)
170

    
171
    if ord(data[-1]) != _checksum(data[:-1]):
172
        msg = "Bad checksum, please try again."
173
        raise errors.RadioError(msg)
174

    
175
    return data
176

    
177

    
178
def _do_upload(radio):
179
    """The upload function"""
180
    # set serial discipline
181
    _set_serial(radio)
182

    
183
    # UI progress
184
    status = chirp_common.Status()
185

    
186
    # 10 seconds timeout
187
    status.cur = 0
188
    status.max = 100
189
    status.msg = " Quick, press MON on the radio to start. "
190
    radio.status_fn(status)
191

    
192
    for byte in range(0,100):
193
        status.cur = byte
194
        radio.status_fn(status)
195
        time.sleep(0.1)
196

    
197

    
198
    # real upload if user don't cancel the timeout
199
    status.cur = 0
200
    status.max = MEM_SIZE
201
    status.msg = " Cloning to radio... "
202
    radio.status_fn(status)
203

    
204
    # send data
205
    data = str(radio.get_mmap())
206

    
207
    # this radio has a trick, the EEPROM is an ancient SPI one, so it needs
208
    # some time to write, so we send every byte and then allow
209
    # a 0.01 seg to complete the write from the MCU to the SPI EEPROM
210
    c = 0
211
    for byte in data:
212
        radio.pipe.write(byte)
213
        time.sleep(0.01)
214

    
215
        # UI Update
216
        status.cur = c
217
        radio.status_fn(status)
218

    
219
        # counter
220
        c = c + 1
221

    
222

    
223
def _model_match(cls, data):
224
    """Use a experimental guess to determine if the radio you just
225
    downloaded or the img you opened is for this model"""
226

    
227
    # It's hard to tell when this radio is really this radio.
228
    # I use the first 4 bytes, that appears to be the ID and FI settings
229

    
230
    LOG.debug("Drivers's ID string:")
231
    LOG.debug(util.hexprint(data[0:4]))
232
    LOG.debug("Radio's ID string:")
233
    LOG.debug(cls.finger)
234

    
235
    fp = data[0:4]
236
    if fp == cls.finger:
237
        return True
238
    else:
239
        return False
240

    
241

    
242
def bcd_to_int(data):
243
    """Convert an array of bcdDataElement like \x12
244
    into an int like 12"""
245
    value = 0
246
    a = (data & 0xF0) >> 4
247
    b = data & 0x0F
248
    value = (a * 10) + b
249
    return value
250

    
251

    
252
def int_to_bcd(data):
253
    """Convert a int like 94 to 0x94"""
254
    data, lsb = divmod(data, 10)
255
    data, msb = divmod(data, 10)
256
    res = (msb << 4) + lsb
257
    return res
258

    
259

    
260
class ftlx011(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio):
261
    """Vertex FTL1011/2011/7011 4/8/12/24 channels"""
262
    VENDOR = "Vertex Standard"
263
    _memsize = MEM_SIZE
264
    _upper = 0
265
    _range = []
266
    finger = ""
267

    
268
    @classmethod
269
    def get_prompts(cls):
270
        rp = chirp_common.RadioPrompts()
271
        rp.experimental = \
272
            ('This is a experimental driver, use it on your own risk.\n'
273
             '\n'
274
             'By now only the 5.0 khz step models are supported. The 6.25 '
275
             'kHz steps models will be ignored as we do not have one of '
276
             'these to process and support.\n'
277
             )
278
        rp.pre_download = _(dedent("""\
279
            Please follow this steps carefully:
280

    
281
            1 - Turn on your radio
282
            2 - Connect the interface cable to your radio.
283
            3 - Click the button on this window to start download
284
                (Radio will beep and led will flash)
285
            4 - Then press the "A" button in your radio to start cloning.
286
                (At the end radio will beep)
287
            """))
288
        rp.pre_upload = _(dedent("""\
289
            Please follow this steps carefully:
290

    
291
            1 - Turn on your radio
292
            2 - Connect the interface cable to your radio
293
            3 - Click the button on this window to start download
294
                (you may see another dialog, click ok)
295
            4 - Radio will beep and led will flash
296
            5 - You will get a 10 seconds timeout to press "MON" before
297
                data upload start
298
            6 - If all goes right radio will beep at end.
299

    
300
            After cloning remove the cable and power cycle your radio to
301
            get into normal mode.
302
            """))
303
        return rp
304

    
305
    def get_features(self):
306
        """Return information about this radio's features"""
307
        rf = chirp_common.RadioFeatures()
308
        rf.has_settings = False
309
        rf.has_bank = False
310
        rf.has_tuning_step = False
311
        rf.has_name = False
312
        #rf.valid_characters = VALID_CHARS
313
        #rf.valid_name_length = 2
314
        rf.has_offset = True
315
        rf.has_mode = True
316
        rf.has_dtcs = True
317
        rf.has_rx_dtcs = True
318
        rf.has_dtcs_polarity = False
319
        rf.has_ctone = True
320
        rf.has_cross = True
321
        rf.valid_duplexes = ["", "-", "+", "off"]
322
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
323
        rf.valid_cross_modes = [
324
            "Tone->Tone",
325
            "DTCS->DTCS",
326
            "DTCS->",
327
            "->DTCS"]
328
        rf.valid_dtcs_codes = DTCS_CODES
329
        rf.valid_skips = []
330
        rf.valid_modes = ["FM"]
331
        #rf.valid_tuning_steps = [5.0]
332
        rf.valid_bands = [self._range]
333
        rf.memory_bounds = (1, self._upper)
334
        return rf
335

    
336
    def sync_in(self):
337
        """Do a download of the radio eeprom"""
338
        try:
339
            data = _do_download(self)
340
        except Exception, e:
341
            raise errors.RadioError("Failed to communicate with radio:\n %s" % e)
342

    
343
        # match model
344
        if _model_match(self, data) is False:
345
            raise errors.RadioError("Incorrect radio model")
346

    
347
        self._mmap = memmap.MemoryMap(data)
348
        self.process_mmap()
349

    
350
        # set the channel count from the radio eeprom
351
        self._upper = int(ord(data[4]))
352

    
353
    def sync_out(self):
354
        """Do an upload to the radio eeprom"""
355
        # update checksum
356
        _update_cs(self)
357

    
358
        # sanity check, match model
359
        data = str(self.get_mmap())
360
        if len(data) != MEM_SIZE:
361
            raise errors.RadioError("Wrong radio image? Size miss match.")
362

    
363
        if _model_match(self, data) is False:
364
            raise errors.RadioError("Wrong image? Fingerprint miss match")
365

    
366
        try:
367
            _do_upload(self)
368
        except Exception, e:
369
            msg = "Failed to communicate with radio:\n%s" % e
370
            raise errors.RadioError(msg)
371

    
372
    def process_mmap(self):
373
        """Process the memory object"""
374
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
375

    
376
    def get_raw_memory(self, number):
377
        """Return a raw representation of the memory object"""
378
        return repr(self._memobj.memory[number])
379

    
380
    def _decode_tone(self, mem, rx=True):
381
        """Parse the tone data to decode from mem tones are encodded like this
382
        CTCS: mapped [0x80...0xa5] = [67.0...250.3]
383
        DTCS: mixed  [0x88, 0x23] last is BCD and first is the 100 power - 88
384

    
385
        It return: ((''|DTCS|Tone), Value (None|###), None)"""
386
        mode = ""
387
        tone = None
388

    
389
        # get the tone depending of rx or tx
390
        if rx:
391
            t = mem.rx_tone
392
        else:
393
            t = mem.tx_tone
394
        
395
        tMSB = t[0]
396
        tLSB = t[1]
397

    
398
        print ("==>> %s, %i, %i" % (t, tMSB, tLSB))
399

    
400
        # no tone at all
401
        if (tMSB == 0 and tLSB < 128):
402
            print("No tone")
403
            return ('', None, None)
404

    
405
        # extract the tone info
406
        if tMSB == 0x00:
407
            # CTCS
408
            mode = "Tone"
409
            try:
410
                tone = TONES[tLSB - 128]
411
                print ("  >> %i" % tone)
412
            except IndexError:
413
                LOG.debug("Error decoding a CTCS tone")
414
                pass
415
        else:
416
            # DTCS
417
            mode = "DTCS"
418
            try:
419
                tone = ((tMSB - 0x88) * 100) + bcd_to_int(tLSB)
420
                print("  >> %i" % tone)
421
            except IndexError:
422
                LOG.debug("Error decoding a DTCS tone")
423
                pass
424

    
425
        return (mode, tone, None)
426

    
427
    def _encode_tone(self, mem, mode, value, pol, rx=True):
428
        """Parse the tone data to encode from UI to mem
429
        CTCS: mapped [0x80...0xa5] = [67.0...250.3]
430
        DTCS: mixed  [0x88, 0x23] last is BCD and first is the 100 power - 88
431
        """
432

    
433
        # array to pass
434
        tone = [0x00, 0x00]
435

    
436
        # which mod
437
        if mode == "DTCS":
438
            tone[0] = int(value / 100) + 0x88
439
            tone[1] = int_to_bcd(value % 100)
440
        
441
        if mode == "Tone":
442
            #CTCS
443
            tone[1] = TONES.index(value) + 128
444

    
445
        # set it
446
        if rx:
447
            mem.rx_tone = tone
448
        else:
449
            mem.tx_tone = tone
450

    
451
    def get_memory(self, number):
452
        """Extract a memory object from the memory map"""
453
        # Get a low-level memory object mapped to the image
454
        _mem = self._memobj.memory[number - 1]
455
        # Create a high-level memory object to return to the UI
456
        mem = chirp_common.Memory()
457
        # number
458
        mem.number = number
459

    
460
        # empty
461
        if bool(_mem.empty) is True:
462
            mem.empty = True
463
            return mem
464

    
465
        # rx freq
466
        mem.freq = int(_mem.rx_freq) * 1000
467

    
468
        # checking if tx freq is disabled
469
        if bool(_mem.notx) is True:
470
            mem.duplex = "off"
471
            mem.offset = 0
472
        else:
473
            tx = int(_mem.tx_freq) * 1000
474
            if tx == mem.freq:
475
                mem.offset = 0
476
                mem.duplex = ""
477
            else:
478
                mem.duplex = mem.freq > tx and "-" or "+"
479
                mem.offset = abs(tx - mem.freq)
480

    
481
        # tone data
482
        rxtone = txtone = None
483
        rxtone = self._decode_tone(_mem)
484
        txtone = self._decode_tone(_mem, False)
485
        chirp_common.split_tone_decode(mem, txtone, rxtone)
486

    
487
        # this radio has a primitive mode to show the channel number on a 7-segment
488
        # two digit LCD, we will use channel number
489
        # we will use a trick to show the numbers < 10 wit a space not a zero in front
490
        chname = int_to_bcd(mem.number)
491
        if mem.number < 10:
492
            # convert to F# as BCD
493
            chname = mem.number + 240
494

    
495
        _mem.chname = chname
496

    
497
        # return mem
498
        return mem
499

    
500
    def set_memory(self, mem):
501
        """Store details about a high-level memory to the memory map
502
        This is called when a user edits a memory in the UI"""
503
        # Get a low-level memory object mapped to the image
504
        _mem = self._memobj.memory[mem.number - 1]
505

    
506
        # Empty memory
507
        if mem.empty:
508
            _mem.empty = True
509
            _mem.rx_freq = _mem.tx_freq = 0
510
            return
511

    
512
        # freq rx
513
        _mem.rx_freq = mem.freq / 1000
514

    
515
        # freq tx
516
        if mem.duplex == "+":
517
            _mem.tx_freq = (mem.freq + mem.offset) / 1000
518
        elif mem.duplex == "-":
519
            _mem.tx_freq = (mem.freq - mem.offset) / 1000
520
        elif mem.duplex == "off":
521
            _mem.notx = 1
522
            _mem.tx_freq = _mem.rx_freq
523
        else:
524
            _mem.tx_freq = mem.freq / 1000
525

    
526
        # tone data
527
        ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
528
            chirp_common.split_tone_encode(mem)
529

    
530
        # validate tone data from here
531
        if rxmode == "Tone" and rxtone in invalid_tones:
532
            msg = "The tone %shz is not valid for this radio" % rxtone
533
            raise errors.UnsupportedToneError(msg)
534

    
535
        if txmode == "Tone" and txtone in invalid_tones:
536
            msg = "The tone %shz is not valid for this radio" % txtone
537
            raise errors.UnsupportedToneError(msg)
538

    
539
        if rxmode == "DTCS" and rxtone not in DTCS_CODES:
540
            msg = "The digital tone %s is not valid for this radio" % rxtone
541
            raise errors.UnsupportedToneError(msg)
542

    
543
        if txmode == "DTCS" and txtone not in DTCS_CODES:
544
            msg = "The digital tone %s is not valid for this radio" % txtone
545
            raise errors.UnsupportedToneError(msg)
546

    
547
        self._encode_tone(_mem, rxmode, rxtone, rxpol)
548
        self._encode_tone(_mem, txmode, txtone, txpol, False)
549

    
550
        # this radio has a primitive mode to show the channel number on a 7-segment
551
        # two digit LCD, we will use channel number
552
        # we will use a trick to show the numbers < 10 wit a space not a zero in front
553
        chname = int_to_bcd(mem.number)
554
        if mem.number < 10:
555
            # convert to F# as BCD
556
            chname = mem.number + 240
557

    
558
        _mem.chname = chname
559

    
560
        return mem
561

    
562
    @classmethod
563
    def match_model(cls, filedata, filename):
564
        match_size = False
565
        match_model = False
566

    
567
        # testing the file data size
568
        if len(filedata) == cls._memsize:
569
            match_size = True
570
            print("Comp: %i file / %i memzise" % (len(filedata), cls._memsize) )
571

    
572
        # testing the firmware fingerprint, this experimental
573
        match_model = _model_match(cls, filedata)
574

    
575
        if match_size and match_model:
576
            return True
577
        else:
578
            return False
579

    
580

    
581
@directory.register
582
class ftl1011(ftlx011):
583
    """Vertex FTL1011"""
584
    MODEL = "FTL-1011"
585
    _memsize = MEM_SIZE
586
    _upper = 4
587
    _range = [44000000, 56000000]
588
    finger = "\x52\xf0\x16\x90"
589

    
590

    
591
@directory.register
592
class ftl2011(ftlx011):
593
    """Vertex FTL2011"""
594
    MODEL = "FTL-2011"
595
    _memsize = MEM_SIZE
596
    _upper = 24
597
    _range = [134000000, 174000000]
598
    finger = "\x50\x90\x21\x40"
599

    
(2-2/16)