Project

General

Profile

New Model #4851 » th8600.py

Andy Knitt, 08/04/2023 06:04 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:-4]
332
        received_checksum = d[-4:-2]
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 (1-200) 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 - 1) / 8))
425
    bv = (chn - 1) % 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
                _do_map(memory.number, 0, self._memobj.chan_avail.bitmap)
578
                return
579
            else:
580
                _do_map(memory.number, 1, self._memobj.chan_avail.bitmap)
581

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

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

    
589
    def get_memory(self, number):
590

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
768
        return mem
769

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

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

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

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

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

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

    
861
        return
862

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1084
        return group       # END get_settings()
1085

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