Project

General

Profile

New Model #4851 » th8600.py

Andy Knitt, 10/02/2023 07:07 PM

 
1
# Version 1.0 for TYT-TH8600
2
# Initial radio protocol decode, channels and memory layout, basic settings
3
# by Andy Knitt <andyknitt@gmail.com>, Summer 2023
4
#
5
# This program is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation, either version 2 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program.
17

    
18
import struct
19
import logging
20
import math
21
from chirp import chirp_common, directory, memmap
22
from chirp import bitwise, errors, util
23
from chirp.settings import RadioSettingGroup, RadioSetting, \
24
    RadioSettingValueBoolean, RadioSettingValueList, \
25
    RadioSettingValueString, RadioSettings
26

    
27
LOG = logging.getLogger(__name__)
28

    
29
MEM_FORMAT = """
30
struct chns {
31
  ul32 rxfreq;
32
  ul32 txfreq;
33
  ul16 rxtone;
34
  ul16 txtone;
35

    
36
  u8 power:2          // High=00,Mid=01,Low=10
37
     mode:2          //  WFM=00, FM=01, NFM=10
38
     b_lock:2         // off=00, sub=01, carrier=10
39
     REV:1
40
     TxInh:1;
41
  u8 sqlmode:3         // SQ=000, CT=001, Tone=010,
42
                       //CT or Tone=011, CTC and Tone=100
43
     signal:2          // off=00, dtmf=01, 2-tone=10, 5-tone=11
44
     display: 1
45
     talkoff: 1
46
     TBD: 1;
47
  u8 fivetonepttid: 2  //off=00, begin=01,end=10,both=11
48
     dtmfpttid: 2      //off=00, begin=01,end=10,both=11
49
     tuning_step: 4;   //
50
  u8 name[6];
51
};
52

    
53
struct chname {
54
  u8  extra_name[10];
55
};
56

    
57
#seekto 0x0000;
58
struct chns chan_mem[200];
59
struct chns scanlimits[4];
60
struct chns vfos[6];
61

    
62
#seekto 0x1960;
63
struct chname chan_name[200];
64

    
65
#seekto 0x1160;
66
struct {
67
  u8 introScreen1[12];    // 0x1160 *Intro Screen Line 1(truncated to 12 alpha
68
                          //         text characters)
69
  u8 unk_bit9 : 1,        // 0x116C
70
     subDisplay : 2,      //   0b00 = OFF; 0b01 = frequency; 0b10 = voltage
71
     unk_bit8 : 1,        //
72
     sqlLevel : 4;        //        *OFF, 1-9
73
  u8 beep : 1,            // 0x116D *OFF, On
74
     burstFreq : 2,       //        1750,2100,1000,1450 Hz tone burst frequency
75
     unkstr4: 4,          //
76
     txChSelect : 1;      //        *Last CH, Main CH
77
  u8 unk_bit7 : 1,        // 0x116E
78
     autoPowOff : 2,      //        OFF, 30Min, 1HR, 2HR
79
     tot : 5;             //        OFF, time in minutes (1 to 30)
80
  u8 pttRelease: 2,       // 0x116F  OFF, Begin, After, Both
81
     unk_bit6: 2,         //
82
     sqlTailElim: 2,      //        OFF, Frequency, No Frequency
83
     disableReset:1,      //        NO, YES
84
     menuOperation:1;     //        NO, YES
85
  u8 scanResumeTime : 2,  // 0x1170 2S, 5S, 10S, 15S
86
     disMode : 2,         //        Frequency, Channel, Name
87
     scanType: 2,         //        Time operated, Carrier operated, Se
88
     ledMode: 2;          //        On, 5 second, 10 second
89
  u8 unky;                // 0x1171
90
  u8 usePowerOnPw : 1,    // 0x1172 NO, YES
91
     elimTailNoTone: 1,   //        NO, YES
92
     unk6 : 6;            //
93
  u8 unk;                 // 0x1173
94
  u8 unk_bit5 : 1,        // 0x1174
95
     mzKeyFunc : 3,      //   A/B, Low, Monitor, Scan, Tone, M/V, MHz, Mute
96
     unk_bit4 : 1,        //
97
     lowKeyFunc : 3;      //   A/B, Low, Monitor, Scan, Tone, M/V, MHz, Mute
98
  u8 unk_bit3 : 1,        // 0x1175
99
     vmKeyFunc : 3,       //   A/B, Low, Monitor, Scan, Tone, M/V, MHz, Mute
100
     unk_bit2 : 1,        //
101
     ctKeyFunc : 3;       //   A/B, Low, Monitor,
102
                          //   Scan, Tone, M/V, MHz, Mute
103
  u8 abKeyFunc : 3,       // 0x1176  A/B, Low, Monitor,
104
                          //         Scan, Tone, M/V, MHz, Mute
105
     unk_bit1 : 1,        //
106
     volume : 4;          //         0 to 15
107
  u8 unk3 : 3,            // 0x1177
108
     introScreen : 2,     //      OFF, Picture, Character String
109
     unk_bits : 3;        //
110
  u8 unk4;                // 0x1178
111
  u8 unk5;                // 0x1179
112
  u8 powerOnPw[6];        // 0x117A  6 ASCII characters
113
} basicsettings;
114

    
115
#seekto 0x1180;
116
struct {
117
  u8 bitmap[26];    // one bit for each channel marked in use
118
} chan_avail;
119

    
120
#seekto 0x11A0;
121
struct {
122
  u8 bitmap[26];    // one bit for each channel; 0 = skip; 1 = dont skip
123
} chan_skip;
124

    
125
#seekto 0x1680;
126
ul32 rx_freq_limit_low_vhf;
127
ul32 rx_freq_limit_high_vhf;
128
ul32 tx_freq_limit_low_vhf;
129
ul32 tx_freq_limit_high_vhf;
130
ul32 rx_freq_limit_low_220;   //not supported by radio - always 0xFFFFFFFF
131
ul32 rx_freq_limit_high_220;  //not supported by radio - always 0xFFFFFFFF
132
ul32 tx_freq_limit_low_220;   //not supported by radio - always 0xFFFFFFFF
133
ul32 tx_freq_limit_high_220;  //not supported by radio - always 0xFFFFFFFF
134
ul32 rx_freq_limit_low_uhf;
135
ul32 rx_freq_limit_high_uhf;
136
ul32 tx_freq_limit_low_uhf;
137
ul32 tx_freq_limit_high_uhf;
138

    
139
#seekto 0x1940;
140
struct {
141
  u8 introLine1[16];    // 16 ASCII characters
142
  u8 introLine2[16];    // 16 ASCII characters
143
} intro_lines;
144
"""
145

    
146
MEM_SIZE = 0x2400
147
BLOCK_SIZE = 0x20
148
STIMEOUT = 2
149
BAUDRATE = 9600
150

    
151
# Channel power: 3 levels
152
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=25.00),
153
                chirp_common.PowerLevel("Mid", watts=10.00),
154
                chirp_common.PowerLevel("Low", watts=5.00)]
155
B_LOCK_LIST = ["OFF", "Sub", "Carrier"]
156
OPTSIG_LIST = ["OFF", "DTMF", "2TONE", "5TONE"]
157
PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
158
STEPS = [2.5, 5.0, 6.25, 7.5, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0]
159
LIST_STEPS = [str(x) for x in STEPS]
160
# In the context of SQL_MODES, 'Tone' refers to 2tone, 5tone, or DTMF
161
# signalling while "CT" refers to CTCSS and DTCS
162
SQL_MODES = ["SQ", "CT", "Tone", "CT or Tone", "CT and Tone"]
163
SPECIAL_CHANS = ("L1", "U1",
164
                 "L2", "U2",
165
                 "VFOA_VHF", "VFOA_UHF",
166
                 "VFOB_VHF", "VFOB_UHF")
167

    
168

    
169
def _clean_buffer(radio):
170
    radio.pipe.timeout = 0.005
171
    junk = radio.pipe.read(256)
172
    radio.pipe.timeout = STIMEOUT
173
    if junk:
174
        LOG.debug("Got %i bytes of junk before starting" % len(junk))
175

    
176

    
177
def _rawrecv(radio, amount):
178
    """Raw read from the radio device"""
179
    data = ""
180
    try:
181
        data = radio.pipe.read(amount)
182
    except Exception:
183
        _exit_program_mode(radio)
184
        msg = "Generic error reading data from radio; check your cable."
185
        raise errors.RadioError(msg)
186

    
187
    if len(data) != amount:
188
        _exit_program_mode(radio)
189
        msg = "Error reading from radio: not the amount of data we want."
190
        raise errors.RadioError(msg)
191

    
192
    return data
193

    
194

    
195
def _rawsend(radio, data):
196
    """Raw send to the radio device"""
197
    try:
198
        radio.pipe.write(data)
199
    except Exception:
200
        raise errors.RadioError("Error sending data to radio")
201

    
202

    
203
def _make_read_frame(addr, length):
204
    frame = b"\xFE\xFE\xEE\xEF\xEB"
205
    """Pack the info in the header format"""
206
    frame += bytes(f'{addr:X}'.zfill(4), 'utf-8')
207
    frame += bytes(f'{length:X}'.zfill(2), 'utf-8')
208
    frame += b"\xFD"
209
    # Return the data
210
    return frame
211

    
212

    
213
def _make_write_frame(addr, length, data=""):
214
    frame = b"\xFE\xFE\xEE\xEF\xE4"
215
    """Pack the info in the header format"""
216
    output = struct.pack(">HB", addr, length)
217
    # Add the data if set
218
    if len(data) != 0:
219
        output += data
220
    # Convert to ASCII
221
    converted_data = b''
222
    for b in output:
223
        converted_data += bytes(f'{b:X}'.zfill(2), 'utf-8')
224
    frame += converted_data
225
    """Unlike other TYT models, the frame header is
226
       not included in the checksum calc"""
227
    cs_byte = _calculate_checksum(data)
228
    # convert checksum to ASCII
229
    converted_checksum = bytes(f'{cs_byte[0]:X}'.zfill(2), 'utf-8')
230
    frame += converted_checksum
231
    frame += b"\xFD"
232
    # Return the data
233
    return frame
234

    
235

    
236
def _calculate_checksum(data):
237
    num = 0
238
    for x in range(0, len(data)):
239
        num = (num + data[x]) % 256
240

    
241
    if num == 0:
242
        return bytes([0])
243

    
244
    return bytes([256 - num])
245

    
246

    
247
def _recv(radio, addr, length):
248
    """Get data from the radio """
249

    
250
    data = _rawrecv(radio, length)
251

    
252
    # DEBUG
253
    LOG.info("Response:")
254
    LOG.debug(util.hexprint(data))
255

    
256
    return data
257

    
258

    
259
def _do_ident(radio):
260
    """Put the radio in PROGRAM mode & identify it"""
261
    radio.pipe.baudrate = BAUDRATE
262
    radio.pipe.parity = "N"
263
    radio.pipe.timeout = STIMEOUT
264

    
265
    # Flush input buffer
266
    _clean_buffer(radio)
267

    
268
    # Ident radio
269
    magic = radio._magic0
270
    _rawsend(radio, magic)
271
    ack = _rawrecv(radio, 36)
272
    if not ack.startswith(radio._fingerprint) or not ack.endswith(b"\xFD"):
273
        _exit_program_mode(radio)
274
        if ack:
275
            LOG.debug(repr(ack))
276
        raise errors.RadioError("Radio did not respond as expected (A)")
277

    
278
    return True
279

    
280

    
281
def _exit_program_mode(radio):
282
    # This may be the last part of a read
283
    magic = radio._magic5
284
    _rawsend(radio, magic)
285
    _clean_buffer(radio)
286

    
287

    
288
def _download(radio):
289
    """Get the memory map"""
290

    
291
    # Put radio in program mode and identify it
292
    _do_ident(radio)
293

    
294
    # Enter read mode
295
    magic = radio._magic2
296
    _rawsend(radio, magic)
297
    ack = _rawrecv(radio, 7)
298
    if ack != b"\xFE\xFE\xEF\xEE\xE6\x00\xFD":
299
        _exit_program_mode(radio)
300
        if ack:
301
            LOG.debug(repr(ack))
302
        raise errors.RadioError("Radio did not respond to enter read mode")
303

    
304
    # UI progress
305
    status = chirp_common.Status()
306
    status.cur = 0
307
    status.max = MEM_SIZE // BLOCK_SIZE
308
    status.msg = "Cloning from radio..."
309
    radio.status_fn(status)
310

    
311
    data = b""
312
    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
313
        frame = _make_read_frame(addr, BLOCK_SIZE)
314
        # DEBUG
315
        LOG.debug("Frame=" + util.hexprint(frame))
316

    
317
        # Sending the read request
318
        _rawsend(radio, frame)
319

    
320
        # Now we read data
321
        d = _recv(radio, addr, BLOCK_SIZE*2 + 14)
322

    
323
        LOG.debug("Response Data= " + util.hexprint(d))
324

    
325
        if not d.startswith(b"\xFE\xFE\xEF\xEE\xE4"):
326
            LOG.warning("Incorrect start")
327
        if not d.endswith(b"\xFD"):
328
            LOG.warning("Incorrect end")
329
        # validate the block data with checksum
330
        # HEADER IS NOT INCLUDED IN CHECKSUM CALC, UNLIKE OTHER TYT MODELS
331
        protected_data = d[5:-3]
332
        received_checksum = d[-3:-1]
333
        # unlike some other TYT models, the data protected by checksum
334
        # is sent over the wire in ASCII format.  Need to convert ASCII
335
        # to integers to perform the checksum calculation, which uses
336
        # the same algorithm as other TYT models.
337
        converted_data = b''
338
        for i in range(0, len(protected_data), 2):
339
            int_data = int(protected_data[i:i+2].decode('utf-8'), 16)
340
            converted_data += int_data.to_bytes(1, 'big')
341
        cs_byte = _calculate_checksum(converted_data)
342
        # checksum is sent over the wire as ASCII characters, so convert
343
        # the calculated value before checking against what was received
344
        converted_checksum = bytes(f'{cs_byte[0]:X}'.zfill(2), 'utf-8')
345
        if received_checksum != converted_checksum:
346
            LOG.warning("Incorrect checksum received")
347
        # Strip out header, addr, length, checksum,
348
        # eof and then aggregate the remaining data
349
        ascii_data = d[11:-3]
350
        if len(ascii_data) % 2 != 0:
351
            LOG.error("Invalid data length")
352
        converted_data = b''
353
        for i in range(0, len(ascii_data), 2):
354
            # LOG.debug(ascii_data[i] + ascii_data[i+1])
355
            int_data = int(ascii_data[i:i+2].decode('utf-8'), 16)
356
            converted_data += int_data.to_bytes(1, 'big')
357
        data += converted_data
358

    
359
        # UI Update
360
        status.cur = addr // BLOCK_SIZE
361
        status.msg = "Cloning from radio..."
362
        radio.status_fn(status)
363

    
364
    _exit_program_mode(radio)
365

    
366
    return data
367

    
368

    
369
def _upload(radio):
370
    """Upload procedure"""
371
    # Put radio in program mode and identify it
372
    _do_ident(radio)
373

    
374
    magic = radio._magic3
375
    _rawsend(radio, magic)
376
    ack = _rawrecv(radio, 7)
377
    if ack != b"\xFE\xFE\xEF\xEE\xE6\x00\xFD":
378
        _exit_program_mode(radio)
379
        if ack:
380
            LOG.debug(repr(ack))
381
        raise errors.RadioError("Radio did not respond to enter write mode")
382

    
383
    # UI progress
384
    status = chirp_common.Status()
385
    status.cur = 0
386
    status.max = MEM_SIZE // BLOCK_SIZE
387
    status.msg = "Cloning to radio..."
388
    radio.status_fn(status)
389

    
390
    # The fun starts here
391
    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
392
        # Official programmer skips writing these memory locations
393
        if addr >= 0x1680 and addr < 0x1940:
394
            continue
395

    
396
        # Sending the data
397
        data = radio.get_mmap()[addr:addr + BLOCK_SIZE]
398

    
399
        frame = _make_write_frame(addr, BLOCK_SIZE, data)
400
        LOG.warning("Frame:%s:" % util.hexprint(frame))
401
        _rawsend(radio, frame)
402

    
403
        ack = _rawrecv(radio, 7)
404
        LOG.debug("Response Data= " + util.hexprint(ack))
405

    
406
        if not ack.startswith(b"\xFE\xFE\xEF\xEE\xE6\x00\xFD"):
407
            LOG.warning("Unexpected response")
408
            _exit_program_mode(radio)
409
            msg = "Bad ack writing block 0x%04x" % addr
410
            raise errors.RadioError(msg)
411

    
412
        # UI Update
413
        status.cur = addr // BLOCK_SIZE
414
        status.msg = "Cloning to radio..."
415
        radio.status_fn(status)
416

    
417
    _exit_program_mode(radio)
418

    
419

    
420
def _do_map(chn, sclr, mary):
421
    """Set or Clear the chn (0-199) bit in mary[] word array map"""
422
    # chn is 1-based channel, sclr:1 = set, 0= = clear, 2= return state
423
    # mary[] is u8 array, but the map is by nibbles
424
    ndx = int(math.floor((chn) / 8))
425
    bv = (chn) % 8
426
    msk = 1 << bv
427
    mapbit = sclr
428
    if sclr == 1:    # Set the bit
429
        mary[ndx] = mary[ndx] | msk
430
    elif sclr == 0:  # clear
431
        mary[ndx] = mary[ndx] & (~ msk)     # ~ is complement
432
    else:       # return current bit state
433
        mapbit = 0
434
        if (mary[ndx] & msk) > 0:
435
            mapbit = 1
436
    return mapbit
437

    
438

    
439
@directory.register
440
class TH8600Radio(chirp_common.CloneModeRadio):
441
    """TYT TH8600 Radio"""
442

    
443
    VENDOR = "TYT"
444
    MODEL = "TH-8600"
445
    NEEDS_COMPAT_SERIAL = False
446
    MODES = ['WFM', 'FM', 'NFM']
447
    sql_modeS = ("", "Tone", "TSQL", "DTCS", "Cross")
448
    TONES = chirp_common.TONES
449
    DTCS_CODES = chirp_common.DTCS_CODES
450
    NAME_LENGTH = 6
451
    DTMF_CHARS = list("0123456789ABCD*#")
452
    # 136-174, 400-480
453
    VALID_BANDS = [(136000000, 174000001), (400000000, 480000001)]
454
    # Valid chars on the LCD
455
    VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
456
        "`!\"#$%&'()*+,-./:;<=>?@[]^_"
457

    
458
    _magic0 = b"\xFE\xFE\xEE\xEF\xE0\x26\x98\x00\x00\xFD"
459
    _magic2 = b"\xFE\xFE\xEE\xEF\xE2\x26\x98\x00\x00\xFD"
460
    _magic3 = b"\xFE\xFE\xEE\xEF\xE3\x26\x98\x00\x00\xFD"
461
    _magic5 = b"\xFE\xFE\xEE\xEF\xE5\x26\x98\x00\x00\xFD"
462
    _fingerprint = b"\xFE\xFE\xEF\xEE\xE1\x26\x98\x00\x00\x31\x31\x31\x31" \
463
                   b"\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31" \
464
                   b"\x31\x31\x31\x34\x33\x34\x45"
465

    
466
    @classmethod
467
    def match_model(cls, filedata, filename):
468
        # This radio has always been post-metadata, so never do
469
        # old-school detection
470
        return False
471

    
472
    @classmethod
473
    def get_prompts(cls):
474
        rp = chirp_common.RadioPrompts()
475
        rp.info = \
476
            (cls.VENDOR + ' ' + cls.MODEL + '\n')
477

    
478
        rp.pre_download = _(
479
            "This is an early stage beta driver\n")
480
        rp.pre_upload = _(
481
            "This is an early stage beta driver - upload at your own risk\n")
482
        return rp
483

    
484
    def get_features(self):
485
        rf = chirp_common.RadioFeatures()
486
        rf.has_settings = True
487
        rf.has_bank = False
488
        rf.has_bank_index = False
489
        rf.has_bank_names = False
490
        rf.has_comment = False
491
        rf.has_tuning_step = True
492
        rf.valid_tuning_steps = STEPS
493
        rf.can_odd_split = True
494
        rf.has_name = True
495
        rf.has_offset = True
496
        rf.has_mode = True
497
        rf.has_dtcs = True
498
        rf.has_rx_dtcs = True
499
        rf.has_dtcs_polarity = True
500
        rf.has_ctone = True
501
        rf.has_cross = True
502
        rf.has_sub_devices = False
503
        rf.has_infinite_number = False
504
        rf.has_nostep_tuning = False
505
        rf.has_variable_power = False
506
        rf.valid_name_length = self.NAME_LENGTH
507
        rf.valid_modes = self.MODES
508
        rf.valid_characters = self.VALID_CHARS
509
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
510
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
511
        rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS",
512
                                "Tone->DTCS", "DTCS->Tone", "->Tone",
513
                                "DTCS->DTCS"]
514
        rf.valid_skips = []
515
        rf.valid_power_levels = POWER_LEVELS
516
        rf.valid_dtcs_codes = chirp_common.DTCS_CODES
517
        rf.valid_bands = self.VALID_BANDS
518
        rf.valid_special_chans = SPECIAL_CHANS
519
        rf.memory_bounds = (0, 199)
520
        rf.valid_skips = ["", "S"]
521
        return rf
522

    
523
    def sync_in(self):
524
        """Download from radio"""
525
        try:
526
            data = _download(self)
527
        except errors.RadioError:
528
            # Pass through any real errors we raise
529
            raise
530
        except Exception:
531
            # If anything unexpected happens, make sure we raise
532
            # a RadioError and log the problem
533
            LOG.exception('Unexpected error during download')
534
            raise errors.RadioError('Unexpected error communicating '
535
                                    'with the radio')
536
        self._mmap = memmap.MemoryMapBytes(data)
537
        self.process_mmap()
538

    
539
    def sync_out(self):
540
        """Upload to radio"""
541

    
542
        try:
543
            _upload(self)
544
        except Exception:
545
            # If anything unexpected happens, make sure we raise
546
            # a RadioError and log the problem
547
            LOG.exception('Unexpected error during upload')
548
            raise errors.RadioError('Unexpected error communicating '
549
                                    'with the radio')
550

    
551
    def process_mmap(self):
552
        """Process the mem map into the mem object"""
553
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
554
    '''
555
    def process_mmap(self):
556
        self._memobj = bitwise.parse(
557
            TH8600_MEM_FORMAT %
558
            (self._mmap_offset, self._scanlimits_offset, self._settings_offset,
559
             self._chan_active_offset, self._info_offset), self._mmap)
560
    '''
561
    def get_raw_memory(self, number):
562
        return repr(self._memobj.memory[number])
563

    
564
    def set_memory(self, memory):
565
        """A value in a UI column for chan 'number' has been modified."""
566
        # update all raw channel memory values (_mem) from UI (mem)
567
        if memory.number >= 200 and memory.number < 204:
568
            _mem = self._memobj.scanlimits[memory.number-200]
569
            _name = None
570
        elif memory.number >= 204:
571
            _mem = self._memobj.vfos[memory.number-204]
572
            _name = None
573
        else:
574
            _mem = self._memobj.chan_mem[memory.number]
575
            _name = self._memobj.chan_name[memory.number]
576
            if memory.empty:
577
                _mem.set_raw("\x00" * 21)
578
                _do_map(memory.number, 0, self._memobj.chan_avail.bitmap)
579
                return
580
            else:
581
                _do_map(memory.number, 1, self._memobj.chan_avail.bitmap)
582

    
583
            if memory.skip == "":
584
                _do_map(memory.number, 1, self._memobj.chan_skip.bitmap)
585
            else:
586
                _do_map(memory.number, 0, self._memobj.chan_skip.bitmap)
587

    
588
        return self._set_memory(memory, _mem, _name)
589

    
590
    def get_memory(self, number):
591

    
592
        mem = chirp_common.Memory()
593
        mem.number = number
594
        # Get a low-level memory object mapped to the image
595
        if isinstance(number, str) or number >= 200:
596
            # mem.number = -10 + SPECIAL_CHANS.index(number)
597
            if number == 'L1' or number == 200:
598
                _mem = self._memobj.scanlimits[0]
599
                _name = None
600
                mem.number = 200
601
                mem.extd_number = 'L1'
602
            elif number == 'U1' or number == 201:
603
                _mem = self._memobj.scanlimits[1]
604
                _name = None
605
                mem.number = 201
606
                mem.extd_number = 'U1'
607
            elif number == 'L2' or number == 202:
608
                _mem = self._memobj.scanlimits[2]
609
                _name = None
610
                mem.number = 202
611
                mem.extd_number = 'L2'
612
            elif number == 'U2' or number == 203:
613
                _mem = self._memobj.scanlimits[3]
614
                _name = None
615
                mem.number = 203
616
                mem.extd_number = 'U2'
617
            elif number == 'VFOA_VHF' or number == 204:
618
                _mem = self._memobj.vfos[0]
619
                mem.number = 204
620
                mem.extd_number = 'VFOA_VHF'
621
                _name = None
622
            elif number == 'VFOA_220' or number == 205:
623
                _mem = self._memobj.vfos[1]
624
                _name = None
625
                mem.number = 205
626
            elif number == 'VFOA_UHF' or number == 206:
627
                _mem = self._memobj.vfos[2]
628
                mem.number = 206
629
                mem.extd_number = 'VFOA_UHF'
630
                _name = None
631
            elif number == 'VFOB_VHF' or number == 207:
632
                _mem = self._memobj.vfos[3]
633
                mem.number = 207
634
                mem.extd_number = 'VFOB_VHF'
635
                _name = None
636
                mem.extd_number = 'VFOA_220'
637
            elif number == 'VFOB_220' or number == 208:
638
                _mem = self._memobj.vfos[4]
639
                _name = None
640
                mem.number = 208
641
                mem.extd_number = 'VFOB_220'
642
            elif number == 'VFOB_UHF' or number == 209:
643
                _mem = self._memobj.vfos[5]
644
                mem.number = 209
645
                mem.extd_number = 'VFOB_UHF'
646
                _name = None
647
        else:
648
            mem.number = number  # Set the memory number
649
            _mem = self._memobj.chan_mem[number]
650
            _name = self._memobj.chan_name[number]
651
            # Determine if channel is empty
652

    
653
            if _do_map(mem.number, 2, self._memobj.chan_avail.bitmap) == 0:
654
                mem.empty = True
655
                return mem
656

    
657
            if _do_map(mem.number, 2, self._memobj.chan_skip.bitmap) == 1:
658
                mem.skip = ""
659
            else:
660
                mem.skip = "S"
661

    
662
        return self._get_memory(mem, _mem, _name)
663

    
664
    def _get_memory(self, mem, _mem, _name):
665
        """Convert raw channel memory data into UI columns"""
666
        mem.extra = RadioSettingGroup("Extra", "extra")
667

    
668
        mem.empty = False
669
        # This function process both 'normal' and Freq up/down' entries
670
        mem.freq = int(_mem.rxfreq) * 10
671

    
672
        if _mem.txfreq == 0xFFFFFFFF:
673
            # TX freq not set
674
            mem.duplex = "off"
675
            mem.offset = 0
676
        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 25000000:
677
            mem.duplex = "split"
678
            mem.offset = int(_mem.txfreq) * 10
679
        elif int(_mem.rxfreq) == int(_mem.txfreq):
680
            mem.duplex = ""
681
            mem.offset = 0
682
        else:
683
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) \
684
                and "-" or "+"
685
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
686

    
687
        mem.name = ""
688
        for i in range(6):   # 0 - 6
689
            mem.name += chr(_mem.name[i])
690
        if _name and mem.number < 200:
691
            for i in range(10):
692
                mem.name += chr(_name.extra_name[i])
693

    
694
        mem.name = mem.name.rstrip()    # remove trailing spaces
695

    
696
        mem.tuning_step = STEPS[_mem.tuning_step]
697

    
698
        # ########## TONE ##########
699
        dtcs_polarity = ['N', 'N']
700
        if _mem.txtone == 0xFFF:
701
            # All off
702
            txmode = ""
703
        elif _mem.txtone >= 0x8000:
704
            # DTSC inverted when high bit is set - signed int
705
            txmode = "DTCS"
706
            mem.dtcs = int(format(int(_mem.txtone & 0x7FFF), 'o'))
707
            dtcs_polarity[0] = "R"
708
        elif _mem.txtone > 500:
709
            txmode = "Tone"
710
            mem.rtone = int(_mem.txtone) / 10.0
711
        else:
712
            # DTCS
713
            txmode = "DTCS"
714
            mem.dtcs = int(format(int(_mem.txtone), 'o'))
715
            dtcs_polarity[0] = "N"
716
        if _mem.rxtone == 0xFFF:
717
            rxmode = ""
718
        elif _mem.rxtone >= 0x8000:
719
            # DTSC inverted when high bit is set
720
            rxmode = "DTCS"
721
            mem.rx_dtcs = int(format(int(_mem.rxtone & 0x7FFF), 'o'))
722
            dtcs_polarity[1] = "R"
723
        elif _mem.rxtone > 500:
724
            rxmode = "Tone"
725
            mem.ctone = int(_mem.rxtone) / 10.0
726
        else:
727
            rxmode = "DTCS"
728
            mem.rx_dtcs = int(format(int(_mem.rxtone), 'o'))
729
            dtcs_polarity[1] = "N"
730
        mem.dtcs_polarity = "".join(dtcs_polarity)
731

    
732
        mem.tmode = ""
733
        if txmode == "Tone" and not rxmode:
734
            mem.tmode = "Tone"
735
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
736
            mem.tmode = "TSQL"
737
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
738
            mem.tmode = "DTCS"
739
            mem.rx_dtcs = mem.dtcs
740
            dtcs_polarity[1] = dtcs_polarity[0]
741
        elif rxmode or txmode:
742
            mem.tmode = "Cross"
743
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
744
        # ########## TONE ##########
745

    
746
        mem.mode = self.MODES[_mem.mode]
747
        mem.power = POWER_LEVELS[int(_mem.power)]
748

    
749
        rs = RadioSettingValueList(B_LOCK_LIST,
750
                                   B_LOCK_LIST[min(_mem.b_lock, 0x02)])
751
        b_lock = RadioSetting("b_lock", "B_Lock", rs)
752
        mem.extra.append(b_lock)
753

    
754
        optsig = RadioSetting("signal", "Optional signaling",
755
                              RadioSettingValueList(
756
                                  OPTSIG_LIST,
757
                                  OPTSIG_LIST[_mem.signal]))
758
        mem.extra.append(optsig)
759

    
760
        vlist = RadioSettingValueList(PTTID_LIST, PTTID_LIST[_mem.dtmfpttid])
761
        dtmfpttid = RadioSetting("dtmfpttid", "DTMF PTT ID", vlist)
762
        mem.extra.append(dtmfpttid)
763

    
764
        vlist2 = RadioSettingValueList(PTTID_LIST,
765
                                       PTTID_LIST[_mem.fivetonepttid])
766
        fivetonepttid = RadioSetting("fivetonepttid", "5 Tone PTT ID", vlist2)
767
        mem.extra.append(fivetonepttid)
768

    
769
        return mem
770

    
771
    def _set_memory(self, mem, _mem, _name):
772
        # """Convert UI column data (mem) into MEM_FORMAT memory (_mem)."""
773

    
774
        _mem.rxfreq = mem.freq / 10
775
        _mem.tuning_step = STEPS.index(mem.tuning_step)
776
        if mem.duplex == "off":
777
            _mem.txfreq = 0xFFFFFFFF
778
        elif mem.duplex == "split":
779
            _mem.txfreq = mem.offset / 10
780
        elif mem.duplex == "+":
781
            _mem.txfreq = (mem.freq + mem.offset) / 10
782
        elif mem.duplex == "-":
783
            _mem.txfreq = (mem.freq - mem.offset) / 10
784
        else:
785
            _mem.txfreq = _mem.rxfreq
786

    
787
        out_name = mem.name.ljust(16)
788

    
789
        for i in range(6):   # 0 - 6
790
            _mem.name[i] = ord(out_name[i])
791
        if mem.number < 200:
792
            for i in range(10):
793
                _name.extra_name[i] = ord(out_name[i+6])
794

    
795
        # autoset display to name if filled, else show frequency
796
        if mem.name != "":
797
            _mem.display = True
798
        else:
799
            _mem.display = False
800
        rxmode = ""
801
        txmode = ""
802
        sql_mode = "SQ"
803
        if mem.tmode == "":
804
            sql_mode = "SQ"
805
            _mem.rxtone = 0xFFF
806
            _mem.txtone = 0xFFF
807
        elif mem.tmode == "Tone":
808
            txmode = "Tone"
809
            sql_mode = "SQ"
810
            _mem.txtone = int(float(mem.rtone) * 10)
811
            _mem.rxtone = 0xFFF
812
        elif mem.tmode == "TSQL":
813
            rxmode = "Tone"
814
            txmode = "TSQL"
815
            sql_mode = "CT"
816
            _mem.rxtone = int(float(mem.ctone) * 10)
817
            _mem.txtone = int(float(mem.ctone) * 10)
818
        elif mem.tmode == "DTCS":
819
            rxmode = "DTCS"
820
            txmode = "DTCS"
821
            sql_mode = "CT"
822
            if mem.dtcs_polarity[0] == "N":
823
                _mem.txtone = int(str(mem.dtcs), 8)
824
            else:
825
                _mem.txtone = int(str(mem.dtcs), 8) | 0x8000
826
            if mem.dtcs_polarity[1] == "N":
827
                _mem.rxtone = int(str(mem.dtcs), 8)
828
            else:
829
                _mem.rxtone = int(str(mem.dtcs), 8) | 0x8000
830
        elif mem.tmode == "Cross":
831
            txmode, rxmode = mem.cross_mode.split("->", 1)
832
            if rxmode == "":
833
                _mem.rxtone = 0xFFF
834
                sql_mode = "SQ"
835
            elif rxmode == "Tone":
836
                sql_mode = "CT"
837
                _mem.rxtone = int(float(mem.ctone) * 10)
838
            elif rxmode == "DTCS":
839
                sql_mode = "CT"
840
                if mem.dtcs_polarity[0] == "N":
841
                    _mem.rxtone = int(str(mem.rx_dtcs), 8)
842
                else:
843
                    _mem.rxtone = int(str(mem.rx_dtcs), 8) | 0x8000
844
            if txmode == "":
845
                _mem.txtone = 0xFFF
846
            elif txmode == "Tone":
847
                _mem.txtone = int(float(mem.rtone) * 10)
848
            elif txmode == "TSQL":
849
                _mem.txtone = int(float(mem.rtone) * 10)
850
            elif txmode == "DTCS":
851
                if mem.dtcs_polarity[1] == "N":
852
                    _mem.txtone = int(str(mem.dtcs), 8)
853
                else:
854
                    _mem.txtone = int(str(mem.dtcs), 8) | 0x8000
855
        _mem.sqlmode = SQL_MODES.index(sql_mode)
856
        _mem.mode = self.MODES.index(mem.mode)
857
        _mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
858

    
859
        for element in mem.extra:
860
            setattr(_mem, element.get_name(), element.value)
861

    
862
        return
863

    
864
    def get_settings(self):
865
        """Translate the MEM_FORMAT structs into setstuf in the UI"""
866
        _settings = self._memobj.basicsettings
867
        # _settings2 = self._memobj.settings2
868
        # _workmode = self._memobj.workmodesettings
869

    
870
        basic = RadioSettingGroup("basic", "Basic Settings")
871
        group = RadioSettings(basic)
872

    
873
        # Display Mode
874
        options = ['Frequency', 'Channel #', 'Name']
875
        rx = RadioSettingValueList(options, options[_settings.disMode])
876
        rset = RadioSetting("basicsettings.disMode", "Display Mode", rx)
877
        basic.append(rset)
878

    
879
        # Subscreen Mode
880
        options = ['Off', 'Frequency', 'Voltage']
881
        rx = RadioSettingValueList(options, options[_settings.subDisplay])
882
        rset = RadioSetting("basicsettings.subDisplay", "Subscreen Mode", rx)
883
        basic.append(rset)
884

    
885
        # Squelch Level
886
        options = ["OFF"] + ["%s" % x for x in range(1, 10)]
887
        rx = RadioSettingValueList(options, options[_settings.sqlLevel])
888
        rset = RadioSetting("basicsettings.sqlLevel", "Squelch Level", rx)
889
        basic.append(rset)
890

    
891
        # Tone Burst Frequency
892
        options = ['1750', '2100', '1000', '1450']
893
        rx = RadioSettingValueList(options, options[_settings.burstFreq])
894
        rset = RadioSetting("basicsettings.burstFreq",
895
                            "Tone Burst Frequency (Hz)", rx)
896
        basic.append(rset)
897

    
898
        # PTT Release
899
        options = ['Off', 'Begin', 'End', 'Both']
900
        rx = RadioSettingValueList(options, options[_settings.pttRelease])
901
        rset = RadioSetting("basicsettings.pttRelease", "PTT Release", rx)
902
        basic.append(rset)
903

    
904
        # TX Channel Select
905
        options = ["Last Channel", "Main Channel"]
906
        rx = RadioSettingValueList(options, options[_settings.txChSelect])
907
        rset = RadioSetting("basicsettings.txChSelect",
908
                            "Priority Transmit", rx)
909
        basic.append(rset)
910

    
911
        # LED Mode
912
        options = ["On", "5 Second", "10 Second"]
913
        rx = RadioSettingValueList(options, options[_settings.ledMode])
914
        rset = RadioSetting("basicsettings.ledMode", "LED Display Mode", rx)
915
        basic.append(rset)
916

    
917
        # Scan Type
918
        options = ["TO", "CO", "SE"]
919
        rx = RadioSettingValueList(options, options[_settings.scanType])
920
        rset = RadioSetting("basicsettings.scanType", "Scan Type", rx)
921
        basic.append(rset)
922

    
923
        # Resume Time
924
        options = ["2 seconds", "5 seconds", "10 seconds", "15 seconds"]
925
        rx = RadioSettingValueList(options, options[_settings.scanResumeTime])
926
        rset = RadioSetting("basicsettings.scanResumeTime",
927
                            "Scan Resume Time", rx)
928
        basic.append(rset)
929

    
930
        # Tail Elim
931
        options = ["Off", "Frequency", "No Frequency"]
932
        rx = RadioSettingValueList(options, options[_settings.sqlTailElim])
933
        rset = RadioSetting("basicsettings.sqlTailElim", "Sql Tail Elim", rx)
934
        basic.append(rset)
935

    
936
        # Auto Power Off
937
        options = ["Off", "30 minute", "60 minute", "120 minute"]
938
        rx = RadioSettingValueList(options, options[_settings.autoPowOff])
939
        rset = RadioSetting("basicsettings.autoPowOff", "Auto Power Off", rx)
940
        basic.append(rset)
941

    
942
        # TOT
943
        options = ["Off"] + ["%s minutes" % x for x in range(1, 31, 1)]
944
        rx = RadioSettingValueList(options, options[_settings.tot])
945
        rset = RadioSetting("basicsettings.tot",
946
                            "Transmission Time-out Timer", rx)
947
        basic.append(rset)
948

    
949
        # Beep
950
        rx = RadioSettingValueBoolean(_settings.beep)
951
        rset = RadioSetting("basicsettings.beep", "Keypad Beep", rx)
952
        basic.append(rset)
953

    
954
        # Volume
955
        options = ["%s" % x for x in range(0, 16)]
956
        rx = RadioSettingValueList(options, options[_settings.volume])
957
        rset = RadioSetting("basicsettings.volume",
958
                            "Volume", rx)
959
        basic.append(rset)
960

    
961
        # Require Power On Password
962
        rx = RadioSettingValueBoolean(_settings.usePowerOnPw)
963
        rset = RadioSetting("basicsettings.usePowerOnPw",
964
                            "Require Power On Password", rx)
965
        basic.append(rset)
966

    
967
        # Power On Password Value
968
        pwdigits = ""
969
        for i in range(6):  # 0 - 6
970
            char = chr(_settings.powerOnPw[i])
971
            pwdigits += char
972
        rx = RadioSettingValueString(0, 6, pwdigits)
973
        rset = RadioSetting("basicsettings.powerOnPw", "Power On Password", rx)
974
        basic.append(rset)
975

    
976
        # Intro Screen
977
        '''
978
        #disabling since the memory map for this is still a bit ambigiuous
979
        options = ["Off", "Image", "Character String"]
980
        rx = RadioSettingValueList(options, options[_settings.introScreen])
981
        rset = RadioSetting("basicsettings.introScreen", "Intro Screen", rx)
982
        basic.append(rset)
983
        '''
984

    
985
        key_options = ["A/B", "Low", "Monitor", "Scan",
986
                       "Tone", "M/V", "MHz", "Mute"]
987
        # LO key function
988
        options = key_options
989
        rx = RadioSettingValueList(options, options[_settings.lowKeyFunc])
990
        rset = RadioSetting("basicsettings.lowKeyFunc", "LO Key Function", rx)
991
        basic.append(rset)
992

    
993
        # Mz key function
994
        options = key_options
995
        rx = RadioSettingValueList(options, options[_settings.mzKeyFunc])
996
        rset = RadioSetting("basicsettings.mzKeyFunc", "Mz Key Function", rx)
997
        basic.append(rset)
998

    
999
        # CT key function
1000
        options = key_options
1001
        rx = RadioSettingValueList(options, options[_settings.ctKeyFunc])
1002
        rset = RadioSetting("basicsettings.ctKeyFunc", "CT Key Function", rx)
1003
        basic.append(rset)
1004

    
1005
        # V/M key function
1006
        options = key_options
1007
        rx = RadioSettingValueList(options, options[_settings.vmKeyFunc])
1008
        rset = RadioSetting("basicsettings.vmKeyFunc", "V/M Key Function", rx)
1009
        basic.append(rset)
1010

    
1011
        # A/B key function
1012
        options = key_options
1013
        rx = RadioSettingValueList(options, options[_settings.abKeyFunc])
1014
        rset = RadioSetting("basicsettings.abKeyFunc", "A/B Key Function", rx)
1015
        basic.append(rset)
1016

    
1017
        # Menu Operation
1018
        rx = RadioSettingValueBoolean(_settings.menuOperation)
1019
        rset = RadioSetting("basicsettings.menuOperation",
1020
                            "Menu Operation", rx)
1021
        basic.append(rset)
1022

    
1023
        # SQ tail elim with no tone option
1024
        rx = RadioSettingValueBoolean(_settings.elimTailNoTone)
1025
        rset = RadioSetting("basicsettings.elimTailNoTone",
1026
                            "Eliminate Squelch Tail When No CT/DCS Signalling",
1027
                            rx)
1028
        basic.append(rset)
1029

    
1030
        # Disable Reset Option
1031
        rx = RadioSettingValueBoolean(_settings.disableReset)
1032
        rset = RadioSetting("basicsettings.disableReset", "Disable Reset", rx)
1033
        basic.append(rset)
1034
        '''
1035
        advanced = RadioSettingGroup("advanced", "Advanced Settings")
1036
        group.append(advanced)
1037

    
1038
        # software only
1039
        options = ['Off', 'Frequency']
1040
        rx = RadioSettingValueList(options, options[_settings.endToneElim])
1041
        rset = RadioSetting("basicsettings.endToneElim", "End Tone Elim", rx)
1042
        advanced.append(rset)
1043

    
1044
        # software only
1045
        name = ""
1046
        for i in range(15):  # 0 - 15
1047
            char = chr(int(self._memobj.openradioname.name1[i]))
1048
            if char == "\x00":
1049
                char = " "  # Other software may have 0x00 mid-name
1050
            name += char
1051
        name = name.rstrip()  # remove trailing spaces
1052

    
1053
        rx = RadioSettingValueString(0, 15, name)
1054
        rset = RadioSetting("openradioname.name1", "Intro Line 1", rx)
1055
        advanced.append(rset)
1056

    
1057
        # software only
1058
        name = ""
1059
        for i in range(15):  # 0 - 15
1060
            char = chr(int(self._memobj.openradioname.name2[i]))
1061
            if char == "\x00":
1062
                char = " "  # Other software may have 0x00 mid-name
1063
            name += char
1064
        name = name.rstrip()  # remove trailing spaces
1065

    
1066
        rx = RadioSettingValueString(0, 15, name)
1067
        rset = RadioSetting("openradioname.name2", "Intro Line 2", rx)
1068
        advanced.append(rset)
1069
        '''
1070

    
1071
        def myset_mask(setting, obj, atrb, nx):
1072
            if bool(setting.value):     # Enabled = 1
1073
                vx = 1
1074
            else:
1075
                vx = 0
1076
            _do_map(nx + 1, vx, self._memobj.fmmap.fmset)
1077
            return
1078

    
1079
        def myset_freq(setting, obj, atrb, mult):
1080
            """ Callback to set frequency by applying multiplier"""
1081
            value = int(float(str(setting.value)) * mult)
1082
            setattr(obj, atrb, value)
1083
            return
1084

    
1085
        return group       # END get_settings()
1086

    
1087
    def set_settings(self, settings):
1088
        _settings = self._memobj.basicsettings
1089
        # _mem = self._memobj
1090
        for element in settings:
1091
            if not isinstance(element, RadioSetting):
1092
                self.set_settings(element)
1093
                continue
1094
            else:
1095
                try:
1096
                    name = element.get_name()
1097
                    if "." in name:
1098
                        bits = name.split(".")
1099
                        obj = self._memobj
1100
                        for bit in bits[:-1]:
1101
                            if "/" in bit:
1102
                                bit, index = bit.split("/", 1)
1103
                                index = int(index)
1104
                                obj = getattr(obj, bit)[index]
1105
                            else:
1106
                                obj = getattr(obj, bit)
1107
                        setting = bits[-1]
1108
                    else:
1109
                        obj = _settings
1110
                        setting = element.get_name()
1111
                    if element.has_apply_callback():
1112
                        LOG.debug("Using apply callback")
1113
                        element.run_apply_callback()
1114
                    elif setting == "powerOnPw":
1115
                        temp = []
1116
                        for c in element.value:
1117
                            temp.append(ord(c))
1118
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1119
                        setattr(obj, setting, temp)
1120
                    elif element.value.get_mutable():
1121
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1122
                        setattr(obj, setting, element.value)
1123
                except Exception:
1124
                    LOG.debug(element.get_name())
1125
                    raise
(8-8/13)