Project

General

Profile

New Model #4933 » bf-t1.py

Latest version, test this one. - Pavel Milanes, 12/13/2017 08:41 AM

 
1
# Copyright 2017 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 time
17
import struct
18
import logging
19

    
20
LOG = logging.getLogger(__name__)
21

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

    
27
# A note about the memmory in these radios
28
# mainly speculation until proven otherwise:
29
#
30
# The '9100' OEM software only manipulates the lower 0x180 bytes on read/write
31
# operations as we know, the file generated by the OEM software IN NOT an exact
32
# eeprom image, it's a crude text file with a pseudo csv format
33

    
34
MEM_SIZE = 0x180 # 384 bytes
35
BLOCK_SIZE = 0x10
36
ACK_CMD = "\x06"
37
MODES = ["NFM", "FM"]
38
SKIP_VALUES = ["S", ""]
39
TONES = chirp_common.TONES
40
DTCS = sorted(chirp_common.DTCS_CODES + [645])
41

    
42
# This is a general serial timeout for all serial read functions.
43
# Practice has show that about 0.07 sec will be enough to cover all radios.
44
STIMEOUT = 0.07
45

    
46
# this var controls the verbosity in the debug and by default it's low (False)
47
# make it True and you will to get a very verbose debug.log
48
debug = True
49

    
50
##### ID strings #####################################################
51

    
52
# BF-T1 handheld
53
BFT1_magic = "\x05PROGRAM"
54
BFT1_ident = " BF9100S"
55

    
56

    
57
def _clean_buffer(radio):
58
    """Cleaning the read serial buffer, hard timeout to survive an infinite
59
    data stream"""
60

    
61
    dump = "1"
62
    datacount = 0
63

    
64
    try:
65
        while len(dump) > 0:
66
            dump = radio.pipe.read(100)
67
            datacount += len(dump)
68
            # hard limit to survive a infinite serial data stream
69
            # 5 times bigger than a normal rx block (20 bytes)
70
            if datacount > 101:
71
                seriale = "Please check your serial port selection."
72
                raise errors.RadioError(seriale)
73

    
74
    except Exception:
75
        raise errors.RadioError("Unknown error cleaning the serial buffer")
76

    
77

    
78
def _rawrecv(radio, amount = 0):
79
    """Raw read from the radio device"""
80

    
81
    # var to hold the data to return
82
    data = ""
83

    
84
    try:
85
        if amount == 0:
86
            data = radio.pipe.read()
87
        else:
88
            data = radio.pipe.read(amount)
89

    
90
        # DEBUG
91
        if debug is True:
92
            LOG.debug("<== (%d) bytes:\n\n%s" %
93
                      (len(data), util.hexprint(data)))
94

    
95
        # fail if no data is received
96
        if len(data) == 0:
97
            raise errors.RadioError("No data received from radio")
98

    
99
    except:
100
        raise errors.RadioError("Error reading data from radio")
101

    
102
    return data
103

    
104

    
105
def _send(radio, data):
106
    """Send data to the radio device"""
107

    
108
    try:
109
        radio.pipe.write(data)
110

    
111
        # DEBUG
112
        if debug is True:
113
            LOG.debug("==> (%d) bytes:\n\n%s" %
114
                      (len(data), util.hexprint(data)))
115
    except:
116
        raise errors.RadioError("Error sending data to radio")
117

    
118

    
119
def _make_frame(cmd, addr, data=""):
120
    """Pack the info in the header format"""
121
    frame = struct.pack(">BHB", ord(cmd), addr, BLOCK_SIZE)
122

    
123
    # add the data if set
124
    if len(data) != 0:
125
        frame += data
126

    
127
    return frame
128

    
129

    
130
def _recv(radio, addr):
131
    """Get data from the radio"""
132

    
133
    # Get the full 20 bytes at a time
134
    # 4 bytes header + 16 bytes of data (BLOCK_SIZE)
135

    
136
    # get the whole block
137
    block = _rawrecv(radio, BLOCK_SIZE + 4)
138

    
139
    # short answer
140
    if len(block) < (BLOCK_SIZE + 4):
141
        raise errors.RadioError("Wrong block length (short) at 0x%04x" % addr)
142

    
143
    # long answer
144
    if len(block) > (BLOCK_SIZE + 4):
145
        raise errors.RadioError("Wrong block length (long) at 0x%04x" % addr)
146

    
147

    
148
    # header validation
149
    c, a, l = struct.unpack(">cHB", block[0:4])
150
    if c != "W" or a != addr or l != BLOCK_SIZE:
151
        LOG.debug("Invalid header for block 0x%04x:" % addr)
152
        LOG.debug("CMD: %s  ADDR: %04x  SIZE: %02x" % (c, a, l))
153
        raise errors.RadioError("Invalid header for block 0x%04x:" % addr)
154

    
155
    # return the data, 16 bytes of payload
156
    return block[4:]
157

    
158

    
159
def _start_clone_mode(radio, status):
160
    """Put the radio in clone mode, 3 tries"""
161

    
162
    # cleaning the serial buffer
163
    _clean_buffer(radio)
164

    
165
    # prep the data to show in the UI
166
    status.cur = 0
167
    status.msg = "Identifying the radio..."
168
    status.max = 3
169
    radio.status_fn(status)
170

    
171
    try:
172
        for a in range(0, status.max):
173
            # Update the UI
174
            status.cur = a + 1
175
            radio.status_fn(status)
176

    
177
            # send the magic word
178
            _send(radio, radio._magic)
179

    
180
            # Now you get a x06 of ACK if all goes well
181
            ack = _rawrecv(radio, 1)
182

    
183
            if ack == ACK_CMD:
184
                # DEBUG
185
                LOG.info("Magic ACK received")
186
                status.cur = status.max
187
                radio.status_fn(status)
188

    
189
                return True
190

    
191
        return False
192

    
193
    except errors.RadioError:
194
        raise
195
    except Exception, e:
196
        raise errors.RadioError("Error sending Magic to radio:\n%s" % e)
197

    
198

    
199
def _do_ident(radio, status):
200
    """Put the radio in PROGRAM mode & identify it"""
201
    #  set the serial discipline (default)
202
    radio.pipe.baudrate = 9600
203
    radio.pipe.parity = "N"
204
    radio.pipe.bytesize = 8
205
    radio.pipe.stopbits = 1
206
    radio.pipe.timeout = STIMEOUT
207

    
208
    # open the radio into program mode
209
    if _start_clone_mode(radio, status) is False:
210
        raise errors.RadioError("Radio did not enter clone mode, wrong model?")
211

    
212
    # Ok, poke it to get the ident string
213
    _send(radio, "\x02")
214
    ident = _rawrecv(radio, len(radio._id))
215

    
216
    # basic check for the ident
217
    if len(ident) != len(radio._id):
218
        raise errors.RadioError("Radio send a odd identification block.")
219

    
220
    # check if ident is OK
221
    if ident != radio._id:
222
        LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident))
223
        raise errors.RadioError("Radio identification failed.")
224

    
225
    # handshake
226
    _send(radio, ACK_CMD)
227
    ack = _rawrecv(radio, 1)
228

    
229
    #checking handshake
230
    if len(ack) == 1 and ack == ACK_CMD:
231
        # DEBUG
232
        LOG.info("ID ACK received")
233
    else:
234
        LOG.debug("Radio handshake failed.")
235
        raise errors.RadioError("Radio handshake failed.")
236

    
237
    # DEBUG
238
    LOG.info("Positive ident, this is a %s %s" % (radio.VENDOR, radio.MODEL))
239

    
240
    return True
241

    
242

    
243
def _download(radio):
244
    """Get the memory map"""
245

    
246
    # UI progress
247
    status = chirp_common.Status()
248

    
249
    # put radio in program mode and identify it
250
    _do_ident(radio, status)
251

    
252
    # reset the progress bar in the UI
253
    status.max = MEM_SIZE / BLOCK_SIZE
254
    status.msg = "Cloning from radio..."
255
    status.cur = 0
256
    radio.status_fn(status)
257

    
258
    # cleaning the serial buffer
259
    _clean_buffer(radio)
260

    
261
    data = ""
262
    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
263
        # sending the read request
264
        _send(radio, _make_frame("R", addr))
265

    
266
        # read
267
        d = _recv(radio, addr)
268

    
269
        # aggregate the data
270
        data += d
271

    
272
        # UI Update
273
        status.cur = addr / BLOCK_SIZE
274
        status.msg = "Cloning from radio..."
275
        radio.status_fn(status)
276

    
277
    # close comms with the radio
278
    _send(radio, "\x62")
279
    # DEBUG
280
    LOG.info("Close comms cmd sent, radio must reboot now.")
281

    
282
    return data
283

    
284

    
285
def _upload(radio):
286
    """Upload procedure"""
287

    
288
    # UI progress
289
    status = chirp_common.Status()
290

    
291
    # put radio in program mode and identify it
292
    _do_ident(radio, status)
293

    
294
    # get the data to upload to radio
295
    data = radio.get_mmap()
296

    
297
    # Reset the UI progress
298
    status.max = MEM_SIZE / BLOCK_SIZE
299
    status.cur = 0
300
    status.msg = "Cloning to radio..."
301
    radio.status_fn(status)
302

    
303
    # cleaning the serial buffer
304
    _clean_buffer(radio)
305

    
306
    # the fun start here
307
    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
308
        # getting the block of data to send
309
        d = data[addr:addr + BLOCK_SIZE]
310

    
311
        # build the frame to send
312
        frame = _make_frame("W", addr, d)
313

    
314
        # send the frame
315
        _send(radio, frame)
316

    
317
        # receiving the response
318
        ack = _rawrecv(radio, 1)
319

    
320
        # basic check
321
        if len(ack) != 1:
322
            raise errors.RadioError("No ACK when writing block 0x%04x" % addr)
323

    
324
        if ack != ACK_CMD:
325
            raise errors.RadioError("Bad ACK writing block 0x%04x:" % addr)
326

    
327
         # UI Update
328
        status.cur = addr / BLOCK_SIZE
329
        status.msg = "Cloning to radio..."
330
        radio.status_fn(status)
331

    
332
    # close comms with the radio
333
    _send(radio, "\x62")
334
    # DEBUG
335
    LOG.info("Close comms cmd sent, radio must reboot now.")
336

    
337

    
338
def _model_match(cls, data):
339
    """Match the opened/downloaded image to the correct version"""
340

    
341
    # we don't have a reliable fingerprint in the mem space by now.
342
    # then we just aim for a specific zone filled with \xff
343
    rid = data[0x0158:0x0160]
344

    
345
    if rid == ("\xff" * 8):
346
        return True
347

    
348
    return False
349

    
350

    
351
def _decode_ranges(low, high):
352
    """Unpack the data in the ranges zones in the memmap and return
353
    a tuple with the integer corresponding to the Mhz it means"""
354
    return (int(low) * 100000, int(high) * 100000)
355

    
356

    
357
MEM_FORMAT = """
358
#seekto 0x0000;         // normal 1-20 mem channels
359
                        // channel 0 is Emergent CH
360
struct {
361
  lbcd rxfreq[4];       // rx freq.
362
  u8 rxtone;            // x00 = none
363
                        // x01 - x32 = index of the analog tones
364
                        // x33 - x9b = index of Digital tones
365
                        // Digital tone polarity is handled below
366
  lbcd txoffset[4];     // the difference against RX
367
                        // pending to find the offset polarity in settings
368
  u8 txtone;            // Idem to rxtone
369
  u8 noskip:1,      // if true is included in the scan
370
     wide:1,        // 1 = Wide, 0 = Narrow
371
     ttondinv:1,    // if true TX tone is Digital & Inverted
372
     unA:1,         //
373
     rtondinv:1,    // if true RX tone is Digital & Inverted
374
     unB:1,         //
375
     offplus:1,     // TX = RX + offset
376
     offminus:1;    // TX = RX - offset
377
  u8 empty[5];
378
} memory[21];
379

    
380
#seekto 0x0150;     // Unknown data... settings?
381
struct {
382
  lbcd vhfl[2];     // VHF low limit
383
  lbcd vhfh[2];     // VHF high limit
384
  lbcd uhfl[2];     // UHF low limit
385
  lbcd uhfh[2];     // UHF high limit
386
  u8 finger[8];     // can we use this as fingerprint "\xFF" * 16
387
  u8 unknown0[16];
388
} settings;
389

    
390
#seekto 0x0170;     // Relay CH: same structure of memory ?
391
struct {
392
  lbcd rxfreq[4];       // rx freq.
393
  u8 rxtone;            // x00 = none
394
                        // x01 - x32 = index of the analog tones
395
                        // x33 - x9b = index of Digital tones
396
                        // Digital tone polarity is handled below
397
  lbcd txoffset[4];     // the difference against RX
398
                        // pending to find the offset polarity in settings
399
  u8 txtone;            // Idem to rxtone
400
  u8 noskip:1,      // if true is included in the scan
401
     wide:1,        // 1 = Wide, 0 = Narrow
402
     ttondinv:1,    // if true TX tone is Digital & Inverted
403
     unC:1,         //
404
     rtondinv:1,    // if true RX tone is Digital & Inverted
405
     unD:1,         //
406
     offplus:1,     // TX = RX + offset
407
     offminus:1;    // TX = RX - offset
408
  u8 empty[5];
409
} relaych;
410

    
411
"""
412

    
413

    
414
@directory.register
415
class BFT1(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio):
416
    """Baofeng BT-F1 radio & possibly alike radios"""
417
    VENDOR = "Baofeng"
418
    MODEL = "BF-T1"
419
    _vhf_range = (136000000, 174000000)
420
    _uhf_range = (400000000, 470000000)
421
    _upper = 20
422
    _magic = BFT1_magic
423
    _id = BFT1_ident
424

    
425
    @classmethod
426
    def get_prompts(cls):
427
        rp = chirp_common.RadioPrompts()
428
        rp.experimental = \
429
            ('This driver is experimental.\n'
430
             '\n'
431
             'Please keep a copy of your memories with the original software '
432
             'if you treasure them, this driver is new and may contain'
433
             ' bugs.\n'
434
             '\n'
435
             'Channel Zero is "Emergent CH", "Relay CH" is not implemented yet,'
436
             'and no settings or configuration by now.'
437
             )
438
        rp.pre_download = _(dedent("""\
439
            Follow these instructions to download your info:
440

    
441
            1 - Turn off your radio
442
            2 - Connect your interface cable
443
            3 - Turn on your radio
444
            4 - Do the download of your radio data
445

    
446
            """))
447
        rp.pre_upload = _(dedent("""\
448
            Follow these instructions to upload your info:
449

    
450
            1 - Turn off your radio
451
            2 - Connect your interface cable
452
            3 - Turn on your radio
453
            4 - Do the upload of your radio data
454

    
455
            """))
456
        return rp
457

    
458
    def get_features(self):
459
        """Get the radio's features"""
460

    
461
        rf = chirp_common.RadioFeatures()
462
        #~ rf.has_settings = True
463
        rf.has_bank = False
464
        rf.has_tuning_step = False
465
        rf.can_odd_split = True
466
        rf.has_name = False
467
        rf.has_offset = True
468
        rf.has_mode = True
469
        rf.valid_modes = MODES
470
        rf.has_dtcs = True
471
        rf.has_rx_dtcs = True
472
        rf.has_dtcs_polarity = True
473
        rf.has_ctone = True
474
        rf.has_cross = True
475
        rf.valid_duplexes = ["", "-", "+", "split"]
476
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
477
        rf.valid_cross_modes = [
478
            "Tone->Tone",
479
            "DTCS->",
480
            "->DTCS",
481
            "Tone->DTCS",
482
            "DTCS->Tone",
483
            "->Tone",
484
            "DTCS->DTCS"]
485
        rf.valid_skips = SKIP_VALUES
486
        rf.valid_dtcs_codes = DTCS
487
        rf.memory_bounds = (0, self._upper)
488

    
489
        # normal dual bands
490
        rf.valid_bands = [self._vhf_range, self._uhf_range]
491

    
492
        return rf
493

    
494
    def process_mmap(self):
495
        """Process the mem map into the mem object"""
496

    
497
        # Get it
498
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
499

    
500
        # set the band limits as the memmap
501
        settings = self._memobj.settings
502
        self._vhf_range = _decode_ranges(settings.vhfl, settings.vhfh)
503
        self._uhf_range = _decode_ranges(settings.uhfl, settings.uhfh)
504

    
505
    def sync_in(self):
506
        """Download from radio"""
507
        data = _download(self)
508
        self._mmap = memmap.MemoryMap(data)
509
        self.process_mmap()
510

    
511
    def sync_out(self):
512
        """Upload to radio"""
513

    
514
        try:
515
            _upload(self)
516
        except errors.RadioError:
517
            raise
518
        except Exception, e:
519
            raise errors.RadioError("Error: %s" % e)
520

    
521
    def _decode_tone(self, val, inv):
522
        """Parse the tone data to decode from mem, it returns:
523
        Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)"""
524

    
525
        if val == 0:
526
            return '', None, None
527
        elif val < 51:  # analog tone
528
            return 'Tone', TONES[val - 1], None
529
        elif val > 50:  # digital tone
530
            pol = "N"
531
            # polarity?
532
            if inv == 1:
533
                pol = "R"
534

    
535
            return 'DTCS', DTCS[val - 51], pol
536

    
537
    def _encode_tone(self, memtone, meminv, mode, tone, pol):
538
        """Parse the tone data to encode from UI to mem"""
539

    
540
        if mode == '' or mode is None:
541
            memtone.set_value(0)
542
            meminv.set_value(0)
543
        elif mode == 'Tone':
544
            # caching errors for analog tones.
545
            try:
546
                memtone.set_value(TONES.index(tone) + 1)
547
                meminv.set_value(0)
548
            except:
549
                msg = "TCSS Tone '%d' is not supported" % tone
550
                LOG.error(msg)
551
                raise errors.RadioError(msg)
552

    
553
        elif mode == 'DTCS':
554
            # caching errors for digital tones.
555
            try:
556
                memtone.set_value(DTCS.index(tone) + 51)
557
                if pol == "R":
558
                    meminv.set_value(True)
559
                else:
560
                    meminv.set_value(False)
561
            except:
562
                msg = "Digital Tone '%d' is not supported" % tone
563
                LOG.error(msg)
564
                raise errors.RadioError(msg)
565
        else:
566
            msg = "Internal error: invalid mode '%s'" % mode
567
            LOG.error(msg)
568
            raise errors.InvalidDataError(msg)
569

    
570
    def get_raw_memory(self, number):
571
        return repr(self._memobj.memory[number])
572

    
573
    def get_memory(self, number):
574
        """Get the mem representation from the radio image"""
575
        _mem = self._memobj.memory[number]
576

    
577
        # Create a high-level memory object to return to the UI
578
        mem = chirp_common.Memory()
579

    
580
        # Memory number
581
        mem.number = number
582

    
583
        if _mem.get_raw()[0] == "\xFF":
584
            mem.empty = True
585
            return mem
586

    
587
        # Freq and offset
588
        mem.freq = int(_mem.rxfreq) * 10
589

    
590
        # TX freq (Stored as a difference)
591
        mem.offset = int(_mem.txoffset) * 10
592
        mem.duplex = ""
593

    
594
        # must work out the polarity
595
        if mem.offset != 0:
596
            if _mem.offminus == 1:
597
                mem.duplex = "-"
598
                #  tx below RX
599

    
600
            if _mem.offplus == 1:
601
                #  tx above RX
602
                mem.duplex = "+"
603

    
604
            # split RX/TX in different bands
605
            if mem.offset > 71000000:
606
                mem.duplex = "split"
607

    
608
                # show the actual value in the offset, depending on the shift
609
                if _mem.offminus == 1:
610
                    mem.offset = mem.freq - mem.offset
611
                if _mem.offplus == 1:
612
                    mem.offset = mem.freq + mem.offset
613

    
614
        # wide/narrow
615
        mem.mode = MODES[int(_mem.wide)]
616

    
617
        # skip
618
        mem.skip = SKIP_VALUES[_mem.noskip]
619

    
620
        # tone data
621
        rxtone = txtone = None
622
        txtone = self._decode_tone(_mem.txtone, _mem.ttondinv)
623
        rxtone = self._decode_tone(_mem.rxtone, _mem.rtondinv)
624
        chirp_common.split_tone_decode(mem, txtone, rxtone)
625

    
626

    
627
        return mem
628

    
629
    def set_memory(self, mem):
630
        """Set the memory data in the eeprom img from the UI"""
631
        # get the eprom representation of this channel
632
        _mem = self._memobj.memory[mem.number]
633

    
634
        # if empty memmory
635
        if mem.empty:
636
            # the channel itself
637
            _mem.set_raw("\xFF" * 16)
638
            # return it
639
            return mem
640

    
641
        # frequency
642
        _mem.rxfreq = mem.freq / 10
643

    
644
        # duplex/ offset Offset is an absolute value
645
        _mem.txoffset = mem.offset / 10
646

    
647
        # must work out the polarity
648
        if mem.duplex == "":
649
            _mem.offplus = 0
650
            _mem.offminus = 0
651
        elif mem.duplex == "+":
652
            _mem.offplus = 1
653
            _mem.offminus = 0
654
        elif mem.duplex == "-":
655
            _mem.offplus = 0
656
            _mem.offminus = 1
657
        elif mem.duplex == "split":
658
            if mem.freq > mem.offset:
659
                _mem.offplus = 0
660
                _mem.offminus = 1
661
                _mem.txoffset = (mem.freq - mem.offset) / 10
662
            else:
663
                _mem.offplus = 1
664
                _mem.offminus = 0
665
                _mem.txoffset = (mem.offset - mem.freq) / 10
666

    
667
        # wide/narrow
668
        _mem.wide = MODES.index(mem.mode)
669

    
670
        # skip
671
        _mem.noskip = SKIP_VALUES.index(mem.skip)
672

    
673
        # tone data
674
        ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
675
            chirp_common.split_tone_encode(mem)
676
        self._encode_tone(_mem.txtone, _mem.ttondinv, txmode, txtone, txpol)
677
        self._encode_tone(_mem.rxtone, _mem.rtondinv, rxmode, rxtone, rxpol)
678

    
679
        return mem
680

    
681
    @classmethod
682
    def match_model(cls, filedata, filename):
683
        match_size = False
684
        match_model = False
685

    
686
        # testing the file data size
687
        if len(filedata) == MEM_SIZE:
688
            match_size = True
689

    
690
            # DEBUG
691
            if debug is True:
692
                LOG.debug("BF-T1 matched!")
693

    
694

    
695
        # testing the firmware model fingerprint
696
        match_model = _model_match(cls, filedata)
697

    
698
        if match_size and match_model:
699
            return True
700
        else:
701
            return False
(59-59/77)