Project

General

Profile

New Model #10478 » uvk5.py

chirp driver for UV-K5, version 20230521 - Jacek Lipkowski SQ5BPF, 05/21/2023 10:11 AM

 
1
# Quansheng UV-K5 driver (c) 2023 Jacek Lipkowski <sq5bpf@lipkowski.org>
2
#
3
# based on template.py Copyright 2012 Dan Smith <dsmith@danplanet.com>
4
#
5
#
6
# This is a preliminary version of a driver for the UV-K5
7
# It is based on my reverse engineering effort described here:
8
# https://github.com/sq5bpf/uvk5-reverse-engineering
9
#
10
# Warning: this driver is experimental, it may brick your radio,
11
# eat your lunch and mess up your configuration. Before even attempting
12
# to use it save a memory image from the radio using k5prog:
13
# https://github.com/sq5bpf/k5prog
14
#
15
#
16
# This program is free software: you can redistribute it and/or modify
17
# it under the terms of the GNU General Public License as published by
18
# the Free Software Foundation, either version 2 of the License, or
19
# (at your option) any later version.
20
#
21
# This program is distributed in the hope that it will be useful,
22
# but WITHOUT ANY WARRANTY; without even the implied warranty of
23
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
# GNU General Public License for more details.
25
#
26
# You should have received a copy of the GNU General Public License
27
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
28

    
29

    
30
import struct
31
import logging
32
import serial  
33
import logging
34
from chirp import chirp_common, directory, bitwise, memmap, errors, util
35
from chirp.settings import RadioSetting, RadioSettingGroup, \
36
        RadioSettingValueBoolean, RadioSettingValueList, \
37
        RadioSettingValueInteger, RadioSettingValueString, \
38
        RadioSettingValueFloat, RadioSettingValueMap, RadioSettings
39

    
40
LOG = logging.getLogger(__name__)
41

    
42
DRIVER_VERSION="Quansheng UV-K5 driver v20230512 (c) Jacek Lipkowski SQ5BPF"
43
PRINT_CONSOLE=False
44

    
45
MEM_FORMAT = """
46
#seekto 0x0000;
47
struct {
48
  ul32 freq;
49
  ul32 offset;
50
  u8 rxcode;
51
  u8 txcode;
52
  u8 code_flag;
53
  u8 flags1;
54
  u8 flags2;
55
  u8 dtmf_flags;
56
  u8 step;
57
  u8 scrambler;
58
} channel[214];
59

    
60
#seekto 0x640;
61
ul16 fmfreq[20];
62

    
63
#seekto 0xe70;
64
u8 call_channel;
65
u8 squelch;
66
u8 max_talk_time;
67
u8 noaa_autoscan;
68
u8 unknown1;
69
u8 unknown2;
70
u8 vox_level;
71
u8 mic_gain;
72
u8 unknown3;
73
u8 channel_display_mode;
74
u8 crossband;
75
u8 battery_save;
76
u8 dual_watch;
77
u8 tail_note_elimination;
78
u8 vfo_open;
79

    
80
#seekto 0xe90;
81
u8 beep_control;
82
#seekto 0xe95;
83
u8 scan_resume_mode;
84
u8 auto_keypad_lock;
85
u8 power_on_dispmode;
86
u8 password[4];
87

    
88
#seekto 0xea0;
89
u8 keypad_tone;
90
u8 language;
91

    
92
#seekto 0xea8;
93
u8 alarm_mode;
94
u8 reminding_of_end_talk;
95
u8 repeater_tail_elimination;
96

    
97
#seekto 0xeb0;
98
char logo_line1[16];
99
char logo_line2[16];
100

    
101
#seekto 0xf40;
102
u8 int_flock;
103
u8 int_350tx;
104
u8 int_unknown1;
105
u8 int_200tx;
106
u8 int_500tx;
107
u8 int_350en;
108
u8 int_screen;
109

    
110
#seekto 0xf50;
111
struct {
112
char name[16];
113
} channelname[200];
114

    
115
"""
116
#flags1
117
FLAGS1_OFFSET=0b1
118
FLAGS1_ISSCANLIST=0b100
119
FLAGS1_ISAM=0b10000
120

    
121
#flags2
122
FLAGS2_BCLO=0b10000
123
FLAGS2_POWER_MASK=0b1100
124
FLAGS2_POWER_HIGH=0b1000
125
FLAGS2_POWER_MEDIUM=0b0100
126
FLAGS2_POWER_LOW=0b0000
127
FLAGS2_BANDWIDTH=0b10
128
FLAGS2_REVERSE=0b1
129

    
130
#dtmf_flags
131
PTTID_LIST = ["off", "BOT", "EOT", "BOTH"]
132
FLAGS_DTMF_PTTID_MASK=0b110 # PTTID: 00 - disabled, 01 - BOT, 10 - EOT, 11 - BOTH
133
FLAGS_DTMF_PTTID_DISABLED=0b000
134
FLAGS_DTMF_PTTID_BOT=0b010
135
FLAGS_DTMF_PTTID_EOT=0b100
136
FLAGS_DTMF_PTTID_BOTH=0b110
137
FLAGS_DTMF_DECODE=0b1
138

    
139
#scrambler
140
SCRAMBLER_LIST = [ "off", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ]
141

    
142
#channel display mode
143
CHANNELDISP_LIST = [ "Frequency", "Channel No", "Channel Name" ]
144
#battery save
145
BATSAVE_LIST = [ "OFF", "1:1", "1:2", "1:3", "1:4" ]
146

    
147
#Crossband receiving/transmitting
148
CROSSBAND_LIST = [ "Off", "Band A", "Band B" ]
149
DUALWATCH_LIST= CROSSBAND_LIST
150

    
151
#steps
152
STEPS = [ 2.5, 5.0, 6.25, 10.0, 12.5, 25.0 , 8.33 ]
153

    
154
#ctcss/dcs codes
155
TMODES = ["", "Tone", "DTCS", "DTCS"]
156
TONE_NONE=0
157
TONE_CTCSS=1
158
TONE_DCS=2
159
TONE_RDCS=3
160

    
161
#DTCS_CODES = sorted(chirp_common.DTCS_CODES)
162
CTCSS_TONES = [
163
        67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4,
164
        88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9,
165
        114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2,
166
        151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8,
167
        177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5,
168
        203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8,
169
        250.3, 254.1
170
        ]
171

    
172
#       from ft4.py
173
DTCS_CODES = [ 
174
              23,  25,  26,  31,  32,  36,  43,  47,  51,  53,  54,
175
              65,  71,  72,  73,  74,  114, 115, 116, 122, 125, 131,
176
              132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174,
177
              205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252,
178
              255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325,
179
              331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412,
180
              413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464,
181
              465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606,
182
              612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723,
183
              731, 732, 734, 743, 754
184
              ]
185

    
186
FLOCK_LIST=[ "Off", "FCC", "CE", "GB", "430", "438" ]
187
SCANRESUME_LIST = [ "TO: Resume after 5 seconds", "CO: Resume after signal dissapears", "SE: Stop scanning after receiving a signal" ]
188
WELCOME_LIST = [ "Full Screen", "Welcome Info", "Voltage" ]
189
KEYPADTONE_LIST = [ "Off", "Chinese", "English" ]
190
LANGUAGE_LIST = [ "Chinese", "English" ]
191
ALARMMODE_LIST = [ "SITE", "TONE" ]
192
REMENDOFTALK_LIST = [ "Off" , "ROGER", "MDC" ]
193
RTE_LIST = [ "Off" , "100ms", "200ms", "300ms", "400ms", "500ms", "600ms", "700ms", "800ms", "900ms" ]
194

    
195
MEM_SIZE=0x2000 #size of all memory
196
PROG_SIZE=0x1d00 #size of the memowy that we will program
197
MEM_BLOCK=0x80 #largest block of memory that we can reliably write
198

    
199
# the communication is obfuscated using this fine mechanism
200
def xorarr(data: bytes):
201
    tbl=[ 22, 108, 20, 230, 46, 145, 13, 64, 33, 53, 213, 64, 19, 3, 233, 128]
202
    #x=[]
203
    x=b""
204
    r=0
205
    for byte in data:
206
        x+=bytes([byte^tbl[r]])
207
        r=(r+1)%len(tbl)
208
    return x
209

    
210
# if this crc was used for communication to AND from the radio, then it
211
# would be a measure to increase reliability.
212
# but it's only used towards the radio, so it's for further obfuscation
213
def calculate_crc16_xmodem(data: bytes):
214
    poly=0x1021
215
    crc = 0x0
216
    for byte in data:
217
        crc = crc ^ (byte << 8)
218
        for i in range(8):
219
            crc = crc << 1
220
            if (crc & 0x10000):
221
                crc = (crc ^ poly ) & 0xFFFF
222
    return crc & 0xFFFF
223

    
224

    
225
def _send_command(radio,data: bytes):
226
    """Send a command to UV-K5 radio"""
227
    serial=radio.pipe
228
    serial.timeout=0.5
229

    
230
    crc=calculate_crc16_xmodem(data)
231
    data2=data+bytes([crc&0xff,(crc>>8)&0xff])
232

    
233
    command=b"\xAB\xCD"+bytes([len(data)])+b"\x00"+xorarr(data2)+b"\xDC\xBA"
234
#    print("Sending command: %s" % util.hexprint(command))
235
    serial.write(command)
236

    
237
def _receive_reply(radio):
238
    serial=radio.pipe
239
    serial.timeout=0.5
240
    headed: bytes
241
    cmd: bytes
242
    footer: bytes
243

    
244
    header=serial.read(4)
245
    if header[0]!=0xAB or header[1]!=0xCD or header[3]!=0x00:
246
        if PRINT_CONSOLE:
247
            print("bad response header")
248
        raise errors.RadioError( "Bad response header")
249
 
250
        return False
251

    
252
    cmd=serial.read(int(header[2]))
253
    footer=serial.read(4)
254

    
255
    if footer[2]!=0xDC or footer[3]!=0xBA:
256
        if PRINT_CONSOLE:
257
            print("bad response footer")
258
        raise errors.RadioError( "Bad response footer")
259

    
260
        return False
261

    
262
    cmd2=xorarr(cmd)
263
    
264
    if PRINT_CONSOLE:
265
        print("Received reply: %s" % util.hexprint(cmd2))
266

    
267
    return cmd2
268

    
269

    
270
def _getstring(data: bytes, begin, maxlen):
271
    s=""
272
    c=0
273
    for i in data:
274
        c+=1
275
        if c<begin:
276
            continue
277
        if i<ord(' ') or i>ord('~'):
278
            break
279
        s+=chr(i)
280
    return s
281

    
282

    
283
def _sayhello(radio):
284
    hellopacket=b"\x14\x05\x04\x00\x6a\x39\x57\x64"
285

    
286
    tries=5
287
    while (True):
288
        _send_command(radio,hellopacket)
289
        o=_receive_reply(radio)
290
        #print("hello",o)
291
        if (o) :
292
            break
293
        tries-=1
294
        if tries==0:
295
            print("Failed to initialize radio")
296
            return False
297
    firmware=_getstring(o,5,16)
298
    if PRINT_CONSOLE:
299
        print("found firmware",firmware)
300
    radio.FIRMWARE_VERSION=firmware
301
    return True
302

    
303
def _readmem(radio,offset,len):
304
    readmem=b"\x1b\x05\x08\x00"+bytes([offset&0xff,(offset>>8)&0xff,len,0])+b"\x6a\x39\x57\x64"
305
    _send_command(radio,readmem)
306
    o=_receive_reply(radio)
307
    if PRINT_CONSOLE:
308
        print("readmem Received data: %s" % util.hexprint(o))
309
    return o[8:]
310

    
311

    
312
def _writemem(radio,data,offset):
313
    dlen=len(data)
314
    writemem=b"\x1d\x05"+bytes([dlen+8])+b"\x00"+bytes([offset&0xff,(offset>>8)&0xff,dlen,1])+b"\x6a\x39\x57\x64"+data
315
    _send_command(radio,writemem)
316
    o=_receive_reply(radio)
317
    if PRINT_CONSOLE:
318
        print("writemem Received data: %s" % util.hexprint(o))
319
    if o[0]==0x1e and o[1]==(offset&0xff) and o[2]==(offset>>8)&0xff:
320
        return True
321
    return False
322

    
323

    
324
def _resetradio(radio):
325
    resetpacket=b"\xdd\x05\x00\x00"
326
    _send_command(radio,resetpacket)
327

    
328
def warnfirmware(radio):
329
    if radio.FIRMWARE_VERSION!=radio.FIRMWARE_VERSION_PREV:
330
        raise errors.RadioError(
331
                "This is NOT an error.\n"
332
                "Found firmware "+radio.FIRMWARE_VERSION+"\n")
333
    radio.FIRMWARE_VERSION_PREV=radio.FIRMWARE_VERSION
334

    
335

    
336

    
337
def do_download(radio):
338
    """This is your download function"""
339
    status = chirp_common.Status()
340
    status.cur = 0
341
    status.max = MEM_SIZE
342
    status.msg = "Downloading from radio"
343
    radio.status_fn(status)
344

    
345
    eeprom=b""
346
    _sayhello(radio)
347

    
348
    addr=0
349
    while addr<MEM_SIZE:
350
        o=_readmem(radio,addr,MEM_BLOCK)
351
        status.cur=addr
352
        radio.status_fn(status)
353

    
354
        if o and len(o)==MEM_BLOCK:
355
            eeprom+=o
356
            addr+=MEM_BLOCK
357
        else:
358
            return False
359
    #warnfirmware()
360
    return memmap.MemoryMapBytes(eeprom)
361

    
362

    
363

    
364
def do_upload(radio):
365
    """This is your upload function"""
366
    # NOTE: Remove this in your real implementation!
367
    #raise Exception("This template driver does not really work!")
368

    
369
    serial = radio.pipe
370

    
371
    status = chirp_common.Status()
372
    status.cur = 0
373
    status.max = PROG_SIZE
374
    status.msg = "Uploading to radio"
375
    radio.status_fn(status)
376

    
377
    _sayhello(radio)
378

    
379
    addr=0
380
    while addr<PROG_SIZE:
381
        o = radio.get_mmap()[addr:addr+MEM_BLOCK]
382
        _writemem(radio,o,addr)
383
        status.cur=addr
384
        radio.status_fn(status)
385
        if o:
386
            addr+=MEM_BLOCK
387
        else:
388
            return False
389
    status.msg = "Uploaded OK"
390

    
391
    _resetradio(radio)
392
    #warnfirmware()
393

    
394
    return True
395

    
396

    
397
# Uncomment this to actually register this radio in CHIRP
398
@directory.register
399
class TemplateRadio(chirp_common.CloneModeRadio):
400
    """Quansheng UV-K5"""
401
    VENDOR = "Quansheng"     # Replace this with your vendor
402
    MODEL = "UV-K5"  # Replace this with your model
403
    BAUD_RATE = 38400    # Replace this with your baud rate
404
    #DTCS_CODES = sorted(chirp_common.DTCS_CODES)
405

    
406
    # All new drivers should be "Byte Clean" so leave this in place.
407
    NEEDS_COMPAT_SERIAL = False
408
    FIRMWARE_VERSION_PREV=""
409
    FIRMWARE_VERSION=""
410

    
411
    def get_prompts(x=None):
412
        rp = chirp_common.RadioPrompts()
413
        rp.experimental = \
414
            ('This is an experimental driver for the Quanscheng UV-K5. '
415
             'It may harm your radio, or worse. Use at your own risk.\n\n'
416
             'Before attempting to do any changes please download'
417
             'the memory image from the radio with chirp or k5prog '
418
             'and keep it. This can be later used to recover the '
419
             'original settings. \n\n'
420
             'FM radio, DTMF settings and scanlists are not yet implemented' )
421
        rp.pre_download = _(
422
            "1. Turn radio on.\n"
423
            "2. Connect cable to mic/spkr connector.\n"
424
            "3. Make sure connector is firmly connected.\n"
425
            "4. Click OK to download image from device.\n\n"
426
            "It will may not work if you turn o the radio "
427
            "with the cable already attached\n")
428
        rp.pre_upload = _(
429
            "1. Turn radio on.\n"
430
            "2. Connect cable to mic/spkr connector.\n"
431
            "3. Make sure connector is firmly connected.\n"
432
            "4. Click OK to upload the image to device.\n\n"
433
            "It will may not work if you turn o the radio "
434
            "with the cable already attached")
435
        return rp
436

    
437
    # Return information about this radio's features, including
438
    # how many memories it has, what bands it supports, etc
439
    def get_features(self):
440
        rf = chirp_common.RadioFeatures()
441
        rf.has_bank = False
442
        rf.valid_dtcs_codes = DTCS_CODES
443
        #rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES
444
        rf.has_rx_dtcs = True
445
        rf.has_ctone = True
446
        rf.has_settings = True
447
        rf.has_comment = True
448
        rf.valid_name_length = 16
449
        rf.valid_power_levels = [ "High", "Med", "Low" ]
450
        rf.valid_tuning_steps = [ 0.01, 0.1, 1.0 ] + STEPS #hack so we can input any frequency, the 0.1 and 0.01 steps don't work unfortunately
451

    
452
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
453
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
454
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
455

    
456
        rf.valid_characters = chirp_common.CHARSET_ASCII
457
        rf.valid_modes = ["FM", "NFM", "AM" ]
458
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "TSQL-R", "Cross"]
459
        rf.memory_bounds = (1, 214)  # This radio supports memories 0-9
460
#        rf.valid_bands = [(144000000, 148000000),  # Supports 2-meters
461
#                          (440000000, 450000000),  # Supports 70-centimeters
462
#                          ]
463
        rf.valid_bands = [(18000000,  620000000),  # For now everything that the BK4819 chip supports
464
                          (840000000, 1300000000)
465
                          ]
466
        return rf
467

    
468
    # Do a download of the radio from the serial port
469
    def sync_in(self):
470
        self._mmap = do_download(self)
471
        self.process_mmap()
472

    
473
    # Do an upload of the radio to the serial port
474
    def sync_out(self):
475
        do_upload(self)
476

    
477
    # Convert the raw byte array into a memory object structure
478
    def process_mmap(self):
479
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
480

    
481
    # Return a raw representation of the memory object, which
482
    # is very helpful for development
483
    def get_raw_memory(self, number):
484
        return repr(self._memobj.channel[number-1])
485

    
486
    def validate_memory(self, mem):
487
        msgs = super().validate_memory(mem)
488
        #print("validate_memory mem:",mem.number," name:",mem.name," msg:",msgs)
489
        return msgs
490

    
491

    
492

    
493
    def _set_tone(self, mem, _mem):
494
        ((txmode, txtone, txpol),
495
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
496

    
497
        if txmode == "Tone":
498
            txtoval = CTCSS_TONES.index(txtone)
499
            txmoval = 0b01
500
        elif txmode == "DTCS":
501
            txmoval = txpol == "R" and 0b11 or 0b10
502
            txtoval = DTCS_CODES.index(txtone)
503
        else:
504
            txmoval=0
505
            txtoval = 0
506

    
507
        if rxmode == "Tone":
508
            rxtoval = CTCSS_TONES.index(rxtone)
509
            rxmoval = 0b01
510
        elif rxmode == "DTCS":
511
            rxmoval = rxpol == "R" and 0b11 or 0b10
512
            rxtoval = DTCS_CODES.index(rxtone)
513
        else:
514
            rxmoval=0
515
            rxtoval = 0
516

    
517
        _mem.code_flag = ( _mem.code_flag & 0b11001100 ) | (txmoval << 4) | rxmoval
518
        #print("set_tone code_flag:"+hex(_mem.code_flag),"rxcode=",rxtoval,"txcode=",txtoval)
519
        _mem.rxcode=rxtoval
520
        _mem.txcode=txtoval
521

    
522

    
523
    def _get_tone(self, mem, _mem):
524
        rxtype=_mem.code_flag&0x03
525
        txtype=(_mem.code_flag>>4)&0x03
526
        rx_tmode = TMODES[rxtype]
527
        tx_tmode = TMODES[txtype]
528

    
529
        rx_tone = tx_tone = None
530

    
531
        if tx_tmode == "Tone":
532
            if _mem.txcode<len(CTCSS_TONES):
533
                tx_tone = CTCSS_TONES[_mem.txcode]
534
            else:
535
                tx_tone=0
536
                tx_tmode = ""
537
        elif tx_tmode == "DTCS":
538
            if _mem.txcode<len(DTCS_CODES):
539
                tx_tone = DTCS_CODES[_mem.txcode]
540
            else:
541
                tx_tone = 0
542
                tx_tmode = ""
543

    
544
        if rx_tmode == "Tone":
545
            if _mem.rxcode<len(CTCSS_TONES):
546
                rx_tone = CTCSS_TONES[_mem.rxcode]
547
            else:
548
                rx_tone=0
549
                rx_tmode = ""
550
        elif rx_tmode == "DTCS":
551
            if _mem.rxcode<len(DTCS_CODES):
552
                rx_tone = DTCS_CODES[_mem.rxcode]
553
            else:
554
                rx_tone = 0
555
                rx_tmode = ""
556

    
557

    
558
        tx_pol = txtype == 0x03 and "R" or "N"
559
        rx_pol = rxtype == 0x03 and "R" or "N"
560

    
561
        chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
562
                                       (rx_tmode, rx_tone, rx_pol))
563

    
564

    
565

    
566
    # Extract a high-level memory object from the low-level memory map
567
    # This is called to populate a memory in the UI
568
    def get_memory(self, number2):
569
        number=number2-1
570

    
571
        # Get a low-level memory object mapped to the image
572
        _mem = self._memobj.channel[number]
573

    
574

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

    
578
        # Ugly hack: the extra parameters will be shown in the comments field, 
579
        # because there is no other way to show them without the user 
580
        # right-clicking every channel
581
        # Unfortunately this means that this field is read-only
582
        mem.comment="" 
583

    
584
        mem.number = number2                 # Set the memory number
585

    
586
        if number>199:
587
            mem.name="VFO_"+str(number-199)
588
        else:
589
            _mem2 = self._memobj.channelname[number]
590
            for char in _mem2.name:
591
                if str(char) == "\xFF" or str(char) == "\x00":
592
                    break
593
                mem.name += str(char)
594
            mem.name = mem.name.rstrip()
595

    
596
#            mem.name = str(_mem2.name).rstrip(chr(0xff)).rstrip()  # Set the alpha tag
597

    
598

    
599
        # Convert your low-level frequency to Hertz
600
        mem.freq = int(_mem.freq)*10
601
        mem.offset = int(_mem.offset)*10
602

    
603

    
604
        if (mem.offset==0):
605
            mem.duplex = ''
606
        else:
607
            if (_mem.flags1&FLAGS1_OFFSET):
608
                mem.duplex = '-'
609
            else:
610
                mem.duplex = '+'
611

    
612
        # tone data 
613
        self._get_tone(mem, _mem)
614

    
615
        # mode
616
        if (_mem.flags1 & FLAGS1_ISAM )>0 :
617
            mem.mode="AM"
618
        else:
619
            if (_mem.flags2 & FLAGS2_BANDWIDTH )>0:
620
                mem.mode="NFM"
621
            else:
622
                mem.mode="FM"
623

    
624
        # tuning step
625
        tstep=(_mem.step>>1)&0x7
626
        if tstep < len(STEPS):
627
            mem.tuning_step = STEPS[tstep]
628
        else:
629
            mem.tuning_step = 2.5
630

    
631

    
632
        # power
633
        if (_mem.flags2 & FLAGS2_POWER_MASK ) == FLAGS2_POWER_HIGH:
634
            mem.power="High"
635
        elif (_mem.flags2 & FLAGS2_POWER_MASK ) == FLAGS2_POWER_MEDIUM:
636
            mem.power="Med"
637
        else:
638
            mem.power="Low"
639

    
640
        # We'll consider any blank (i.e. 0MHz frequency) to be empty
641
        if (_mem.freq == 0xffffffff) or (_mem.freq == 0):
642
            mem.empty = True
643
        else:
644
            mem.empty = False
645

    
646
        mem.extra = RadioSettingGroup("Extra", "extra")
647
# BCLO
648
        is_bclo=bool(_mem.flags2&FLAGS2_BCLO>0);
649
        rs = RadioSetting("bclo", "BCLO", RadioSettingValueBoolean(is_bclo))
650
        mem.extra.append(rs)
651
        mem.comment+="BCLO:"+(is_bclo and "ON" or "off")+" "
652

    
653
# Frequency reverse
654
        is_frev=bool(_mem.flags2&FLAGS2_REVERSE>0);
655
        rs = RadioSetting("frev", "FreqRev", RadioSettingValueBoolean(is_frev))
656
        mem.extra.append(rs)
657
        mem.comment+="FreqReverse:"+(is_frev and "ON" or "off")+" "
658
#
659
# PTTID
660
        pttid=(_mem.dtmf_flags&FLAGS_DTMF_PTTID_MASK)>>1
661
        rs = RadioSetting("pttid", "PTTID", RadioSettingValueList(PTTID_LIST,PTTID_LIST[pttid]))
662
        mem.extra.append(rs)
663
        mem.comment+="PTTid:"+PTTID_LIST[pttid]+" "
664
# DTMF DECODE
665
        is_dtmf=bool(_mem.dtmf_flags&FLAGS_DTMF_DECODE>0);
666
        rs = RadioSetting("dtmfdecode", "DTMF decode", RadioSettingValueBoolean(is_dtmf))
667
        mem.extra.append(rs)
668
        mem.comment+="DTMFdecode:"+(is_dtmf and "ON" or "off")+" "
669
#
670
# Scrambler
671
        if _mem.scrambler&0x0f<len(SCRAMBLER_LIST):
672
            enc=_mem.scrambler&0x0f
673
        else:
674
            enc=0
675

    
676
        rs = RadioSetting("scrambler", "Scrambler", RadioSettingValueList(SCRAMBLER_LIST,SCRAMBLER_LIST[enc]))
677
        mem.extra.append(rs)
678
        mem.comment+="Scrambler:"+SCRAMBLER_LIST[enc]+" "
679

    
680
        return mem
681

    
682
    def set_settings(self, settings):
683
        _mem = self._memobj
684
        for element in settings:
685
            if not isinstance(element, RadioSetting):
686
                self.set_settings(element)
687
                continue
688
            #print("set_settings element",element.get_name()," value",element.value)
689

    
690
            # call channel
691
            if element.get_name()=="call_channel":
692
                _mem.call_channel=int(element.value)-1
693

    
694
            #squelch
695
            if element.get_name()=="squelch":
696
                _mem.squelch=int(element.value)
697
            #TOT
698
            if element.get_name()=="tot":
699
                _mem.max_talk_time=int(element.value)
700
            #NOAA autoscan
701
            if element.get_name()=="noaa_autoscan":
702
                _mem.noaa_autoscan=element.value and 1 or 0
703

    
704
            #vox level
705
            if element.get_name()=="vox_level":
706
                _mem.vox_level=int(element.value)-1
707

    
708
            #mic gain
709
            if element.get_name()=="mic_gain":
710
                _mem.mic_gain=int(element.value)
711

    
712
            #Channel display mode
713
            if element.get_name()=="channel_display_mode":
714
                _mem.channel_display_mode=CHANNELDISP_LIST.index(str(element.value))
715

    
716
            #Crossband receiving/transmitting
717
            if element.get_name()=="crossband":
718
                _mem.crossband=CROSSBAND_LIST.index(str(element.value))
719

    
720
            #Battery Save
721
            if element.get_name()=="battery_save":
722
                _mem.battery_save=BATSAVE_LIST.index(str(element.value))
723
            #Dual Watch
724
            if element.get_name()=="dualwatch":
725
                _mem.dual_watch=DUALWATCH_LIST.index(str(element.value))
726

    
727
            #Tail tone elimination
728
            if element.get_name()=="tail_note_elimination":
729
                _mem.tail_note_elimination=element.value and 1 or 0
730

    
731
            #VFO Open
732
            if element.get_name()=="vfo_open":
733
                _mem.vfo_open=element.value and 1 or 0
734

    
735
            #Beep control
736
            if element.get_name()=="beep_control":
737
                _mem.beep_control=element.value and 1 or 0
738

    
739
            #Scan resume mode
740
            if element.get_name()=="scan_resume_mode":
741
                _mem.scan_resume_mode=SCANRESUME_LIST.index(str(element.value))
742

    
743
            #Auto keypad lock
744
            if element.get_name()=="auto_keypad_lock":
745
                _mem.auto_keypad_lock=element.value and 1 or 0
746

    
747
            #Power on display mode
748
            if element.get_name()=="welcome_mode":
749
                _mem.power_on_dispmode=WELCOME_LIST.index(str(element.value))
750

    
751
            #Keypad Tone
752
            if element.get_name()=="keypad_tone":
753
                _mem.keypad_tone=KEYPADTONE_LIST.index(str(element.value))
754

    
755
            #Language
756
            if element.get_name()=="language":
757
                _mem.language=LANGUAGE_LIST.index(str(element.value))
758

    
759
            #Alarm mode
760
            if element.get_name()=="alarm_mode":
761
                _mem.alarm_mode=ALARMMODE_LIST.index(str(element.value))
762

    
763
            #Reminding of end of talk
764
            if element.get_name()=="reminding_of_end_talk":
765
                _mem.reminding_of_end_talk=REMENDOFTALK_LIST.index(str(element.value))
766

    
767
            #Repeater tail tone elimination
768
            if element.get_name()=="repeater_tail_elimination":
769
                _mem.repeater_tail_elimination=RTE_LIST.index(str(element.value))
770

    
771
            #Logo string 1
772
            if element.get_name()=="logo1":
773
                b=str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
774
                _mem.logo_line1=b[0:12]+"\xff\xff\xff\xff"
775

    
776

    
777
            #Logo string 2
778
            if element.get_name()=="logo2":
779
                b=str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
780
                _mem.logo_line2=b[0:12]+"\xff\xff\xff\xff"
781

    
782

    
783

    
784

    
785
### unlock settings
786
            #FLOCK
787
            if element.get_name()=="flock":
788
                _mem.int_flock=FLOCK_LIST.index(str(element.value))
789

    
790
#350TX
791
            if element.get_name()=="350tx":
792
                _mem.int_350tx=element.value and 1 or 0
793

    
794
#UNKNOWN1
795
            if element.get_name()=="unknown1":
796
                _mem.int_unknown1=element.value and 1 or 0
797

    
798

    
799
#200TX
800
            if element.get_name()=="200tx":
801
                _mem.int_200tx=element.value and 1 or 0
802

    
803

    
804
#500TX
805
            if element.get_name()=="500tx":
806
                _mem.int_500tx=element.value and 1 or 0
807

    
808

    
809
#350EN
810
            if element.get_name()=="350en":
811
                _mem.int_350en=element.value and 1 or 0
812

    
813

    
814

    
815

    
816

    
817

    
818
    def get_settings(self):
819
        #print ("get_settings")
820
        _mem = self._memobj
821
        basic = RadioSettingGroup("basic", "Basic Settings")
822
        unlock = RadioSettingGroup("unlock", "Unlock Settings")
823
        fmradio = RadioSettingGroup("fmradio", "FM Radio (unimplemented yet)")
824
        roinfo = RadioSettingGroup("roinfo","Information (read-only)")
825

    
826
        top = RadioSettings(basic, unlock, fmradio,roinfo)
827

    
828
    #basic settings
829

    
830
        # call channel
831
        tmpc=_mem.call_channel+1
832
        if tmpc>200:
833
            tmpc=1
834
        rs = RadioSetting("call_channel", "One key call channel",RadioSettingValueInteger(1, 200, tmpc))
835
        basic.append(rs)
836

    
837
        # squelch
838
        tmpsq=_mem.squelch
839
        if tmpsq>9:
840
            tmpsq=1
841
        rs = RadioSetting("squelch", "Squelch", RadioSettingValueInteger(0, 9, tmpsq))
842
        basic.append(rs)
843

    
844
        # TOT
845
        tmptot=_mem.max_talk_time
846
        if tmptot>10:
847
            tmptot=10
848
        rs = RadioSetting("tot", "Max talk time [min]", RadioSettingValueInteger(0, 10, tmptot))
849
        basic.append(rs)
850

    
851
        # NOAA autoscan
852
        rs = RadioSetting("noaa_autoscan", "NOAA Autoscan", RadioSettingValueBoolean(bool(_mem.noaa_autoscan>0)))
853
        basic.append(rs)
854

    
855
        # VOX Level
856
        tmpvox=_mem.vox_level+1
857
        if tmpvox>10:
858
            tmpvox=10
859
        rs = RadioSetting("vox_level", "VOX Level", RadioSettingValueInteger(1, 10, tmpvox))
860
        basic.append(rs)
861

    
862
        # Mic gain
863
        tmpmicgain=_mem.mic_gain
864
        if tmpmicgain>4:
865
            tmpmicgain=4
866
        rs = RadioSetting("mic_gain", "Mic Gain", RadioSettingValueInteger(0, 4, tmpmicgain))
867
        basic.append(rs)
868

    
869
        # Channel display mode
870
        tmpchdispmode=_mem.channel_display_mode
871
        if tmpchdispmode>=len(CHANNELDISP_LIST):
872
            tmpchdispmode=0
873
        rs = RadioSetting("channel_display_mode", "Channel display mode", RadioSettingValueList( CHANNELDISP_LIST, CHANNELDISP_LIST[tmpchdispmode]))
874
        basic.append(rs)
875

    
876
        #Crossband receiving/transmitting
877
        tmpcross=_mem.crossband
878
        if tmpcross>=len(CROSSBAND_LIST):
879
            tmpcross=0
880
        rs = RadioSetting("crossband", "Cross-band receiving/transmitting", RadioSettingValueList( CROSSBAND_LIST, CROSSBAND_LIST[tmpcross]))
881
        basic.append(rs)
882

    
883
        #Battery save
884
        rs = RadioSetting("battery_save", "Battery Save", RadioSettingValueList( BATSAVE_LIST, BATSAVE_LIST[_mem.battery_save]))
885
        basic.append(rs)
886

    
887
        #Dual watch
888
        tmpdual=_mem.dual_watch
889
        if tmpdual>=len(DUALWATCH_LIST):
890
            tmpdual=0
891
        rs = RadioSetting("dualwatch", "Dual Watch", RadioSettingValueList( DUALWATCH_LIST, DUALWATCH_LIST[tmpdual]))
892
        basic.append(rs)
893

    
894
        #Tail tone elimination
895
        rs = RadioSetting("tail_note_elimination", "Tail tone elimination", RadioSettingValueBoolean(bool(_mem.tail_note_elimination>0)))
896
        basic.append(rs)
897

    
898
        #VFO open
899
        rs = RadioSetting("vfo_open", "VFO open", RadioSettingValueBoolean(bool(_mem.vfo_open>0)))
900
        basic.append(rs)
901

    
902
        #Beep control
903
        rs = RadioSetting("beep_control", "Beep control", RadioSettingValueBoolean(bool(_mem.beep_control>0)))
904
        basic.append(rs)
905

    
906
        #Scan resume mode
907
        tmpscanres=_mem.scan_resume_mode
908
        if tmpscanres>=len(SCANRESUME_LIST):
909
            tmpscanres=0
910
        rs = RadioSetting("scan_resume_mode", "Scan resume mode", RadioSettingValueList( SCANRESUME_LIST, SCANRESUME_LIST[tmpscanres]))
911
        basic.append(rs)
912

    
913
        #Auto keypad lock
914
        rs = RadioSetting("auto_keypad_lock", "Auto keypad lock", RadioSettingValueBoolean(bool(_mem.auto_keypad_lock>0)))
915
        basic.append(rs)
916

    
917

    
918
        #Power on display mode
919
        tmpdispmode=_mem.power_on_dispmode
920
        if tmpdispmode>=len(WELCOME_LIST):
921
            tmpdispmode=0
922
        rs = RadioSetting("welcome_mode", "Power on display mode", RadioSettingValueList( WELCOME_LIST, WELCOME_LIST[tmpdispmode]))
923
        basic.append(rs)
924

    
925
        #Keypad Tone
926
        tmpkeypadtone=_mem.keypad_tone
927
        if tmpkeypadtone>=len(KEYPADTONE_LIST):
928
            tmpkeypadtone=0
929
        rs = RadioSetting("keypad_tone", "Keypad tone", RadioSettingValueList( KEYPADTONE_LIST, KEYPADTONE_LIST[tmpkeypadtone]))
930
        basic.append(rs)
931

    
932
        #Language
933
        tmplanguage=_mem.language
934
        if tmplanguage>=len(LANGUAGE_LIST):
935
            tmplanguage=0
936
        rs = RadioSetting("language", "Language", RadioSettingValueList( LANGUAGE_LIST, LANGUAGE_LIST[tmplanguage]))
937
        basic.append(rs)
938

    
939
        # Alarm mode
940
        tmpalarmmode=_mem.alarm_mode
941
        if tmpalarmmode>=len(ALARMMODE_LIST):
942
            tmpalarmmode=0
943
        rs = RadioSetting("alarm_mode", "Alarm mode", RadioSettingValueList( ALARMMODE_LIST, ALARMMODE_LIST[tmpalarmmode]))
944
        basic.append(rs)
945

    
946
        # Reminding of end of talk
947
        tmpalarmmode=_mem.reminding_of_end_talk
948
        if tmpalarmmode>=len(REMENDOFTALK_LIST):
949
            tmpalarmmode=0
950
        rs = RadioSetting("reminding_of_end_talk", "Reminding of end of talk", RadioSettingValueList( REMENDOFTALK_LIST, REMENDOFTALK_LIST[tmpalarmmode]))
951
        basic.append(rs)
952

    
953
        # Repeater tail tone elimination
954
        tmprte=_mem.repeater_tail_elimination
955
        if tmprte>=len(RTE_LIST):
956
            tmprte=0
957
        rs = RadioSetting("repeater_tail_elimination", "Repeater tail tone elimination", RadioSettingValueList( RTE_LIST, RTE_LIST[tmprte]))
958
        basic.append(rs)
959

    
960
        #Logo string 1
961
        logo1=str(_mem.logo_line1).strip("\x20\x00\xff") #+"\x20"*12
962
        logo1=logo1[0:12]
963
        rs = RadioSetting("logo1", "Logo string 1 (12 characters)", RadioSettingValueString(0,12,logo1))
964
        basic.append(rs)
965

    
966

    
967
        #Logo string 2
968
        logo2=str(_mem.logo_line2).strip("\x20\x00\xff") #+"\x20"*12
969
        logo2=logo2[0:12]
970
        rs = RadioSetting("logo2", "Logo string 2 (12 characters)", RadioSettingValueString(0,12,logo2))
971
        basic.append(rs)
972

    
973

    
974

    
975

    
976

    
977
####unlock settings
978

    
979
        # F-LOCK
980
        tmpflock=_mem.int_flock
981
        if tmpflock>=len(FLOCK_LIST):
982
            tmpflock=0
983
        rs = RadioSetting("flock", "F-LOCK", RadioSettingValueList( FLOCK_LIST, FLOCK_LIST[tmpflock]))
984
        unlock.append(rs)
985

    
986
        #350TX
987
        rs = RadioSetting("350tx", "350TX", RadioSettingValueBoolean(bool(_mem.int_350tx>0)))
988
        unlock.append(rs)
989

    
990
        #unknown1
991
        rs = RadioSetting("unknown11", "UNKNOWN1", RadioSettingValueBoolean(bool(_mem.int_unknown1>0)))
992
        unlock.append(rs)
993

    
994
        #200TX
995
        rs = RadioSetting("200tx", "200TX", RadioSettingValueBoolean(bool(_mem.int_200tx>0)))
996
        unlock.append(rs)
997

    
998
        #500TX
999
        rs = RadioSetting("500tx", "500TX", RadioSettingValueBoolean(bool(_mem.int_500tx>0)))
1000
        unlock.append(rs)
1001

    
1002
        #350EN
1003
        rs = RadioSetting("350en", "350EN", RadioSettingValueBoolean(bool(_mem.int_350en>0)))
1004
        unlock.append(rs)
1005

    
1006
        #SCREEN
1007
        rs = RadioSetting("screen", "SCREEN", RadioSettingValueBoolean(bool(_mem.int_screen>0)))
1008
        unlock.append(rs)
1009

    
1010
### readonly info
1011
# Firmware
1012
        if self.FIRMWARE_VERSION=="":
1013
            firmware="To get the firmware version please download the image from the radio first"
1014
        else:
1015
            firmware=self.FIRMWARE_VERSION
1016
        rs = RadioSetting("fw_ver", "Firmware Version", RadioSettingValueString(0,128,firmware))
1017
        roinfo.append(rs)
1018

    
1019
# Driver version
1020
        rs = RadioSetting("driver_ver", "Driver version", RadioSettingValueString(0,128,DRIVER_VERSION))
1021
        roinfo.append(rs)
1022

    
1023
        return top
1024

    
1025
    # Store details about a high-level memory to the memory map
1026
    # This is called when a user edits a memory in the UI
1027
    def set_memory(self, mem):
1028
        number=mem.number-1
1029

    
1030

    
1031
        #print("set_memory ch=",mem.number);
1032
        # Get a low-level memory object mapped to the image
1033
        _mem = self._memobj.channel[number]
1034
        #empty memory
1035
        if mem.empty:
1036
            _mem.set_raw("\xFF" * 16)
1037
            if number<200:
1038
                _mem2 = self._memobj.channelname[number]
1039
                _mem2.set_raw("\xFF" * 16)
1040
            return mem
1041

    
1042
        # mode
1043
        if mem.mode == "AM":
1044
            _mem.flags1 = _mem.flags1 | FLAGS1_ISAM
1045
            _mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH
1046
        else:
1047
            _mem.flags1 = _mem.flags1 & ~FLAGS1_ISAM
1048
            if mem.mode == "NFM":
1049
                _mem.flags2 = _mem.flags2 | FLAGS2_BANDWIDTH
1050
            else:
1051
                _mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH
1052

    
1053
        # frequency/offset
1054
        _mem.freq = mem.freq/10
1055
        _mem.offset = mem.offset/10
1056

    
1057
        if mem.duplex == "off":
1058
            _mem.offset=0
1059
        elif mem.duplex == '-':
1060
            _mem.flags1=_mem.flags1|FLAGS1_OFFSET
1061
        elif mem.duplex == '+':
1062
            _mem.flags1=_mem.flags1&~FLAGS1_OFFSET
1063

    
1064
        #channels >200 are the 14 VFO chanells and don't have names
1065
        if number<200:
1066
            _mem2 = self._memobj.channelname[number]
1067
            tag=mem.name.ljust(16)[:16]
1068
            #print("tag=",tag)
1069
            _mem2.name = tag  # Store the alpha tag
1070

    
1071
                    # tone data
1072
        self._set_tone(mem, _mem)
1073

    
1074
        # step
1075
        _mem.step=STEPS.index(mem.tuning_step)<<1
1076

    
1077
        # tx power
1078
        if str(mem.power) == "High":
1079
            _mem.flags2 = (_mem.flags2 & ~FLAGS2_POWER_MASK ) | FLAGS2_POWER_HIGH
1080
        elif str(mem.power) == "Med":
1081
            _mem.flags2 = (_mem.flags2 & ~FLAGS2_POWER_MASK ) | FLAGS2_POWER_MEDIUM
1082
        else:
1083
            _mem.flags2 = (_mem.flags2 & ~FLAGS2_POWER_MASK ) 
1084

    
1085

    
1086
        for setting in mem.extra:
1087
            sname=setting.get_name()
1088
            svalue=setting.value.get_value()
1089
            #print("set_memory extra name",sname," value",svalue)
1090

    
1091
            if sname == "bclo":
1092
                if svalue:
1093
                    _mem.flags2 = _mem.flags2 | FLAGS2_BCLO
1094
                else:
1095
                    _mem.flags2 = _mem.flags2 & ~FLAGS2_BCLO
1096

    
1097
            if sname == "pttid":
1098
                _mem.dtmf_flags = (_mem.dtmf_flags & ~FLAGS_DTMF_PTTID_MASK) | (PTTID_LIST.index(svalue)<<1)
1099

    
1100
            if sname == "frev":
1101
                if svalue:
1102
                    _mem.flags2 = _mem.flags2 | FLAGS2_REVERSE;
1103
                else:
1104
                    _mem.flags2 = _mem.flags2 & ~FLAGS2_REVERSE;
1105

    
1106
            if sname == "dtmfdecode":
1107
                if svalue:
1108
                    _mem.dtmf_flags = _mem.dtmf_flags | FLAGS_DTMF_DECODE;
1109
                else:
1110
                    _mem.dtmf_flags = _mem.dtmf_flags & ~FLAGS_DTMF_DECODE;
1111

    
1112
            if sname == "scrambler":
1113
                _mem.scrambler = (_mem.scrambler & 0xf0) | SCRAMBLER_LIST.index(svalue)
1114

    
1115

    
1116

    
1117

    
1118
        return mem #not sure if needed?
1119

    
(3-3/47)